diff --git a/lib/rubocop-rspec.rb b/lib/rubocop-rspec.rb index 0d7816575..bbd849525 100644 --- a/lib/rubocop-rspec.rb +++ b/lib/rubocop-rspec.rb @@ -9,21 +9,23 @@ require_relative 'rubocop/rspec/version' require_relative 'rubocop/rspec/inject' require_relative 'rubocop/rspec/node' -require_relative 'rubocop/rspec/top_level_describe' require_relative 'rubocop/rspec/wording' require_relative 'rubocop/rspec/language' require_relative 'rubocop/rspec/language/node_pattern' -require_relative 'rubocop/rspec/top_level_group' + +require_relative 'rubocop/cop/rspec/mixin/top_level_describe' +require_relative 'rubocop/cop/rspec/mixin/top_level_group' +require_relative 'rubocop/cop/rspec/mixin/variable' +require_relative 'rubocop/cop/rspec/mixin/final_end_location' +require_relative 'rubocop/cop/rspec/mixin/empty_line_separation' + require_relative 'rubocop/rspec/concept' require_relative 'rubocop/rspec/example_group' require_relative 'rubocop/rspec/example' require_relative 'rubocop/rspec/hook' -require_relative 'rubocop/rspec/variable' require_relative 'rubocop/cop/rspec/cop' require_relative 'rubocop/rspec/align_let_brace' require_relative 'rubocop/rspec/factory_bot' -require_relative 'rubocop/rspec/final_end_location' -require_relative 'rubocop/rspec/empty_line_separation' require_relative 'rubocop/rspec/corrector/move_node' RuboCop::RSpec::Inject.defaults! diff --git a/lib/rubocop/cop/rspec/describe_class.rb b/lib/rubocop/cop/rspec/describe_class.rb index 6bdf4ffb9..e80843181 100644 --- a/lib/rubocop/cop/rspec/describe_class.rb +++ b/lib/rubocop/cop/rspec/describe_class.rb @@ -22,7 +22,7 @@ module RSpec # describe "A feature example", type: :feature do # end class DescribeClass < Cop - include RuboCop::RSpec::TopLevelDescribe + include TopLevelDescribe MSG = 'The first argument to describe should be '\ 'the class or module being tested.' diff --git a/lib/rubocop/cop/rspec/describe_method.rb b/lib/rubocop/cop/rspec/describe_method.rb index 7a32739e9..e0f8d191f 100644 --- a/lib/rubocop/cop/rspec/describe_method.rb +++ b/lib/rubocop/cop/rspec/describe_method.rb @@ -17,7 +17,7 @@ module RSpec # describe MyClass, '.my_class_method' do # end class DescribeMethod < Cop - include RuboCop::RSpec::TopLevelDescribe + include TopLevelDescribe MSG = 'The second argument to describe should be the method '\ "being tested. '#instance' or '.class'." diff --git a/lib/rubocop/cop/rspec/empty_line_after_example.rb b/lib/rubocop/cop/rspec/empty_line_after_example.rb index 7bfbc8804..b0abf5b28 100644 --- a/lib/rubocop/cop/rspec/empty_line_after_example.rb +++ b/lib/rubocop/cop/rspec/empty_line_after_example.rb @@ -43,7 +43,7 @@ module RSpec # class EmptyLineAfterExample < Cop extend AutoCorrector - include RuboCop::RSpec::EmptyLineSeparation + include EmptyLineSeparation MSG = 'Add an empty line after `%s`.' diff --git a/lib/rubocop/cop/rspec/empty_line_after_example_group.rb b/lib/rubocop/cop/rspec/empty_line_after_example_group.rb index b6e9b9fc8..d83bb1fac 100644 --- a/lib/rubocop/cop/rspec/empty_line_after_example_group.rb +++ b/lib/rubocop/cop/rspec/empty_line_after_example_group.rb @@ -25,7 +25,7 @@ module RSpec # class EmptyLineAfterExampleGroup < Cop extend AutoCorrector - include RuboCop::RSpec::EmptyLineSeparation + include EmptyLineSeparation MSG = 'Add an empty line after `%s`.' diff --git a/lib/rubocop/cop/rspec/empty_line_after_final_let.rb b/lib/rubocop/cop/rspec/empty_line_after_final_let.rb index f75253e4e..fbde0ae2c 100644 --- a/lib/rubocop/cop/rspec/empty_line_after_final_let.rb +++ b/lib/rubocop/cop/rspec/empty_line_after_final_let.rb @@ -18,7 +18,7 @@ module RSpec # it { does_something } class EmptyLineAfterFinalLet < Cop extend AutoCorrector - include RuboCop::RSpec::EmptyLineSeparation + include EmptyLineSeparation MSG = 'Add an empty line after the last `%s`.' diff --git a/lib/rubocop/cop/rspec/empty_line_after_hook.rb b/lib/rubocop/cop/rspec/empty_line_after_hook.rb index abfbb9076..6dfa1e248 100644 --- a/lib/rubocop/cop/rspec/empty_line_after_hook.rb +++ b/lib/rubocop/cop/rspec/empty_line_after_hook.rb @@ -35,7 +35,7 @@ module RSpec # class EmptyLineAfterHook < Cop extend AutoCorrector - include RuboCop::RSpec::EmptyLineSeparation + include EmptyLineSeparation MSG = 'Add an empty line after `%s`.' diff --git a/lib/rubocop/cop/rspec/empty_line_after_subject.rb b/lib/rubocop/cop/rspec/empty_line_after_subject.rb index 90384e8c5..51be9b577 100644 --- a/lib/rubocop/cop/rspec/empty_line_after_subject.rb +++ b/lib/rubocop/cop/rspec/empty_line_after_subject.rb @@ -16,7 +16,7 @@ module RSpec # let(:foo) { bar } class EmptyLineAfterSubject < Cop extend AutoCorrector - include RuboCop::RSpec::EmptyLineSeparation + include EmptyLineSeparation MSG = 'Add an empty line after `%s`.' diff --git a/lib/rubocop/cop/rspec/file_path.rb b/lib/rubocop/cop/rspec/file_path.rb index 4407c871b..d34264012 100644 --- a/lib/rubocop/cop/rspec/file_path.rb +++ b/lib/rubocop/cop/rspec/file_path.rb @@ -57,7 +57,7 @@ module RSpec # my_class_spec.rb # describe MyClass, '#method' # class FilePath < Cop - include RuboCop::RSpec::TopLevelDescribe + include TopLevelDescribe MSG = 'Spec path should end with `%s`.' diff --git a/lib/rubocop/cop/rspec/instance_variable.rb b/lib/rubocop/cop/rspec/instance_variable.rb index 01eee1886..48eb135c8 100644 --- a/lib/rubocop/cop/rspec/instance_variable.rb +++ b/lib/rubocop/cop/rspec/instance_variable.rb @@ -47,7 +47,7 @@ module RSpec # end # class InstanceVariable < Cop - include RuboCop::RSpec::TopLevelGroup + include TopLevelGroup MSG = 'Avoid instance variables – use let, ' \ 'a method call, or a local variable (if possible).' diff --git a/lib/rubocop/cop/rspec/mixin/empty_line_separation.rb b/lib/rubocop/cop/rspec/mixin/empty_line_separation.rb new file mode 100644 index 000000000..5d48603f2 --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/empty_line_separation.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helps determine the offending location if there is not an empty line + # following the node. Allows comments to follow directly after. + module EmptyLineSeparation + include FinalEndLocation + include RangeHelp + + def missing_separating_line_offense(node) + return if last_child?(node) + + missing_separating_line(node) do |location| + msg = yield(node.method_name) + add_offense(location, message: msg) do |corrector| + corrector.insert_after(location.end, "\n") + end + end + end + + def missing_separating_line(node) + line = final_end_location(node).line + + line += 1 while comment_line?(processed_source[line]) + + return if processed_source[line].blank? + + yield offending_loc(line) + end + + def offending_loc(last_line) + offending_line = processed_source[last_line - 1] + + content_length = offending_line.lstrip.length + start = offending_line.length - content_length + + source_range(processed_source.buffer, + last_line, start, content_length) + end + + def last_child?(node) + return true unless node.parent&.begin_type? + + node.equal?(node.parent.children.last) + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/mixin/final_end_location.rb b/lib/rubocop/cop/rspec/mixin/final_end_location.rb new file mode 100644 index 000000000..526049f5d --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/final_end_location.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helps find the true end location of nodes which might contain heredocs. + module FinalEndLocation + def final_end_location(start_node) + heredoc_endings = + start_node.each_node(:str, :dstr, :xstr) + .select(&:heredoc?) + .map { |node| node.loc.heredoc_end } + + [start_node.loc.end, *heredoc_endings].max_by(&:line) + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/mixin/top_level_describe.rb b/lib/rubocop/cop/rspec/mixin/top_level_describe.rb new file mode 100644 index 000000000..6561c8964 --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/top_level_describe.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helper methods for top level describe cops + module TopLevelDescribe + extend NodePattern::Macros + + def on_send(node) + return unless respond_to?(:on_top_level_describe) + return unless top_level_describe?(node) + + on_top_level_describe(node, node.arguments) + end + + private + + def top_level_describe?(node) + return false unless node.method_name == :describe + + top_level_nodes.include?(node) + end + + def top_level_nodes + nodes = describe_statement_children(root_node) + # If we have no top level describe statements, we need to check any + # blocks on the top level (e.g. after a require). + if nodes.empty? + nodes = root_node.each_child_node(:block).flat_map do |child| + describe_statement_children(child) + end + end + + nodes + end + + def root_node + processed_source.ast + end + + def single_top_level_describe? + top_level_nodes.one? + end + + def describe_statement_children(node) + node.each_child_node(:send).select do |element| + element.method_name == :describe + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/mixin/top_level_group.rb b/lib/rubocop/cop/rspec/mixin/top_level_group.rb new file mode 100644 index 000000000..be19815fd --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/top_level_group.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helper methods for top level example group cops + module TopLevelGroup + extend RuboCop::NodePattern::Macros + include RuboCop::RSpec::Language + + def_node_matcher :example_or_shared_group?, + (ExampleGroups::ALL + SharedGroups::ALL).block_pattern + + def on_block(node) + return unless respond_to?(:on_top_level_group) + return unless top_level_group?(node) + + on_top_level_group(node) + end + + private + + def top_level_group?(node) + top_level_groups.include?(node) + end + + def top_level_groups + @top_level_groups ||= + top_level_nodes.select { |n| example_or_shared_group?(n) } + end + + def top_level_nodes + if root_node.begin_type? + root_node.children + else + [root_node] + end + end + + def root_node + processed_source.ast + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/mixin/variable.rb b/lib/rubocop/cop/rspec/mixin/variable.rb new file mode 100644 index 000000000..fb6eb43fe --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/variable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helps check offenses with variable definitions + module Variable + include RuboCop::RSpec::Language + extend RuboCop::NodePattern::Macros + + def_node_matcher :variable_definition?, <<~PATTERN + (send #{RSPEC} #{(Helpers::ALL + Subject::ALL).node_pattern_union} + $({sym str dsym dstr} ...) ...) + PATTERN + end + end + end +end diff --git a/lib/rubocop/cop/rspec/multiple_describes.rb b/lib/rubocop/cop/rspec/multiple_describes.rb index 79d28cd3f..3fcc1fd4c 100644 --- a/lib/rubocop/cop/rspec/multiple_describes.rb +++ b/lib/rubocop/cop/rspec/multiple_describes.rb @@ -23,7 +23,7 @@ module RSpec # end # end class MultipleDescribes < Cop - include RuboCop::RSpec::TopLevelDescribe + include TopLevelDescribe MSG = 'Do not use multiple top level describes - '\ 'try to nest them.' diff --git a/lib/rubocop/cop/rspec/nested_groups.rb b/lib/rubocop/cop/rspec/nested_groups.rb index e626df506..cc3c70cd2 100644 --- a/lib/rubocop/cop/rspec/nested_groups.rb +++ b/lib/rubocop/cop/rspec/nested_groups.rb @@ -87,7 +87,7 @@ module RSpec # class NestedGroups < Cop include ConfigurableMax - include RuboCop::RSpec::TopLevelDescribe + include TopLevelDescribe MSG = 'Maximum example group nesting exceeded [%d/%d].' diff --git a/lib/rubocop/cop/rspec/subject_stub.rb b/lib/rubocop/cop/rspec/subject_stub.rb index f51e419a0..7e60a7eee 100644 --- a/lib/rubocop/cop/rspec/subject_stub.rb +++ b/lib/rubocop/cop/rspec/subject_stub.rb @@ -22,7 +22,7 @@ module RSpec # end # class SubjectStub < Cop - include RuboCop::RSpec::TopLevelGroup + include TopLevelGroup MSG = 'Do not stub methods of the object under test.' diff --git a/lib/rubocop/cop/rspec/variable_definition.rb b/lib/rubocop/cop/rspec/variable_definition.rb index 5981d07e4..a7d614d01 100644 --- a/lib/rubocop/cop/rspec/variable_definition.rb +++ b/lib/rubocop/cop/rspec/variable_definition.rb @@ -24,7 +24,7 @@ module RSpec # subject('user') { create_user } class VariableDefinition < Cop include ConfigurableEnforcedStyle - include RuboCop::RSpec::Variable + include Variable MSG = 'Use %