-
-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect repeated descriptions in example groups #259
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
module RuboCop | ||
module Cop | ||
module RSpec | ||
# Check for repeated description strings in example groups. | ||
# | ||
# @example | ||
# | ||
# # bad | ||
# RSpec.describe User do | ||
# it 'is valid' do | ||
# # ... | ||
# end | ||
# | ||
# it 'is valid' do | ||
# # ... | ||
# end | ||
# end | ||
# | ||
# # good | ||
# RSpec.describe User do | ||
# it 'is valid when first and last name are present' do | ||
# # ... | ||
# end | ||
# | ||
# it 'is valid when last name only is present' do | ||
# # ... | ||
# end | ||
# end | ||
# | ||
class RepeatedDescription < Cop | ||
def_node_matcher :example, <<-PATTERN | ||
(block $(send _ #{Examples::ALL.node_pattern_union} str ...) ...) | ||
PATTERN | ||
|
||
# @!method scope_change?(node) | ||
# | ||
# Detect if the node is an example group or shared example | ||
# | ||
# Selectors which indicate that we should stop searching | ||
# | ||
def_node_matcher :scope_change?, | ||
(ExampleGroups::ALL + SharedGroups::ALL).block_pattern | ||
|
||
MSG = "Don't repeat descriptions within an example group.".freeze | ||
|
||
def on_block(node) | ||
return unless example_group?(node) | ||
|
||
repeated_descriptions(node).each do |repeated_hook| | ||
add_offense(repeated_hook, :expression) | ||
end | ||
end | ||
|
||
private | ||
|
||
# Select examples in the current scope with repeated description strings | ||
def repeated_descriptions(node) | ||
examples_in_scope(node) # Select examples in example group | ||
.group_by(&:method_args) # Group examples by description string | ||
.values # Reduce to array of grouped examples | ||
.reject(&:one?) # Reject groups with only one example | ||
.flatten # Flatten down to array of offending nodes | ||
end | ||
|
||
def examples_in_scope(node, &block) | ||
return to_enum(__method__, node) unless block_given? | ||
|
||
node.each_child_node { |child| find_examples(child, &block) } | ||
end | ||
|
||
# Recursively search for examples within the current scope | ||
# | ||
# Searches node for examples and halts when a scope change is detected | ||
# | ||
# @param node [RuboCop::Node] node to recursively search for examples | ||
# | ||
# @yield [RuboCop::Node] discovered example nodes | ||
def find_examples(node, &block) | ||
return if scope_change?(node) | ||
|
||
example(node, &block) | ||
examples_in_scope(node, &block) | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# frozen_string_literal: true | ||
|
||
describe RuboCop::Cop::RSpec::RepeatedDescription do | ||
subject(:cop) { described_class.new } | ||
|
||
it 'registers an offense for repeated descriptions' do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps also add the scenario where the same description is used in different There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also test a scenario where two examples with the same description are separated by a scope change. I’m thinking e.g. it "does x" do
end
context "something else" do
it "works fine" do
end
end
it "does x" do
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah good catch. I actually thought I had more than one test here but I guess not. I'll add a few more There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bquorning the tests should be sufficient now |
||
expect_violation(<<-RUBY) | ||
describe 'doing x' do | ||
it "does x" do | ||
^^^^^^^^^^^ Don't repeat descriptions within an example group. | ||
end | ||
it "does x" do | ||
^^^^^^^^^^^ Don't repeat descriptions within an example group. | ||
end | ||
end | ||
RUBY | ||
end | ||
|
||
it 'registers offense for repeated descriptions separated by a context' do | ||
expect_violation(<<-RUBY) | ||
describe 'doing x' do | ||
it "does x" do | ||
^^^^^^^^^^^ Don't repeat descriptions within an example group. | ||
end | ||
context 'during some use case' do | ||
it "does x" do | ||
# this should be fine | ||
end | ||
end | ||
it "does x" do | ||
^^^^^^^^^^^ Don't repeat descriptions within an example group. | ||
end | ||
end | ||
RUBY | ||
end | ||
|
||
it 'ignores descriptions repeated in a shared context' do | ||
expect_no_violations(<<-RUBY) | ||
describe 'doing x' do | ||
it "does x" do | ||
end | ||
shared_context 'shared behavior' do | ||
it "does x" do | ||
end | ||
end | ||
end | ||
RUBY | ||
end | ||
|
||
it 'ignores repeated descriptions in a nested context' do | ||
expect_no_violations(<<-RUBY) | ||
describe 'doing x' do | ||
it "does x" do | ||
end | ||
context 'in a certain use case' do | ||
it "does x" do | ||
end | ||
end | ||
end | ||
RUBY | ||
end | ||
|
||
it 'does not flag tests which do not contain description strings' do | ||
expect_no_violations(<<-RUBY) | ||
describe 'doing x' do | ||
it { foo } | ||
it { bar } | ||
end | ||
RUBY | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not sure what this method call does. It looks like it matches the node and block against a pattern, but the return value is neither stored nor returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry about that. I added some doc comments and I also renamed the misleading name... It was a holdover from code I borrowed from #254. Didn't realize I forgot to change the name sorry about that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method does a recursive search until it hits a scope changing node. RuboCop's node pattern methods will yield (unless you defined a predicate method ending in
?
I believe) if you give it a block and the provided node matches. By writingyou get a recursive node search that stops when it hits a match for
node_should_stop_recursion?
. The end result is that I can writewhich I like. I follow this pattern in a handful of cops