From 8eb4f1b99ac2c4d28a7aa22ad0e953544f082916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Sun, 3 Oct 2021 12:18:26 -0700 Subject: [PATCH] [Fix #7900] Fix `Style/FormatStringToken` false positive with formatted input and `template` style enforced, and add autocorrection --- ...tyle_format_string_token_false_positive.md | 1 + lib/rubocop/cop/style/format_string_token.rb | 65 +++++-- .../cop/style/format_string_token_spec.rb | 177 ++++++++++++------ 3 files changed, 171 insertions(+), 72 deletions(-) create mode 100644 changelog/fix_fix_style_format_string_token_false_positive.md diff --git a/changelog/fix_fix_style_format_string_token_false_positive.md b/changelog/fix_fix_style_format_string_token_false_positive.md new file mode 100644 index 00000000000..36eb675ed99 --- /dev/null +++ b/changelog/fix_fix_style_format_string_token_false_positive.md @@ -0,0 +1 @@ +* [#7900](https://github.com/rubocop/rubocop/issues/7900): Fix `Style/FormatStringToken` false positive with formatted input and `template` style enforced, and add autocorrection. ([@FnControlOption][]) diff --git a/lib/rubocop/cop/style/format_string_token.rb b/lib/rubocop/cop/style/format_string_token.rb index 5d2256ed644..62231058f3b 100644 --- a/lib/rubocop/cop/style/format_string_token.rb +++ b/lib/rubocop/cop/style/format_string_token.rb @@ -69,6 +69,7 @@ module Style class FormatStringToken < Base include ConfigurableEnforcedStyle include IgnoredMethods + extend AutoCorrector def on_str(node) return if format_string_token?(node) || use_ignored_method?(node) @@ -77,13 +78,8 @@ def on_str(node) return if detections.empty? return if allowed_unannotated?(detections) - detections.each do |detected_style, token_range| - if detected_style == style - correct_style_detected - else - style_detected(detected_style) - add_offense(token_range, message: message(detected_style)) - end + detections.each do |detected_sequence, token_range| + check_sequence(detected_sequence, token_range) end end @@ -106,6 +102,38 @@ def use_ignored_method?(node) send_parent && ignored_method?(send_parent.method_name) end + def check_sequence(detected_sequence, token_range) + if detected_sequence.style == style + correct_style_detected + elsif correctable_sequence?(detected_sequence.type) + style_detected(detected_sequence.style) + add_offense(token_range, message: message(detected_sequence.style)) do |corrector| + autocorrect_sequence(corrector, detected_sequence, token_range) + end + end + end + + def correctable_sequence?(detected_type) + detected_type == 's' || style == :annotated || style == :unannotated + end + + def autocorrect_sequence(corrector, detected_sequence, token_range) + return if style == :unannotated + + name = detected_sequence.name + return if name.nil? + + flags = detected_sequence.flags + width = detected_sequence.width + precision = detected_sequence.precision + type = detected_sequence.style == :template ? 's' : detected_sequence.type + correction = case style + when :annotated then "%<#{name}>#{flags}#{width}#{precision}#{type}" + when :template then "%#{flags}#{width}#{precision}{#{name}}" + end + corrector.replace(token_range, correction) + end + def unannotated_format?(node, detected_style) detected_style == :unannotated && !format_string_in_typical_context?(node) end @@ -143,30 +171,33 @@ def str_contents(source_map) def token_ranges(contents) format_string = RuboCop::Cop::Utils::FormatString.new(contents.source) - format_string.format_sequences.each do |seq| - next if seq.percent? + format_string.format_sequences.each do |detected_sequence| + next if detected_sequence.percent? - detected_style = seq.style - token = contents.begin.adjust(begin_pos: seq.begin_pos, end_pos: seq.end_pos) + token = contents.begin.adjust(begin_pos: detected_sequence.begin_pos, + end_pos: detected_sequence.end_pos) - yield(detected_style, token) + yield(detected_sequence, token) end end def collect_detections(node) detections = [] - tokens(node) do |detected_style, token_range| - unless unannotated_format?(node, detected_style) - detections << [detected_style, token_range] + tokens(node) do |detected_sequence, token_range| + unless unannotated_format?(node, detected_sequence.style) + detections << [detected_sequence, token_range] end end detections end def allowed_unannotated?(detections) - return false if detections.size > max_unannotated_placeholders_allowed + return false unless detections.all? do |detected_sequence,| + detected_sequence.style == :unannotated + end + return true if detections.size <= max_unannotated_placeholders_allowed - detections.all? { |detected_style,| detected_style == :unannotated } + detections.any? { |detected_sequence,| !correctable_sequence?(detected_sequence.type) } end def max_unannotated_placeholders_allowed diff --git a/spec/rubocop/cop/style/format_string_token_spec.rb b/spec/rubocop/cop/style/format_string_token_spec.rb index 2e1fe1d036a..a298763a1be 100644 --- a/spec/rubocop/cop/style/format_string_token_spec.rb +++ b/spec/rubocop/cop/style/format_string_token_spec.rb @@ -13,7 +13,7 @@ } end - shared_examples 'maximum allowed unannotated' do |token| + shared_examples 'maximum allowed unannotated' do |token, correctable_sequence:| context 'when MaxUnannotatedPlaceholdersAllowed is 1' do before { cop_config['MaxUnannotatedPlaceholdersAllowed'] = 1 } @@ -21,12 +21,20 @@ expect_no_offenses("format('%#{token}', foo)") end - it 'registers offense for dual unannotated' do - expect_offense(<<~RUBY) - format('%#{token} %s', foo, bar) - ^^ Prefer [...] - ^^ Prefer [...] - RUBY + if correctable_sequence + it 'registers offense for dual unannotated' do + expect_offense(<<~RUBY) + format('%#{token} %s', foo, bar) + ^^ Prefer [...] + ^^ Prefer [...] + RUBY + end + else + it 'does not register offenses for dual unannotated' do + expect_no_offenses(<<~RUBY) + format('%#{token} %s', foo, bar) + RUBY + end end end @@ -43,13 +51,28 @@ end end - shared_examples 'enforced styles for format string tokens' do |token| + shared_examples 'enforced styles for format string tokens' do |token, template_correction:| template = '%{template}' annotated = "%#{token}" + template_to_annotated = '%