diff --git a/CHANGELOG.md b/CHANGELOG.md index de10779f666..ed202d3a267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master (unreleased) +### New features + +* [#7944](https://github.com/rubocop-hq/rubocop/issues/7944): Add `MaxUnannotatedPlaceholdersAllowed` option to `Style/FormatStringToken` cop. ([@Tietew][]) + ### Bug fixes * [#8892](https://github.com/rubocop-hq/rubocop/issues/8892): Fix an error for `Style/StringConcatenation` when correcting nested concatenable parts. ([@fatkodima][]) diff --git a/config/default.yml b/config/default.yml index f57334fa916..18d7238baa8 100644 --- a/config/default.yml +++ b/config/default.yml @@ -3109,8 +3109,12 @@ Style/FormatStringToken: # Prefer simple looking "template" style tokens like `%{name}`, `%{age}` - template - unannotated + # `MaxUnannotatedPlaceholdersAllowed` defines the number of `unannotated` + # style token in a format string to be allowed when enforced style is not + # `unannotated`. + MaxUnannotatedPlaceholdersAllowed: 1 VersionAdded: '0.49' - VersionChanged: '0.75' + VersionChanged: '1.0' Style/FrozenStringLiteralComment: Description: >- diff --git a/docs/modules/ROOT/pages/cops_style.adoc b/docs/modules/ROOT/pages/cops_style.adoc index 456bc89f1ef..ba1ddc1ba52 100644 --- a/docs/modules/ROOT/pages/cops_style.adoc +++ b/docs/modules/ROOT/pages/cops_style.adoc @@ -3405,7 +3405,7 @@ puts '%10s' % 'hoge' | Yes | No | 0.49 -| 0.75 +| 1.0 |=== Use a consistent style for named format string tokens. @@ -3416,6 +3416,10 @@ which are passed as arguments to those methods: The reason is that _unannotated_ format is very similar to encoded URLs or Date/Time formatting strings. +It is allowed to contain unannotated token +if the number of them is less than or equals to +`MaxUnannotatedPlaceholdersAllowed`. + === Examples ==== EnforcedStyle: annotated (default) @@ -3454,6 +3458,29 @@ format('%{greeting}', greeting: 'Hello') format('%s', 'Hello') ---- +==== MaxUnannotatedPlaceholdersAllowed: 0 + +[source,ruby] +---- +# bad +format('%06d', 10) +format('%s %s.', 'Hello', 'world') + +# good +format('%06d', number: 10) +---- + +==== MaxUnannotatedPlaceholdersAllowed: 1 (default) + +[source,ruby] +---- +# bad +format('%s %s.', 'Hello', 'world') + +# good +format('%06d', 10) +---- + === Configurable attributes |=== @@ -3462,6 +3489,10 @@ format('%s', 'Hello') | EnforcedStyle | `annotated` | `annotated`, `template`, `unannotated` + +| MaxUnannotatedPlaceholdersAllowed +| `1` +| Integer |=== == Style/FrozenStringLiteralComment diff --git a/lib/rubocop/cop/style/format_string_token.rb b/lib/rubocop/cop/style/format_string_token.rb index 688b06ada2d..d17d8505d24 100644 --- a/lib/rubocop/cop/style/format_string_token.rb +++ b/lib/rubocop/cop/style/format_string_token.rb @@ -37,6 +37,27 @@ module Style # # # good # format('%s', 'Hello') + # + # It is allowed to contain unannotated token + # if the number of them is less than or equals to + # `MaxUnannotatedPlaceholdersAllowed`. + # + # @example MaxUnannotatedPlaceholdersAllowed: 0 + # + # # bad + # format('%06d', 10) + # format('%s %s.', 'Hello', 'world') + # + # # good + # format('%06d', number: 10) + # + # @example MaxUnannotatedPlaceholdersAllowed: 1 (default) + # + # # bad + # format('%s %s.', 'Hello', 'world') + # + # # good + # format('%06d', 10) class FormatStringToken < Base include ConfigurableEnforcedStyle @@ -44,8 +65,12 @@ def on_str(node) return unless node.value.include?('%') return if node.each_ancestor(:xstr, :regexp).any? - tokens(node) do |detected_style, token_range| - if detected_style == style || unannotated_format?(node, detected_style) + detections = collect_detections(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) @@ -112,6 +137,26 @@ def token_ranges(contents) yield(detected_style, 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] + end + end + detections + end + + def allowed_unannotated?(detections) + return false if detections.size > max_unannotated_placeholders_allowed + + detections.all? { |detected_style,| detected_style == :unannotated } + end + + def max_unannotated_placeholders_allowed + cop_config['MaxUnannotatedPlaceholdersAllowed'] + end end end end diff --git a/spec/rubocop/cop/style/format_string_token_spec.rb b/spec/rubocop/cop/style/format_string_token_spec.rb index 3b1e8bbaa5a..bb008c8bb15 100644 --- a/spec/rubocop/cop/style/format_string_token_spec.rb +++ b/spec/rubocop/cop/style/format_string_token_spec.rb @@ -6,10 +6,41 @@ let(:cop_config) do { 'EnforcedStyle' => enforced_style, - 'SupportedStyles' => %i[annotated template unannotated] + 'SupportedStyles' => %i[annotated template unannotated], + 'MaxUnannotatedPlaceholdersAllowed' => 0 } end + shared_examples 'maximum allowed unannotated' do |token| + context 'when MaxUnannotatedPlaceholdersAllowed is 1' do + before { cop_config['MaxUnannotatedPlaceholdersAllowed'] = 1 } + + it 'does not register offenses for single unannotated' do + expect_no_offenses("format('%#{token}', foo)") + end + + it 'registers offence for dual unannotated' do + expect_offense(<<~RUBY) + format('%#{token} %s', foo, bar) + ^^ Prefer [...] + ^^ Prefer [...] + RUBY + end + end + + context 'when MaxUnannotatedPlaceholdersAllowed is 2' do + before { cop_config['MaxUnannotatedPlaceholdersAllowed'] = 2 } + + it 'does not register offenses for single unannotated' do + expect_no_offenses("format('%#{token}', foo)") + end + + it 'does not register offenses for dual unannotated' do + expect_no_offenses("format('%#{token} %s', foo, bar)") + end + end + end + shared_examples 'enforced styles for format string tokens' do |token| template = '%{template}' annotated = "%#{token}" @@ -54,6 +85,8 @@ 'EnforcedStyle' => 'template' ) end + + it_behaves_like 'maximum allowed unannotated', token end context 'when enforced style is template' do @@ -96,6 +129,8 @@ 'EnforcedStyle' => 'template' ) end + + it_behaves_like 'maximum allowed unannotated', token end end