Skip to content

Commit

Permalink
Add support for enforced symbolized shared examples
Browse files Browse the repository at this point in the history
* Existing check only allows to enforce one style, which is called
  "titelized" in the error message but really just looks for a string
  type.
* Enforcing the existing style raises an error when a shared example
  uses a symbol definition rather than a string.
* But there are some benefits to using symbol types instead, as is
  discussed here: https://gitlab.com/gitlab-org/gitlab/-/issues/427697
* As a result, some codebases might want to enforce a symbol type
  instead.
  • Loading branch information
jessieay authored and bquorning committed Jan 4, 2024
1 parent 200d96f commit 878bcb1
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 83 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Add support for correcting "it will" (future tense) for `RSpec/ExampleWording`. ([@jdufresne])
- Add new `RSpec/RemoveConst` cop. ([@swelther])
- Ensure `PendingWithoutReason` can detect violations inside shared groups. ([@robinaugh])
- Add support for `symbol` style for `RSpec/SharedExamples`. ([@jessieay])

## 2.25.0 (2023-10-27)

Expand Down Expand Up @@ -862,6 +863,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@jaredmoody]: https://github.com/jaredmoody
[@jdufresne]: https://github.com/jdufresne
[@jeffreyc]: https://github.com/jeffreyc
[@jessieay]: https://github.com/jessieay
[@jfragoulis]: https://github.com/jfragoulis
[@johnny-miyake]: https://github.com/johnny-miyake
[@jojos003]: https://github.com/jojos003
Expand Down
7 changes: 6 additions & 1 deletion config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -844,9 +844,14 @@ RSpec/SharedContext:
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedContext

RSpec/SharedExamples:
Description: Enforces use of string to titleize shared examples.
Description: Checks for consistent style for shared example names.
Enabled: true
EnforcedStyle: string
SupportedStyles:
- string
- symbol
VersionAdded: '1.25'
VersionChanged: "<<next>>"
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedExamples

RSpec/SingleArgumentMessageChain:
Expand Down
39 changes: 37 additions & 2 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5050,13 +5050,19 @@ end
| Yes
| Yes
| 1.25
| -
| <<next>>
|===

Enforces use of string to titleize shared examples.
Checks for consistent style for shared example names.

Enforces either `string` or `symbol` for shared example names.

This cop can be configured using the `EnforcedStyle` option

=== Examples

==== `EnforcedStyle: string` (default)

[source,ruby]
----
# bad
Expand All @@ -5074,6 +5080,35 @@ shared_examples_for 'foo bar baz'
include_examples 'foo bar baz'
----

==== `EnforcedStyle: symbol`

[source,ruby]
----
# bad
it_behaves_like 'foo bar baz'
it_should_behave_like 'foo bar baz'
shared_examples 'foo bar baz'
shared_examples_for 'foo bar baz'
include_examples 'foo bar baz'
# good
it_behaves_like :foo_bar_baz
it_should_behave_like :foo_bar_baz
shared_examples :foo_bar_baz
shared_examples_for :foo_bar_baz
include_examples :foo_bar_baz
----

=== Configurable attributes

|===
| Name | Default value | Configurable values

| EnforcedStyle
| `string`
| `string`, `symbol`
|===

=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SharedExamples
Expand Down
79 changes: 63 additions & 16 deletions lib/rubocop/cop/rspec/shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
module RuboCop
module Cop
module RSpec
# Enforces use of string to titleize shared examples.
# Checks for consistent style for shared example names.
#
# @example
# Enforces either `string` or `symbol` for shared example names.
#
# This cop can be configured using the `EnforcedStyle` option
#
# @example `EnforcedStyle: string` (default)
# # bad
# it_behaves_like :foo_bar_baz
# it_should_behave_like :foo_bar_baz
Expand All @@ -20,8 +24,24 @@ module RSpec
# shared_examples_for 'foo bar baz'
# include_examples 'foo bar baz'
#
# @example `EnforcedStyle: symbol`
# # bad
# it_behaves_like 'foo bar baz'
# it_should_behave_like 'foo bar baz'
# shared_examples 'foo bar baz'
# shared_examples_for 'foo bar baz'
# include_examples 'foo bar baz'
#
# # good
# it_behaves_like :foo_bar_baz
# it_should_behave_like :foo_bar_baz
# shared_examples :foo_bar_baz
# shared_examples_for :foo_bar_baz
# include_examples :foo_bar_baz
#
class SharedExamples < Base
extend AutoCorrector
include ConfigurableEnforcedStyle

# @!method shared_examples(node)
def_node_matcher :shared_examples, <<~PATTERN
Expand All @@ -34,19 +54,37 @@ class SharedExamples < Base
def on_send(node)
shared_examples(node) do
ast_node = node.first_argument
next unless ast_node&.sym_type?
next unless offense?(ast_node)

checker = Checker.new(ast_node)
add_offense(checker.node, message: checker.message) do |corrector|
corrector.replace(checker.node, checker.preferred_style)
checker = new_checker(ast_node)
add_offense(ast_node, message: checker.message) do |corrector|
corrector.replace(ast_node, checker.preferred_style)
end
end
end

private

def offense?(ast_node)
if style == :symbol
ast_node.str_type?
else # string
ast_node.sym_type?
end
end

def new_checker(ast_node)
if style == :symbol
SymbolChecker.new(ast_node)
else # string
StringChecker.new(ast_node)
end
end

# :nodoc:
class Checker
class SymbolChecker
MSG = 'Prefer %<prefer>s over `%<current>s` ' \
'to titleize shared examples.'
'to symbolize shared examples.'

attr_reader :node

Expand All @@ -55,22 +93,31 @@ def initialize(node)
end

def message
format(MSG, prefer: preferred_style, current: symbol.inspect)
format(MSG, prefer: preferred_style, current: node.value.inspect)
end

def preferred_style
string = symbol.to_s.tr('_', ' ')
wrap_with_single_quotes(string)
":#{node.value.to_s.downcase.tr(' ', '_')}"
end
end

# :nodoc:
class StringChecker
MSG = 'Prefer %<prefer>s over `%<current>s` ' \
'to titleize shared examples.'

private
attr_reader :node

def symbol
node.value
def initialize(node)
@node = node
end

def wrap_with_single_quotes(string)
"'#{string}'"
def message
format(MSG, prefer: preferred_style, current: node.value.inspect)
end

def preferred_style
"'#{node.value.to_s.tr('_', ' ')}'"
end
end
end
Expand Down
Loading

0 comments on commit 878bcb1

Please sign in to comment.