Skip to content

Commit

Permalink
Add new RSpec/SortMetadata cop
Browse files Browse the repository at this point in the history
This cop sorts the RSpec metadata alphabetically:

```ruby
  # bad
  describe 'Something', :b, :a
  context 'Something', foo: 'bar', baz: true
  it 'works', :b, :a, foo: 'bar', baz: true

  # good
  describe 'Something', :a, :b
  context 'Something', baz: true, foo: 'bar'
  it 'works', :a, :b, baz: true, foo: 'bar'
```
  • Loading branch information
leoarnold authored and bquorning committed Oct 21, 2022
1 parent fb2ccea commit 1858d16
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ RSpec/IdenticalEqualityAssertion:
Enabled: true
RSpec/NoExpectationExample:
Enabled: true
RSpec/SortMetadata:
Enabled: true
RSpec/SubjectDeclaration:
Enabled: true
RSpec/VerifiedDoubleReference:
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,12 @@ RSpec/SingleArgumentMessageChain:
VersionChanged: '1.10'
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain

RSpec/SortMetadata:
Description: Sort RSpec metadata alphabetically.
Enabled: pending
VersionAdded: "<<next>>"
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SortMetadata

RSpec/StubbedMock:
Description: Checks that message expectations do not have a configured response.
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
* xref:cops_rspec.adoc#rspecsharedcontext[RSpec/SharedContext]
* xref:cops_rspec.adoc#rspecsharedexamples[RSpec/SharedExamples]
* xref:cops_rspec.adoc#rspecsingleargumentmessagechain[RSpec/SingleArgumentMessageChain]
* xref:cops_rspec.adoc#rspecsortmetadata[RSpec/SortMetadata]
* xref:cops_rspec.adoc#rspecstubbedmock[RSpec/StubbedMock]
* xref:cops_rspec.adoc#rspecsubjectdeclaration[RSpec/SubjectDeclaration]
* xref:cops_rspec.adoc#rspecsubjectstub[RSpec/SubjectStub]
Expand Down
33 changes: 33 additions & 0 deletions docs/modules/ROOT/pages/cops_rspec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4464,6 +4464,39 @@ allow(foo).to receive("bar.baz")

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain

== RSpec/SortMetadata

|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed

| Pending
| Yes
| Yes
| <<next>>
| -
|===

Sort RSpec metadata alphabetically.

=== Examples

[source,ruby]
----
# bad
describe 'Something', :b, :a
context 'Something', foo: 'bar', baz: true
it 'works', :b, :a, foo: 'bar', baz: true
# good
describe 'Something', :a, :b
context 'Something', baz: true, foo: 'bar'
it 'works', :a, :b, baz: true, foo: 'bar'
----

=== References

* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SortMetadata

== RSpec/StubbedMock

|===
Expand Down
100 changes: 100 additions & 0 deletions lib/rubocop/cop/rspec/sort_metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpec
# Sort RSpec metadata alphabetically.
#
# @example
# # bad
# describe 'Something', :b, :a
# context 'Something', foo: 'bar', baz: true
# it 'works', :b, :a, foo: 'bar', baz: true
#
# # good
# describe 'Something', :a, :b
# context 'Something', baz: true, foo: 'bar'
# it 'works', :a, :b, baz: true, foo: 'bar'
#
class SortMetadata < Base
extend AutoCorrector
include RangeHelp

MSG = 'Sort metadata alphabetically.'

# @!method rspec_metadata(node)
def_node_matcher :rspec_metadata, <<~PATTERN
(block
(send
#rspec? {#Examples.all #ExampleGroups.all #SharedGroups.all #Hooks.all #Includes.all}
_ ${send str sym}* (hash $...)?)
...)
PATTERN

# @!method rspec_configure(node)
def_node_matcher :rspec_configure, <<~PATTERN
(block (send #rspec? :configure) (args (arg $_)) ...)
PATTERN

# @!method metadata_in_block(node)
def_node_search :metadata_in_block, <<~PATTERN
(send (lvar $_) #Hooks.all _ ${send str sym}* (hash $...)?)
PATTERN

def on_block(node)
if (block_var = rspec_configure(node))
metadata_in_block(node).each do |receiver, symbols, pairs|
investigate(symbols, pairs.flatten) if receiver == block_var
end
elsif (symbols, pairs = rspec_metadata(node))
investigate(symbols, pairs.flatten)
end
end

private

def investigate(symbols, pairs)
return if sorted?(symbols, pairs)
return unless (crime_scene = crime_scene(symbols, pairs))

add_offense(crime_scene) do |corrector|
corrector.replace(crime_scene, replacement(symbols, pairs))
end
end

def crime_scene(symbols, pairs)
metadata = symbols + pairs

range = range_between(
metadata.first.loc.expression.begin_pos,
metadata.last.loc.expression.end_pos
)

range if range.last_line == range.first_line
end

def replacement(symbols, pairs)
(sort_symbols(symbols) + sort_pairs(pairs)).map(&:source).join(', ')
end

def sorted?(symbols, pairs)
symbols == sort_symbols(symbols) && pairs == sort_pairs(pairs)
end

def sort_pairs(pairs)
pairs.sort_by { |pair| pair.key.source.downcase }
end

def sort_symbols(symbols)
symbols.sort_by do |symbol|
if %i[str sym].include?(symbol.type)
symbol.value.to_s.downcase
else
symbol.source.downcase
end
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
require_relative 'rspec/shared_context'
require_relative 'rspec/shared_examples'
require_relative 'rspec/single_argument_message_chain'
require_relative 'rspec/sort_metadata'
require_relative 'rspec/stubbed_mock'
require_relative 'rspec/subject_declaration'
require_relative 'rspec/subject_stub'
Expand Down
217 changes: 217 additions & 0 deletions spec/rubocop/cop/rspec/sort_metadata_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::RSpec::SortMetadata do
it 'does not register an offense when using only symbol metadata ' \
'in alphabetical order' do
expect_no_offenses(<<~RUBY)
RSpec.describe 'Something', :a, :b do
end
RUBY
end

it 'registers an offense when using only symbol metadata, ' \
'but not in alphabetical order' do
expect_offense(<<~RUBY)
RSpec.describe 'Something', :b, :a do
^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
RSpec.describe 'Something', :a, :b do
end
RUBY
end

it 'does not register an offense when using only a hash of metadata ' \
'with keys in alphabetical order' do
expect_no_offenses(<<~RUBY)
context 'Something', baz: true, foo: 'bar' do
end
RUBY
end

it 'registers an offense when using only a hash of metadata, ' \
'but with keys not in alphabetical order' do
expect_offense(<<~RUBY)
context 'Something', foo: 'bar', baz: true do
^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
context 'Something', baz: true, foo: 'bar' do
end
RUBY
end

it 'does not register an offense when using mixed metadata ' \
'in alphabetical order (respectively)' do
expect_no_offenses(<<~RUBY)
it 'Something', :a, :b, baz: true, foo: 'bar' do
end
RUBY
end

it 'registers an offense when using mixed metadata, ' \
'but only the hash keys are in alphabetical order' do
expect_offense(<<~RUBY)
it 'Something', :b, :a, baz: true, foo: 'bar' do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
it 'Something', :a, :b, baz: true, foo: 'bar' do
end
RUBY
end

it 'registers an offense when using mixed metadata, ' \
'but only the symbol keys are in alphabetical order' do
expect_offense(<<~RUBY)
it 'Something', :a, :b, foo: 'bar', baz: true do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
it 'Something', :a, :b, baz: true, foo: 'bar' do
end
RUBY
end

it 'registers an offense when using mixed metadata ' \
'and both symbols metadata and hash keys are not in alphabetical order' do
expect_offense(<<~RUBY)
it 'Something', :b, :a, foo: 'bar', baz: true do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
it 'Something', :a, :b, baz: true, foo: 'bar' do
end
RUBY
end

it 'registers an offense when using mixed metadata ' \
'and both symbols metadata and hash keys are not in alphabetical order ' \
'and the hash values are complex objects' do
expect_offense(<<~RUBY)
it 'Something', variable, 'B', :a, key => {}, foo: ->(x) { bar(x) }, Identifier.sample => true, baz: Snafu.new do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
it 'Something', :a, 'B', variable, baz: Snafu.new, foo: ->(x) { bar(x) }, Identifier.sample => true, key => {} do
end
RUBY
end

it 'registers an offense only when example or group has a block' do
expect_offense(<<~RUBY)
shared_examples 'a difficult situation', 'B', :a do |x, y|
^^^^^^^ Sort metadata alphabetically.
end
include_examples 'a difficult situation', 'value', 'another value'
RUBY

expect_correction(<<~RUBY)
shared_examples 'a difficult situation', :a, 'B' do |x, y|
end
include_examples 'a difficult situation', 'value', 'another value'
RUBY
end

it 'does not register an offense ' \
'when the metadata is not on one single line' do
expect_no_offenses(<<~RUBY)
RSpec.describe 'Something', :foo, :bar,
baz: 'goo' do
end
RUBY
end

it 'registers an offense when using only symbol metadata ' \
'in a config block, but not in alphabetical order' do
expect_offense(<<~RUBY)
RSpec.configure do |c|
c.before(:each, :b, :a) { freeze_time }
^^^^^^ Sort metadata alphabetically.
c.after(:each, foo: 'bar', baz: true) { travel_back }
^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
RUBY

expect_correction(<<~RUBY)
RSpec.configure do |c|
c.before(:each, :a, :b) { freeze_time }
c.after(:each, baz: true, foo: 'bar') { travel_back }
end
RUBY
end

context 'when using custom RSpec language ' \
'without adjusting the RuboCop RSpec language configuration' do
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
RSpec.describan "Algo", :b, :a do
contexto_compartido 'una situación complicada', foo: 'bar', baz: true do
end
ejemplo "hablando español", foo: 'bar', baz: true do
end
end
RUBY
end
end

context 'when using custom RSpec language ' \
'and adjusting the RuboCop RSpec language configuration' do
before do
other_cops.tap do |config|
config.dig('RSpec', 'Language', 'Includes', 'Context').push(
'describan', 'contexto_compartido'
)
config.dig('RSpec', 'Language', 'Includes', 'Examples').push('ejemplo')
end
end

let(:language_config) do
<<~YAML
RSpec:
Language:
ExampleGroups:
Regular:
- describan
Examples:
Regular:
- ejemplo
Hooks:
- antes
SharedGroups:
Context:
- contexto_compartido
YAML
end

it 'registers an offense' do
expect_offense(<<~RUBY)
RSpec.describan "Algo", :b, :a do
^^^^^^ Sort metadata alphabetically.
contexto_compartido 'una situación complicada', foo: 'bar', baz: true do
^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
ejemplo "hablando español", foo: 'bar', baz: true do
^^^^^^^^^^^^^^^^^^^^^ Sort metadata alphabetically.
end
end
RUBY
end
end
end

0 comments on commit 1858d16

Please sign in to comment.