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 committed Nov 23, 2023
1 parent 9d51eb0 commit 1eac336
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 32 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Master (Unreleased)

- Add support for `symbol` style for `RSpec/SharedExamples`. ([@jessieay])

## 2.25.0 (2023-10-27)

- Add support single quoted string and percent string and heredoc for `RSpec/Rails/HttpStatus`. ([@ydah])
Expand Down Expand Up @@ -856,6 +858,7 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features.
[@jaredbeck]: https://github.com/jaredbeck
[@jaredmoody]: https://github.com/jaredmoody
[@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 @@ -832,9 +832,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
40 changes: 38 additions & 2 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4984,13 +4984,20 @@ 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 @@ -5008,6 +5015,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
77 changes: 63 additions & 14 deletions lib/rubocop/cop/rspec/shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
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 +25,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 +55,38 @@ 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?(node.first_argument)

checker = Checker.new(ast_node)
checker = new_checker(ast_node)
add_offense(checker.node, message: checker.message) do |corrector|
corrector.replace(checker.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 +95,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
111 changes: 96 additions & 15 deletions spec/rubocop/cop/rspec/shared_examples_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::SharedExamples do
it 'registers an offense when using symbolic title' do
expect_offense(<<-RUBY)
context 'when using default (string) enforced style' do
it 'registers an offense when using symbolic title' do
expect_offense(<<-RUBY)
it_behaves_like :foo_bar_baz
^^^^^^^^^^^^ Prefer 'foo bar baz' over `:foo_bar_baz` to titleize shared examples.
it_should_behave_like :foo_bar_baz
Expand All @@ -16,33 +17,33 @@
include_examples :foo_bar_baz, 'foo', 'bar'
^^^^^^^^^^^^ Prefer 'foo bar baz' over `:foo_bar_baz` to titleize shared examples.
shared_examples :foo_bar_baz, 'foo', 'bar' do |param|
shared_examples :foo_bar_baz, 'foo', 'bar', :foo_bar, 5 do |param|
^^^^^^^^^^^^ Prefer 'foo bar baz' over `:foo_bar_baz` to titleize shared examples.
# ...
end
RSpec.shared_examples :foo_bar_baz
^^^^^^^^^^^^ Prefer 'foo bar baz' over `:foo_bar_baz` to titleize shared examples.
RUBY
RUBY

expect_correction(<<-RUBY)
expect_correction(<<-RUBY)
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'
include_examples 'foo bar baz', 'foo', 'bar'
shared_examples 'foo bar baz', 'foo', 'bar' do |param|
shared_examples 'foo bar baz', 'foo', 'bar', :foo_bar, 5 do |param|
# ...
end
RSpec.shared_examples 'foo bar baz'
RUBY
end
RUBY
end

it 'does not register an offense when using string title' do
expect_no_offenses(<<-RUBY)
it 'does not register an offense when using string title' do
expect_no_offenses(<<-RUBY)
it_behaves_like 'foo bar baz'
it_should_behave_like 'foo bar baz'
shared_examples 'foo bar baz'
Expand All @@ -53,11 +54,11 @@
shared_examples 'foo bar baz', 'foo', 'bar' do |param|
# ...
end
RUBY
end
RUBY
end

it 'does not register an offense when using Module/Class title' do
expect_no_offenses(<<-RUBY)
it 'does not register an offense when using Module/Class title' do
expect_no_offenses(<<-RUBY)
it_behaves_like FooBarBaz
it_should_behave_like FooBarBaz
shared_examples FooBarBaz
Expand All @@ -68,6 +69,86 @@
shared_examples FooBarBaz, 'foo', 'bar' do |param|
# ...
end
RUBY
RUBY
end
end

context 'when using symbol enforced style' do
let(:cop_config) { { 'EnforcedStyle' => 'symbol' } }

it 'registers an offense when using string title' do
expect_offense(<<-RUBY)
it_behaves_like 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
it_should_behave_like 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
shared_examples 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
shared_examples_for 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
include_examples 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
include_examples 'Foo Bar Baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"Foo Bar Baz"` to symbolize shared examples.
include_examples 'foo bar baz', 'foo', 'bar'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
shared_examples 'foo bar baz', 'foo', 'bar' do |param|
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
# ...
end
RSpec.shared_examples 'foo bar baz'
^^^^^^^^^^^^^ Prefer :foo_bar_baz over `"foo bar baz"` to symbolize shared examples.
RUBY

expect_correction(<<-RUBY)
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
include_examples :foo_bar_baz
include_examples :foo_bar_baz, 'foo', 'bar'
shared_examples :foo_bar_baz, 'foo', 'bar' do |param|
# ...
end
RSpec.shared_examples :foo_bar_baz
RUBY
end

it 'does not register an offense when using symbol' do
expect_no_offenses(<<-RUBY)
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
include_examples :foo_bar_baz, :foo, :bar
shared_examples :foo_bar_baz, 'foo', 'bar', 'foo bar', 5 do |param|
# ...
end
RSpec.shared_examples :foo_bar_baz
RUBY
end

it 'does not register an offense when using Module/Class title' do
expect_no_offenses(<<-RUBY)
it_behaves_like FooBarBaz
it_should_behave_like FooBarBaz
shared_examples FooBarBaz
shared_examples_for FooBarBaz
include_examples FooBarBaz
include_examples FooBarBaz, 'foo', 'bar'
shared_examples FooBarBaz, 'foo', 'bar', 'foo bar', 5 do |param|
# ...
end
RUBY
end
end
end

0 comments on commit 1eac336

Please sign in to comment.