Skip to content

Commit

Permalink
[Fix #7944] Add MaxUnannotatedPlaceholdersAllowed option to `Style/…
Browse files Browse the repository at this point in the history
…FormatStringToken` cop.

`MaxUnannotatedPlaceholdersAllowed` defines the number of `unannotated`
style token in a format string to be allowed when enforced style is not
`unannotated`.
  • Loading branch information
Tietew authored and bbatsov committed Oct 16, 2020
1 parent 6ac7cd4 commit d6b1516
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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][])
Expand Down
6 changes: 5 additions & 1 deletion config/default.yml
Expand Up @@ -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: >-
Expand Down
33 changes: 32 additions & 1 deletion docs/modules/ROOT/pages/cops_style.adoc
Expand Up @@ -3405,7 +3405,7 @@ puts '%10s' % 'hoge'
| Yes
| No
| 0.49
| 0.75
| 1.0
|===

Use a consistent style for named format string tokens.
Expand All @@ -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)
Expand Down Expand Up @@ -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('%<number>06d', number: 10)
----

==== MaxUnannotatedPlaceholdersAllowed: 1 (default)

[source,ruby]
----
# bad
format('%s %s.', 'Hello', 'world')
# good
format('%06d', 10)
----

=== Configurable attributes

|===
Expand All @@ -3462,6 +3489,10 @@ format('%s', 'Hello')
| EnforcedStyle
| `annotated`
| `annotated`, `template`, `unannotated`

| MaxUnannotatedPlaceholdersAllowed
| `1`
| Integer
|===

== Style/FrozenStringLiteralComment
Expand Down
49 changes: 47 additions & 2 deletions lib/rubocop/cop/style/format_string_token.rb
Expand Up @@ -37,15 +37,40 @@ 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('%<number>06d', number: 10)
#
# @example MaxUnannotatedPlaceholdersAllowed: 1 (default)
#
# # bad
# format('%s %s.', 'Hello', 'world')
#
# # good
# format('%06d', 10)
class FormatStringToken < Base
include ConfigurableEnforcedStyle

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)
Expand Down Expand Up @@ -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
Expand Down
37 changes: 36 additions & 1 deletion spec/rubocop/cop/style/format_string_token_spec.rb
Expand Up @@ -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 = "%<named>#{token}"
Expand Down Expand Up @@ -54,6 +85,8 @@
'EnforcedStyle' => 'template'
)
end

it_behaves_like 'maximum allowed unannotated', token
end

context 'when enforced style is template' do
Expand Down Expand Up @@ -96,6 +129,8 @@
'EnforcedStyle' => 'template'
)
end

it_behaves_like 'maximum allowed unannotated', token
end
end

Expand Down

0 comments on commit d6b1516

Please sign in to comment.