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 1d1fce89179..f33008ddafb 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 @@ -105,6 +101,38 @@ def use_ignored_method?(node) (parent = node.parent) && parent.send_type? && ignored_method?(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 @@ -142,30 +170,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 99ce2c7ffa1..4e2fc69d237 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 = '%