Skip to content

Commit

Permalink
Add AllowedPatterns configuration option to RSpec/ContextWording
Browse files Browse the repository at this point in the history
Resolve: #1356
  • Loading branch information
ydah committed Aug 31, 2022
1 parent f149228 commit b6ba8de
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Add support for numblocks to `RSpec/AroundBlock`, `RSpec/EmptyLineAfterHook`, `RSpec/ExpectInHook`, `RSpec/HookArgument`, `RSpec/HooksBeforeExamples`, `RSpec/IteratedExpectation`, and `RSpec/NoExpectationExample`. ([@ydah][])
* Fix incorrect documentation URLs when using `rubocop --show-docs-url`. ([@r7kamura][])
* Add `AllowedGroups` configuration option to `RSpec/NestedGroups`. ([@ydah][])
* Add `AllowedPatterns` configuration option to `RSpec/ContextWording`. ([@ydah][])

## 2.12.1 (2022-07-03)

Expand Down
3 changes: 2 additions & 1 deletion config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ RSpec/ContextWording:
- when
- with
- without
AllowedPatterns: []
VersionAdded: '1.20'
VersionChanged: 1.20.1
VersionChanged: '2.13'
StyleGuide: https://rspec.rubystyle.guide/#context-descriptions
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ContextWording

Expand Down
32 changes: 31 additions & 1 deletion docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ end
| Yes
| No
| 1.20
| 1.20.1
| 2.13
|===

Checks that `context` docstring starts with an allowed prefix.
Expand All @@ -534,6 +534,9 @@ the configuration to meet project needs. Other acceptable prefixes may
include `if`, `unless`, `for`, `before`, `after`, or `during`.
They may consist of multiple words if desired.

This cop can be customized allowed context description pattern
with `AllowedPatterns`. By default, there are no checking by pattern.

=== Examples

==== `Prefixes` configuration
Expand Down Expand Up @@ -564,6 +567,29 @@ context 'when the display name is not present' do
end
----

==== `AllowedPatterns` configuration

[source,ruby]
----
# .rubocop.yml
# RSpec/ContextWording:
# AllowedPatterns:
# - /とき$/
----

[source,ruby]
----
# bad
context '条件を満たす' do
# ...
end
# good
context '条件を満たすとき' do
# ...
end
----

=== Configurable attributes

|===
Expand All @@ -572,6 +598,10 @@ end
| Prefixes
| `when`, `with`, `without`
| Array

| AllowedPatterns
| `[]`
| Array
|===

=== References
Expand Down
92 changes: 79 additions & 13 deletions lib/rubocop/cop/rspec/context_wording.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,107 @@ module RSpec
# # ...
# end
#
# This cop can be customized allowed context description pattern
# with `AllowedPatterns`. By default, there are no checking by pattern.
#
# @example `AllowedPatterns` configuration
#
# # .rubocop.yml
# # RSpec/ContextWording:
# # AllowedPatterns:
# # - /とき$/
#
# @example
# # bad
# context '条件を満たす' do
# # ...
# end
#
# # good
# context '条件を満たすとき' do
# # ...
# end
#
class ContextWording < Base
MSG = 'Start context description with %<prefixes>s.'
include AllowedPattern

PREFIX_MSG = 'Start context description with %<prefixes>s.'
PATTERN_MSG = 'Context description should match %<patterns>s.'
BOTH_MSG = 'Context description should begin with %<prefixes>s ' \
'or match %<patterns>s.'

# @!method context_wording(node)
def_node_matcher :context_wording, <<-PATTERN
(block (send #rspec? { :context :shared_context } $(str #bad_prefix?) ...) ...)
(block (send #rspec? { :context :shared_context } $(str $_) ...) ...)
PATTERN

def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
context_wording(node) do |context|
add_offense(context,
message: format(MSG, prefixes: joined_prefixes))
context_wording(node) do |context, description|
if !prefixes.empty? && !allowed_patterns.empty?
prefix_and_pattern_check(context, description)
next
end

offense(context, message_prefix) if bad_prefix?(description)
offense(context, message_patterns) if bad_pattern?(description)
end
end

private

def prefix_and_pattern_check(context, description)
if !match_prefix?(description) && !match_pattern?(description)
offense(context, message_both)
end
end

def bad_prefix?(description)
!prefix_regex.match?(description)
return false if prefixes.empty?

!match_prefix?(description)
end

def bad_pattern?(description)
return false if allowed_patterns.empty?

!match_pattern?(description)
end

def match_prefix?(description)
/^#{Regexp.union(prefixes)}\b/.match?(description)
end

def match_pattern?(description)
matches_allowed_pattern?(description)
end

def offense(context, message)
add_offense(context, message: message)
end

def message_prefix
format(PREFIX_MSG, prefixes: expect_word(prefixes))
end

def message_patterns
format(PATTERN_MSG, patterns: expect_word(allowed_patterns))
end

def joined_prefixes
quoted = prefixes.map { |prefix| "'#{prefix}'" }
def message_both
format(BOTH_MSG, prefixes: expect_word(prefixes),
patterns: expect_word(allowed_patterns))
end

def expect_word(config_values)
quoted = config_values.map { |value| "'#{value}'" }
return quoted.first if quoted.size == 1

quoted << "or #{quoted.pop}"
quoted.join(', ')
end

def prefixes
cop_config['Prefixes'] || []
end

def prefix_regex
/^#{Regexp.union(prefixes)}\b/
cop_config['Prefixes']
end
end
end
Expand Down
79 changes: 75 additions & 4 deletions spec/rubocop/cop/rspec/context_wording_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::ContextWording do
let(:cop_config) { { 'Prefixes' => %w[when with] } }
let(:cop_config) do
{ 'Prefixes' => %w[when with], 'AllowedPatterns' => [] }
end

it 'skips describe blocks' do
expect_no_offenses(<<-RUBY)
Expand Down Expand Up @@ -79,7 +81,7 @@
end

context 'when configured' do
let(:cop_config) { { 'Prefixes' => %w[if] } }
let(:cop_config) { { 'Prefixes' => %w[if], 'AllowedPatterns' => [] } }

it 'finds context without allowed prefixes at the beginning' do
expect_offense(<<-RUBY)
Expand All @@ -97,7 +99,9 @@
end

context 'with a multi-word prefix' do
let(:cop_config) { { 'Prefixes' => ['assuming that'] } }
let(:cop_config) do
{ 'Prefixes' => ['assuming that'], 'AllowedPatterns' => [] }
end

it 'skips descriptions with allowed multi-word prefixes' do
expect_no_offenses(<<-RUBY)
Expand All @@ -108,7 +112,7 @@
end

context 'with special regex characters' do
let(:cop_config) { { 'Prefixes' => ['a$b\d'] } }
let(:cop_config) { { 'Prefixes' => ['a$b\d'], 'AllowedPatterns' => [] } }

it 'matches the full prefix' do
expect_offense(<<-RUBY)
Expand All @@ -125,5 +129,72 @@
RUBY
end
end

context 'when `AllowedPatterns: [/とき$/]`' do
let(:cop_config) do
{ 'Prefixes' => [], 'AllowedPatterns' => [/とき$/] }
end

it 'finds context without `とき` at the ending' do
expect_offense(<<-RUBY)
context '条件を満たす' do
^^^^^^^^ Context description should match '(?-mix:とき$)'.
end
RUBY
end

it 'finds shared_context without `とき` at the ending' do
expect_offense(<<-RUBY)
shared_context '条件を満たす' do
^^^^^^^^ Context description should match '(?-mix:とき$)'.
end
RUBY
end

it "skips descriptions ending with 'とき'" do
expect_no_offenses(<<-RUBY)
context '条件を満たすとき' do
end
RUBY
end
end

context 'when `Prefixes: [when]` and `AllowedPatterns: [/patterns/]`' do
let(:cop_config) do
{ 'Prefixes' => %w[when], 'AllowedPatterns' => [/patterns/] }
end

it 'finds context without `when` at the beginning and not included ' \
'`/patterns/`' do
expect_offense(<<-RUBY)
context 'this is an incorrect context' do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Context description should begin with 'when' or match '(?-mix:patterns)'.
end
RUBY
end

it 'finds shared_context without `when` at the beginning and ' \
'not included `/patterns/`' do
expect_offense(<<-RUBY)
shared_context 'this is an incorrect context' do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Context description should begin with 'when' or match '(?-mix:patterns)'.
end
RUBY
end

it "skips descriptions beginning with 'when'" do
expect_no_offenses(<<-RUBY)
context 'when this is vaild context' do
end
RUBY
end

it "skips descriptions include with 'patterns'" do
expect_no_offenses(<<-RUBY)
context 'this is valid patterns context' do
end
RUBY
end
end
end
end

0 comments on commit b6ba8de

Please sign in to comment.