Class: RuboCop::Cop::RSpec::UnusedLet::ScopeBuilder
- Inherits:
-
Object
- Object
- RuboCop::Cop::RSpec::UnusedLet::ScopeBuilder
- Includes:
- Matchers, References
- Defined in:
- lib/rubocop/cop/rspec/unused_let/scope_builder.rb,
sig/rubocop/cop/rspec/unused_let/scope_builder.rbs
Overview
Constant Summary collapse
- HELPER_SPEC_PATH =
rspec-rails infers
type: :helperfor spec files underspec/helpers. %r{(?:^|/)spec/helpers/}.freeze
- IMPLICIT_REFS_BY_TYPE =
letnames that well-known gems' shared contexts implicitly reference, hidden from single-file analysis, keyed by thetype:metadata that pulls the shared context in. When a group carries a matchingtype:, the listed names are recorded as references so thatlets of those names (here or in a descendant) count as used. { # rspec-validator_spec_helper # https://github.com/izumin5210/rspec-validator_spec_helper # `type: :validator` triggers a shared subject that dereferences # these names via `eval`, hidden from static analysis. validator: %i[ value attribute_names options validator_name validator_class validator_type validation_name model_class ].freeze }.freeze
- IMPLICIT_REFS_BY_TAG =
The same, keyed by a bare symbol an example group carries as metadata (an RSpec tag) instead of by its
type:. A tag and atype:of the same name are different metadata, so the two maps stay apart. { # RuboCop's own cop spec support # https://github.com/rubocop/rubocop/blob/master/lib/rubocop/rspec/support.rb # Requiring it maps the `:config` tag to the `config` shared # context, whose `let`s a cop spec overrides, and unconditionally # includes `CopHelper`, whose `let`s it overrides too. Only the tag # is visible from the spec file, so it stands in for both. config: %i[ cop_class cop_config other_cops cop_options gem_versions processed_source source_buffer all_cops_config cur_cop_config config cop source ruby_version parser_engine rails_version ].freeze }.freeze
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
- #registry ⇒ SharedExampleRegistry readonly
- #spec_filename ⇒ String? readonly
Instance Method Summary collapse
-
#build_from(node) ⇒ Scope
Build the Scope for
nodefrom its own region alone; nested groups are left for their own traversal. -
#candidate_helpers_in(node) ⇒ Array[RuboCop::AST::Node]
The
defs innode's region that may turn out to be helpers of this group: every one noclass/modulebody claims. - #collect_definitions(node, scope) ⇒ void
-
#collect_example_references(node, scope, helpers) ⇒ void
References in
node's region that sit outside its helper bodies (examples and any other group-level code), collected intorefs_in_example. -
#collect_method_definitions(node, scope) ⇒ void
def fooat an example group's level becomes an instance method on the group's example class, so it is a definition just like alet. -
#collect_subject_definitions(group, scope) ⇒ void
A
subjectis aletthat additionally answers to the implicit namesubject(see References::SUBJECT_ALIASES), recorded as its alias. -
#defined_in?(defn, group) {|arg0| ... } ⇒ Boolean
Whether
defnis written directly ingroup's region: walking outward from it reachesgroupbefore a nested spec group (which collects thedefs written in it on its own traversal) or a nodeowned_by_othercalls true for, a boundary of its own. -
#helper_nodes(node) ⇒ Array[RuboCop::AST::Node]
The nodes whose bodies are read as running in the example's scope: the group's own
lets,subjects and hooks, plus everydefwritten in its region (see #candidate_helpers_in). - #implicit_reference_names(scope) ⇒ Array[Symbol]
-
#initialize(spec_filename, registry) ⇒ ScopeBuilder
constructor
A new instance of ScopeBuilder.
-
#inject_implicit_references(scope) ⇒ void
A well-known gem's shared context (pulled in by
type:metadata or by a tag) can referenceletnames that single-file analysis never sees. -
#mark_inline_bound_references(name, node, scope) ⇒ void
An inline inclusion injects the block's definitions here, so its bound references (names it both defines and references) reach a same-named
letin this scope — mark those referenced. -
#method_definitions_in(node) ⇒ Array[RuboCop::AST::Node]
def foowritten at an example group's level becomes an instance method on the group's example class. - #record_helper_references(node, scope) ⇒ void
-
#record_inclusion(node, scope) ⇒ void
A shared inclusion whose block is defined in this file consumes only its free references, recorded like any other example reference.
-
#tags_from_group(node) ⇒ Array[Symbol]
The bare symbols an example group carries as metadata.
-
#type_from_filename(filename) ⇒ Symbol?
rspec-rails infers a spec's
type:from its location when none is set explicitly. - #type_from_group(node) ⇒ Symbol?
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(spec_filename, registry) ⇒ ScopeBuilder
Returns a new instance of ScopeBuilder.
58 59 60 61 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 58 def initialize(spec_filename, registry) #: void @spec_filename = spec_filename @registry = registry end |
Instance Attribute Details
#registry ⇒ SharedExampleRegistry (readonly)
83 84 85 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 83 def registry @registry end |
#spec_filename ⇒ String? (readonly)
82 83 84 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 82 def spec_filename @spec_filename end |
Instance Method Details
#build_from(node) ⇒ Scope
Build the Scope for node from its own region alone; nested groups
are left for their own traversal.
67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 67 def build_from(node) #: Scope kind = example_group?(node) ? :example : :shared #: Scope::kind type = type_from_group(node) || type_from_filename(spec_filename) scope = Scope.new(node: node, kind: kind, type: type, tags: (node), carries_examples: carries_examples?(node)) helpers = helper_nodes(node) collect_definitions(node, scope) helpers.each { record_helper_references(_1, scope) } collect_example_references(node, scope, helpers) inject_implicit_references(scope) scope end |
#candidate_helpers_in(node) ⇒ Array[RuboCop::AST::Node]
The defs in node's region that may turn out to be helpers of this
group: every one no class/module body claims. Wider than
#method_definitions_in: a def inside Class.new/class_eval/an
unrecognized block still has its references read here, on the chance
it runs in the example's scope after all.
232 233 234 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 232 def candidate_helpers_in(node) #: Array[RuboCop::AST::Node] node.each_descendant(:def).select { defined_in?(_1, node, &method(:keyword_definee?)) } end |
#collect_definitions(node, scope) ⇒ void
This method returns an undefined value.
110 111 112 113 114 115 116 117 118 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 110 def collect_definitions(node, scope) #: void group = RuboCop::RSpec::ExampleGroup.new(node) group.lets.each do |let| helper, name = let_definition(let) scope.add_definition(helper, name.to_sym, let) if helper && name end collect_subject_definitions(group, scope) collect_method_definitions(node, scope) end |
#collect_example_references(node, scope, helpers) ⇒ void
This method returns an undefined value.
References in node's region that sit outside its helper bodies
(examples and any other group-level code), collected into
refs_in_example. Stops at nested spec groups and skips the helper
definitions, whose references belong to refs. Resolves a shared
inclusion found along the way.
A subject in a form helper_nodes misses (subject { _1 },
subject(:x, &blk)) reaches this walk, so its declaring call is
left out of the references here too.
161 162 163 164 165 166 167 168 169 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 161 def collect_example_references(node, scope, helpers) #: void node.each_child_node do |child| next if spec_group?(child) || helpers.any? { _1.equal?(child) } references_in(child).each { scope.add_reference_in_example(_1) } unless subject_definition_head?(child) record_inclusion(child, scope) if inclusion_call?(child) collect_example_references(child, scope, helpers) end end |
#collect_method_definitions(node, scope) ⇒ void
This method returns an undefined value.
def foo at an example group's level becomes an instance method on
the group's example class, so it is a definition just like a let.
141 142 143 144 145 146 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 141 def collect_method_definitions(node, scope) #: void method_definitions_in(node).each do |defn| inner = defn #: untyped scope.add_definition(:def, inner.method_name, defn) end end |
#collect_subject_definitions(group, scope) ⇒ void
This method returns an undefined value.
A subject is a let that additionally answers to the implicit
name subject (see References::SUBJECT_ALIASES), recorded as its
alias. An anonymous subject { } has no other name, so that alias
is the only way to reach it.
127 128 129 130 131 132 133 134 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 127 def collect_subject_definitions(group, scope) #: void group.subjects.each do |subject_node| helper, name = subject_definition(subject_node) next unless helper scope.add_definition(helper, name, subject_node, alias_name: :subject) end end |
#defined_in?(defn, group) {|arg0| ... } ⇒ Boolean
Whether defn is written directly in group's region: walking
outward from it reaches group before a nested spec group (which
collects the defs written in it on its own traversal) or a node
owned_by_other calls true for, a boundary of its own.
256 257 258 259 260 261 262 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 256 def defined_in?(defn, group, &owned_by_other) #: bool defn.each_ancestor do |ancestor| return true if ancestor.equal?(group) return false if spec_group?(ancestor) || owned_by_other.call(ancestor) end false end |
#helper_nodes(node) ⇒ Array[RuboCop::AST::Node]
The nodes whose bodies are read as running in the example's scope:
the group's own lets, subjects and hooks, plus every def written
in its region (see #candidate_helpers_in).
210 211 212 213 214 215 216 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 210 def helper_nodes(node) #: Array[RuboCop::AST::Node] group = RuboCop::RSpec::ExampleGroup.new(node) group.lets + group.subjects + group.hooks.map(&:to_node) + candidate_helpers_in(node) end |
#implicit_reference_names(scope) ⇒ Array[Symbol]
102 103 104 105 106 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 102 def implicit_reference_names(scope) #: Array[Symbol] by_type = IMPLICIT_REFS_BY_TYPE[scope.type] || [] by_tag = scope..flat_map { IMPLICIT_REFS_BY_TAG[_1] || [] } by_type + by_tag end |
#inject_implicit_references(scope) ⇒ void
This method returns an undefined value.
A well-known gem's shared context (pulled in by type: metadata
or by a tag) can reference let names that single-file analysis
never sees.
Record them exactly as a real helper on this group would be: an
example reference (justifying a let here or in an ancestor) and a
helper reference (reaching lets in descendant groups, since helper
bodies run in the example's scope).
94 95 96 97 98 99 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 94 def inject_implicit_references(scope) #: void implicit_reference_names(scope).each do |name| scope.add_reference_in_example(name) scope.add_reference(name) end end |
#mark_inline_bound_references(name, node, scope) ⇒ void
This method returns an undefined value.
An inline inclusion injects the block's definitions here, so its bound
references (names it both defines and references) reach a same-named
let in this scope — mark those referenced. Scoped to this group's
own definitions, so it never suppresses an unrelated let in an
ancestor or descendant group.
201 202 203 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 201 def mark_inline_bound_references(name, node, scope) #: void registry.bound_references(name, node).each { scope.mark_referenced(_1) } end |
#method_definitions_in(node) ⇒ Array[RuboCop::AST::Node]
def foo written at an example group's level becomes an instance
method on the group's example class. Skip defs that belong
elsewhere: nested inside a deeper example/shared group (not visible
from node), or inside a definee scope written in the group (see
Matchers#definee_scope?), which defines methods on that inner
scope, not on the example class.
244 245 246 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 244 def method_definitions_in(node) #: Array[RuboCop::AST::Node] node.each_descendant(:def).select { defined_in?(_1, node, &method(:definee_scope?)) } end |
#record_helper_references(node, scope) ⇒ void
This method returns an undefined value.
220 221 222 223 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 220 def record_helper_references(node, scope) #: void references_in(node).each { scope.add_reference(_1) } unless subject_definition_head?(node) node.each_child_node { record_helper_references(_1, scope) } end |
#record_inclusion(node, scope) ⇒ void
This method returns an undefined value.
A shared inclusion whose block is defined in this file consumes only
its free references, recorded like any other example reference. An
inclusion we cannot resolve (an unknown or dynamically named block)
falls back to silencing every let visible at this point. An inline
inclusion additionally reaches a same-named let here through its
bound references, via #mark_inline_bound_references.
180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 180 def record_inclusion(node, scope) #: void name = inclusion_name(node) free_refs = name && registry.resolve(name, node) unless name && free_refs scope.mark_inclusion return end free_refs.each { scope.add_reference_in_example(_1) } mark_inline_bound_references(name, node, scope) if inline_inclusion?(node) end |
#tags_from_group(node) ⇒ Array[Symbol]
The bare symbols an example group carries as metadata. The first
argument is skipped: a symbol there is the group's description
(describe :config do), not a tag.
RSpec itself reads only the symbols trailing the argument list (a
trailing hash aside), so describe X, :config, "extra" leaves
:config in the description. Reading every symbol past the first
over-collects there, which errs toward leaving a let alone rather
than flagging one wrongly.
290 291 292 293 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 290 def (node) #: Array[Symbol] block = node #: untyped block.send_node.arguments.drop(1).select(&:sym_type?).map(&:value) end |
#type_from_filename(filename) ⇒ Symbol?
rspec-rails infers a spec's type: from its location when none is
set explicitly. Only :helper is inferred here, the one type the
cop acts on.
300 301 302 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 300 def type_from_filename(filename) #: Symbol? :helper if filename && HELPER_SPEC_PATH.match?(filename) end |
#type_from_group(node) ⇒ Symbol?
265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/rubocop/cop/rspec/unused_let/scope_builder.rb', line 265 def type_from_group(node) #: Symbol? block = node #: untyped block.send_node.arguments.each do |arg| next unless arg.hash_type? arg.pairs.each do |pair| key = pair.key value = pair.value return value.value if key.sym_type? && key.value == :type && value.sym_type? end end nil end |