Class: RuboCop::Cop::RSpec::UnusedLet::SharedExampleRegistry
- Inherits:
-
Object
- Object
- RuboCop::Cop::RSpec::UnusedLet::SharedExampleRegistry
- 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
- #external_definitions ⇒ Array[definition_mapping] readonly
- #local_definitions ⇒ definition_mapping readonly
Instance Method Summary collapse
-
#bound_references(name, inclusion_node) ⇒ Set[Symbol]
The names the shared block of
nameboth defines and references (defs & refs), limited to the block's own definitions. - #build_definition(node) ⇒ Definition
-
#classify(child, block_node, definition) ⇒ void
Sort one subtree node into the shared block's definitions, references or nested inclusions.
-
#classify_send(child, definition) ⇒ void
A bare send inside a shared block is either an inclusion of another shared block (recorded for nested resolution) or a plain reference.
-
#defined_names(child, block_node) ⇒ Array[Symbol]
The names
childdefines on the shared block, empty when it defines none. -
#enclosing_group(node) ⇒ RuboCop::AST::Node?
The nearest example/shared group enclosing
node, ornilwhen it sits at the file's top level. -
#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).
-
#implicit_subject_name(child, block_node) ⇒ Symbol?
:subjectwhen the subject atchildis the block's own,nilwhen a nestedcontextholds it. -
#initialize(ast, external_definitions = []) ⇒ SharedExampleRegistry
constructor
A new instance of SharedExampleRegistry.
-
#lookup(name, inclusion_node) ⇒ Definition?
The definition of
namethat RSpec would apply atinclusion_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. -
#lookup_external(name) ⇒ Definition?
The last-registered top-level definition of
nameamong the external definitions, ornil. -
#nested_references(definition, stack) ⇒ Set[Symbol]?
The free references reaching
definitionthrough the shared blocks it includes, reduced by the names it defines itself. -
#own_level_def?(defn, block_node) ⇒ Boolean
Whether
defnsits directly inblock_node, with no inner definee scope claiming it along the way. -
#resolve(name, inclusion_node) ⇒ Set[Symbol]?
The free references an inclusion of
nameatinclusion_nodeconsumes, ornilwhen no definition is visible or it cannot be resolved. - #resolve_from(name, inclusion_node, stack) ⇒ Set[Symbol]?
-
#scan(ast) ⇒ void
Index every shared block in the file under its name.
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.
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_definitions ⇒ Array[definition_mapping] (readonly)
69 70 71 |
# File 'lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb', line 69 def external_definitions @external_definitions end |
#local_definitions ⇒ definition_mapping (readonly)
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.
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
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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]?
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.
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 |