Class: RuboCop::Cop::RSpec::UnusedLet::SharedExampleRegistry

Inherits:
Object
  • Object
show all
Includes:
Matchers, References
Defined in:
lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb,
sig/rubocop/cop/rspec/unused_let/shared_example_registry.rbs

Overview

An index of the shared_examples/shared_context blocks available when resolving a spec's inclusions: those defined in the file it is built from, plus definitions supplied for shared examples defined elsewhere in the project. Given an inclusion (it_behaves_like, include_context, ...), #resolve maps it to the named block's free references — the let-visible names its subtree uses but does not itself define — following RSpec's scoping (see #lookup), with the supplied external definitions as a global fallback.

Defined Under Namespace

Classes: Definition

Constant Summary

Constants included from References

References::DYNAMIC_DISPATCH_METHODS, References::SUBJECT_ALIASES

Constants included from Matchers

Matchers::BANG_HELPERS, Matchers::BLOCK_TYPES, Matchers::DEFINEE_SCOPE_TYPES, Matchers::ENCLOSING_BODY_TYPES, Matchers::SUBJECT_HELPERS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from References

references_in, #self?.references_in

Methods included from Matchers

#anonymous_subject_definition, #carries_examples?, #definee_scope?, #example_group?, #example_of?, #example_send?, #inclusion_call?, #inclusion_name, #inline_inclusion?, #keyword_definee?, #let_definition, #named_subject_definition, #nested_inclusion?, #own_block_of?, #rspec_scope_block?, #shared_group_name, #spec_group?, #subject_definition, #subject_definition_head?

Constructor Details

#initialize(ast, external_definitions = []) ⇒ SharedExampleRegistry

Returns a new instance of SharedExampleRegistry.

RBS:

  • ast: RuboCop::AST::Node?

  • external_definitions: Array[definition_mapping]

Parameters:

  • ast (RuboCop::AST::Node, nil)
  • external_definitions (Array[definition_mapping]) (defaults to: [])


38
39
40
41
42
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 38

def initialize(ast, external_definitions = []) #: void
  @local_definitions = {} #: definition_mapping
  scan(ast) if ast
  @external_definitions = external_definitions
end

Instance Attribute Details

#external_definitionsArray[definition_mapping] (readonly)

Signature:

  • Array[definition_mapping] -- name-to-Definition maps for external files

Returns:

  • (Array[definition_mapping])


69
70
71
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 69

def external_definitions
  @external_definitions
end

#local_definitionsdefinition_mapping (readonly)

Signature:

  • definition_mapping -- name-to-Definition map for this file

Returns:

  • (definition_mapping)


34
35
36
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 34

def local_definitions
  @local_definitions
end

Instance Method Details

#bound_references(name, inclusion_node) ⇒ Set[Symbol]

The names the shared block of name both defines and references (defs & refs), limited to the block's own definitions. Empty when no definition is visible at inclusion_node.

RBS:

  • name: Symbol | String

  • inclusion_node: RuboCop::AST::Node

Parameters:

  • name (Symbol, String)
  • inclusion_node (RuboCop::AST::Node)

Returns:

  • (Set[Symbol])


60
61
62
63
64
65
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 60

def bound_references(name, inclusion_node) #: Set[Symbol]
  definition = lookup(name.to_s, inclusion_node)
  return Set.new unless definition

  definition.defs & definition.refs
end

#build_definition(node) ⇒ Definition

RBS:

  • node: RuboCop::AST::Node

Parameters:

  • node (RuboCop::AST::Node)

Returns:



84
85
86
87
88
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 84

def build_definition(node) #: Definition
  definition = Definition.new(enclosing_group(node), node, Set.new, Set.new, [])
  node.each_descendant(:send, :block, :def) { classify(_1, node, definition) }
  definition
end

#classify(child, block_node, definition) ⇒ void

This method returns an undefined value.

Sort one subtree node into the shared block's definitions, references or nested inclusions. A node that defines nothing contributes a reference or an inclusion only when it is a bare send.

RBS:

  • child: RuboCop::AST::Node

  • block_node: RuboCop::AST::Node

  • definition: Definition

Parameters:

  • child (RuboCop::AST::Node)
  • block_node (RuboCop::AST::Node)
  • definition (Definition)


98
99
100
101
102
103
104
105
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 98

def classify(child, block_node, definition) #: void
  names = defined_names(child, block_node)
  if names.any?
    definition.defs.merge(names)
  elsif child.send_type? && !subject_definition_head?(child)
    classify_send(child, definition)
  end
end

#classify_send(child, definition) ⇒ void

This method returns an undefined value.

A bare send inside a shared block is either an inclusion of another shared block (recorded for nested resolution) or a plain reference.

RBS:

  • child: RuboCop::AST::Node

  • definition: Definition

Parameters:

  • child (RuboCop::AST::Node)
  • definition (Definition)


160
161
162
163
164
165
166
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 160

def classify_send(child, definition) #: void
  if inclusion_call?(child)
    definition.inclusions << [inclusion_name(child)&.to_s, child]
  else
    references_in(child).each { definition.refs << _1 }
  end
end

#defined_names(child, block_node) ⇒ Array[Symbol]

The names child defines on the shared block, empty when it defines none. Two of them are conditional, on different grounds: a def helper's name counts only outside an inner definee scope (see #own_level_def?), the implicit subject only outside a nested group (see #implicit_subject_name).

RBS:

  • child: RuboCop::AST::Node

  • block_node: RuboCop::AST::Node

Parameters:

  • child (RuboCop::AST::Node)
  • block_node (RuboCop::AST::Node)

Returns:

  • (Array[Symbol])


115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 115

def defined_names(child, block_node) #: Array[Symbol]
  if (let = let_definition(child))
    [let[1].to_sym]
  elsif (subject = subject_definition(child))
    [subject[1], implicit_subject_name(child, block_node)].compact
  elsif child.def_type? && own_level_def?(child, block_node)
    def_node = child #: untyped
    [def_node.method_name]
  else
    []
  end
end

#enclosing_group(node) ⇒ RuboCop::AST::Node?

The nearest example/shared group enclosing node, or nil when it sits at the file's top level.

RBS:

  • node: RuboCop::AST::Node

Parameters:

  • node (RuboCop::AST::Node)

Returns:

  • (RuboCop::AST::Node, nil)


172
173
174
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 172

def enclosing_group(node) #: RuboCop::AST::Node?
  node.each_ancestor(:block).find { spec_group?(_1) }
end

#free_references(definition, stack) ⇒ Set[Symbol]?

The free references the definition consumes: the names it references but does not define, plus the free references of the blocks it includes (reduced by its own definitions). nil if it is part of a cycle, or includes something dynamic or itself unresolvable.

RBS:

  • definition: Definition

  • stack: Array[RuboCop::AST::Node]

Parameters:

  • definition (Definition)
  • stack (Array[RuboCop::AST::Node])

Returns:

  • (Set[Symbol], nil)


232
233
234
235
236
237
238
239
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 232

def free_references(definition, stack) #: Set[Symbol]?
  return nil if stack.include?(definition.node) # cycle -> conservative

  nested = nested_references(definition, stack)
  return nil if nested.nil?

  (definition.refs - definition.defs) | nested
end

#implicit_subject_name(child, block_node) ⇒ Symbol?

:subject when the subject at child is the block's own, nil when a nested context holds it. Every group has a subject, so a nested one shadows the block's own only there; taken flat it would cancel the subject reference an example at the block's level makes, and the including group's subject would look unreferenced.

RBS:

  • child: RuboCop::AST::Node

  • block_node: RuboCop::AST::Node

Parameters:

  • child (RuboCop::AST::Node)
  • block_node (RuboCop::AST::Node)

Returns:

  • (Symbol, nil)


136
137
138
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 136

def implicit_subject_name(child, block_node) #: Symbol?
  :subject if block_node.equal?(enclosing_group(child))
end

#lookup(name, inclusion_node) ⇒ Definition?

The definition of name that RSpec would apply at inclusion_node: the innermost enclosing group that defines the name wins (shadowing outer ones), falling back to the file's top-level definitions, and finally to the external files' top-level definitions. Within a scope the last definition wins, mirroring RSpec, which warns on a redefinition and keeps the latest. nil when no definition is visible here.

RBS:

  • name: String

  • inclusion_node: RuboCop::AST::Node

Parameters:

  • name (String)
  • inclusion_node (RuboCop::AST::Node)

Returns:



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 196

def lookup(name, inclusion_node) #: Definition?
  candidates = local_definitions[name]
  if candidates
    inclusion_node.each_ancestor(:block) do |group|
      next unless spec_group?(group)

      found = candidates.select { _1.owner&.equal?(group) }.last
      return found if found
    end
    top = candidates.select { _1.owner.nil? }.last
    return top if top
  end

  lookup_external(name)
end

#lookup_external(name) ⇒ Definition?

The last-registered top-level definition of name among the external definitions, or nil. Only top-level (globally visible) definitions are eligible: a shared block nested inside a group is not globally visible, exactly as in RSpec.

RBS:

  • name: String

Parameters:

  • name (String)

Returns:



218
219
220
221
222
223
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 218

def lookup_external(name) #: Definition?
  external_definitions
    .flat_map { _1[name] || [] }
    .select { _1.owner.nil? }
    .last
end

#nested_references(definition, stack) ⇒ Set[Symbol]?

The free references reaching definition through the shared blocks it includes, reduced by the names it defines itself. nil if any nested inclusion is dynamic or unresolvable.

RBS:

  • definition: Definition

  • stack: Array[RuboCop::AST::Node]

Parameters:

  • definition (Definition)
  • stack (Array[RuboCop::AST::Node])

Returns:

  • (Set[Symbol], nil)


247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 247

def nested_references(definition, stack) #: Set[Symbol]?
  result = Set.new #: Set[Symbol]

  definition.inclusions.each do |name, node|
    return nil unless name # dynamic inclusion -> conservative

    nested = resolve_from(name, node, stack + [definition.node])
    return nil unless nested # unknown/cyclic nested -> conservative

    result |= (nested - definition.defs)
  end

  result
end

#own_level_def?(defn, block_node) ⇒ Boolean

Whether defn sits directly in block_node, with no inner definee scope claiming it along the way. Nested example groups are not a boundary here: the block's whole subtree is gathered flat, as its lets and references are.

RBS:

  • defn: RuboCop::AST::Node

  • block_node: RuboCop::AST::Node

Parameters:

  • defn (RuboCop::AST::Node)
  • block_node (RuboCop::AST::Node)

Returns:

  • (Boolean)


147
148
149
150
151
152
153
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 147

def own_level_def?(defn, block_node) #: bool
  defn.each_ancestor do |ancestor|
    return true if ancestor.equal?(block_node)
    return false if definee_scope?(ancestor)
  end
  false
end

#resolve(name, inclusion_node) ⇒ Set[Symbol]?

The free references an inclusion of name at inclusion_node consumes, or nil when no definition is visible or it cannot be resolved.

RBS:

  • name: Symbol | String

  • inclusion_node: RuboCop::AST::Node

Parameters:

  • name (Symbol, String)
  • inclusion_node (RuboCop::AST::Node)

Returns:

  • (Set[Symbol], nil)


50
51
52
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 50

def resolve(name, inclusion_node) #: Set[Symbol]?
  resolve_from(name.to_s, inclusion_node, [])
end

#resolve_from(name, inclusion_node, stack) ⇒ Set[Symbol]?

RBS:

  • name: String

  • inclusion_node: RuboCop::AST::Node

  • stack: Array[RuboCop::AST::Node]

Parameters:

  • name (String)
  • inclusion_node (RuboCop::AST::Node)
  • stack (Array[RuboCop::AST::Node])

Returns:

  • (Set[Symbol], nil)


179
180
181
182
183
184
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 179

def resolve_from(name, inclusion_node, stack) #: Set[Symbol]?
  definition = lookup(name, inclusion_node)
  return nil unless definition # unknown here -> conservative

  free_references(definition, stack)
end

#scan(ast) ⇒ void

This method returns an undefined value.

Index every shared block in the file under its name. Names can carry more than one definition (in different scopes, or a same-scope redefinition), disambiguated at lookup time by scope.

RBS:

  • ast: RuboCop::AST::Node

Parameters:

  • ast (RuboCop::AST::Node)


76
77
78
79
80
81
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 76

def scan(ast) #: void
  [ast, *ast.each_descendant(:block)].each do |node|
    name = shared_group_name(node)
    (local_definitions[name.to_s] ||= []) << build_definition(node) if name
  end
end