lib/rubocop/cop/rspec/described_class.rb in rubocop-rspec-1.33.0 vs lib/rubocop/cop/rspec/described_class.rb in rubocop-rspec-1.34.0

- old
+ new

@@ -31,11 +31,10 @@ # describe MyClass do # subject { MyClass.do_something } # end # class DescribedClass < Cop - include RuboCop::RSpec::TopLevelDescribe include ConfigurableEnforcedStyle DESCRIBED_CLASS = 'described_class' MSG = 'Use `%<replacement>s` instead of `%<src>s`.' @@ -46,24 +45,23 @@ def_node_matcher :rspec_block?, RuboCop::RSpec::Language::ALL.block_pattern def_node_matcher :scope_changing_syntax?, '{def class module}' + def_node_matcher :described_constant, <<-PATTERN + (block (send _ :describe $(const ...) ...) (args) $_) + PATTERN + def on_block(node) - # In case the explicit style is used, we needs to remember what's - # being described. Thus, we use an ivar for @described_class. - describe, @described_class, body = described_constant(node) + # In case the explicit style is used, we need to remember what's + # being described. + @described_class, body = described_constant(node) - return if body.nil? - return unless top_level_describe?(describe) + return unless body find_usage(body) do |match| - add_offense( - match, - location: :expression, - message: message(match.const_name) - ) + add_offense(match, message: message(match.const_name)) end end def autocorrect(node) replacement = if style == :described_class @@ -106,18 +104,86 @@ def skippable_block?(node) node.block_type? && !rspec_block?(node) && skip_blocks? end def skip_blocks? - cop_config['SkipBlocks'].equal?(true) + cop_config['SkipBlocks'] end def offensive?(node) if style == :described_class - node.eql?(@described_class) + offensive_described_class?(node) else node.send_type? && node.method_name == :described_class end + end + + def offensive_described_class?(node) + return unless node.const_type? + + nearest_described_class, = node.each_ancestor(:block) + .map { |ancestor| described_constant(ancestor) }.find(&:itself) + + return if nearest_described_class.equal?(node) + + full_const_name(nearest_described_class) == full_const_name(node) + end + + def full_const_name(node) + collapse_namespace(namespace(node), const_name(node)) + end + + # @param namespace [Array<Symbol>] + # @param const [Array<Symbol>] + # @return [Array<Symbol>] + # @example + # # nil represents base constant + # collapse_namespace([], :C) # => [:C] + # collapse_namespace([:A, :B], [:C) # => [:A, :B, :C] + # collapse_namespace([:A, :B], [:B, :C) # => [:A, :B, :C] + # collapse_namespace([:A, :B], [nil, :C) # => [nil, :C] + # collapse_namespace([:A, :B], [nil, :B, :C) # => [nil, :B, :C] + def collapse_namespace(namespace, const) + return const if namespace.empty? + return const if const.first.nil? + + start = [0, (namespace.length - const.length)].max + max = namespace.length + intersection = (start..max).find do |shift| + namespace[shift, max - shift] == const[0, max - shift] + end + [*namespace[0, intersection], *const] + end + + # @param node [RuboCop::AST::Node] + # @return [Array<Symbol>] + # @example + # const_name(s(:const, nil, :C)) # => [:C] + # const_name(s(:const, s(:const, nil, :M), :C)) # => [:M, :C] + # const_name(s(:const, s(:cbase), :C)) # => [nil, :C] + def const_name(node) + # rubocop:disable InternalAffairs/NodeDestructuring + namespace, name = *node + # rubocop:enable InternalAffairs/NodeDestructuring + if !namespace + [name] + elsif namespace.cbase_type? + [nil, name] + else + [*const_name(namespace), name] + end + end + + # @param node [RuboCop::AST::Node] + # @return [Array<Symbol>] + # @example + # namespace(node) # => [:A, :B, :C] + def namespace(node) + node + .each_ancestor(:class, :module) + .reverse_each + .flat_map { |ancestor| ancestor.defined_module_name.split('::') } + .map(&:to_sym) end end end end end