Class: RuboCop::Cop::RSpec::UnusedLet
- Extended by:
- AutoCorrector
- Includes:
- RangeHelp, Matchers
- Defined in:
- lib/rubocop/cop/rspec/unused_let.rb,
lib/rubocop/cop/rspec/unused_let/scope.rb,
lib/rubocop/cop/rspec/unused_let/matchers.rb,
lib/rubocop/cop/rspec/unused_let/references.rb,
lib/rubocop/cop/rspec/unused_let/scope_stack.rb,
lib/rubocop/cop/rspec/unused_let/scope_builder.rb,
lib/rubocop/cop/rspec/unused_let/external_definitions.rb,
lib/rubocop/cop/rspec/unused_let/shared_example_registry.rb,
sig/rubocop/cop/rspec/unused_let.rbs,
sig/rubocop/cop/rspec/unused_let/scope.rbs,
sig/rubocop/cop/rspec/unused_let/matchers.rbs,
sig/rubocop/cop/rspec/unused_let/references.rbs,
sig/rubocop/cop/rspec/unused_let/scope_stack.rbs,
sig/rubocop/cop/rspec/unused_let/scope_builder.rbs,
sig/rubocop/cop/rspec/unused_let/external_definitions.rbs,
sig/rubocop/cop/rspec/unused_let/shared_example_registry.rbs
Overview
Checks for let/subject definitions and helper methods that are never
referenced.
A let (or let!) whose name is never used within its scope is dead
code that makes specs harder to read. A subject (or subject!) and a
helper method (def) written at an example group's level are checked
the same way, the latter because it becomes an instance method on the
group's example class.
A let that a shared example or context consumes counts as used when
the inclusion is in reach: the shared block has to be visible in the
same file, or be a top-level block in a file listed in
SharedExamplePaths (default spec/support/**/*.rb). Otherwise every
let visible at that inclusion is left alone. See
the README
for the full rules and their known limitations.
Defined Under Namespace
Modules: Matchers, References Classes: ExternalDefinitions, Scope, ScopeBuilder, ScopeStack, SharedExampleRegistry
Constant Summary collapse
- MSG =
"`%<helper>s(:%<name>s)` is not referenced anywhere. " \ "Remove it or reference it in an example."
- DEF_MSG =
"`def %<name>s` is not referenced anywhere. " \ "Remove it or reference it in an example."
- ANONYMOUS_SUBJECT_MSG =
"`%<helper>s` is not referenced anywhere. " \ "Remove it or reference it in an example."
- MSG_IN_SHARED_GROUP =
Inside a shared block the claim has to be narrower: a group including the block from a file the cop never reads could still reference the name, so the finding is scoped to what this block can be seen to do.
"`%<helper>s(:%<name>s)` is not referenced anywhere in this shared example group. " \ "Remove it or reference it in an example."
- DEF_MSG_IN_SHARED_GROUP =
"`def %<name>s` is not referenced anywhere in this shared example group. " \ "Remove it or reference it in an example."
- ANONYMOUS_SUBJECT_MSG_IN_SHARED_GROUP =
"`%<helper>s` is not referenced anywhere in this shared example group. " \ "Remove it or reference it in an example."
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
- #builder ⇒ ScopeBuilder readonly
- #scope_stack ⇒ ScopeStack readonly
Instance Method Summary collapse
- #add_offense_for(definition, in_shared_group:) ⇒ void
-
#after_block(node) ⇒ void
A group's
lets know whether they were referenced once its whole subtree has been entered, which is complete by the time it is left. -
#external_definitions ⇒ ExternalDefinitions
The
SharedExamplePathspatterns resolved to file contents, memoized since RuboCop asks for the checksum and builds the registry from the same instance during one investigation. -
#external_dependency_checksum ⇒ String?
The pre-loaded files' digest for RuboCop's result-cache key, which otherwise covers no project file but the inspected one.
- #message_for(definition, in_shared_group:) ⇒ String
-
#on_block(node) ⇒ void
RuboCop visits nested groups on their own
on_block, so we never descend manually: the scope stack resolves this group's references against its ancestry now, and descendant groups resolve against this one later, as they are entered. - #on_new_investigation ⇒ void
- #report(scope, in_shared_group:) ⇒ void
-
#reportable_in?(shared_groups) ⇒ Boolean
Whether a group enclosed by
shared_groups(the shared blocks among it and its ancestors, in no particular order) can be judged from this file: only when every one of them carries examples.
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?
Methods inherited from Base
#add_offense, #config, #cop_config, def_node_matcher, def_node_search, #processed_source, #range_by_whole_lines, #target_ruby_version
Instance Attribute Details
#builder ⇒ ScopeBuilder (readonly)
189 190 191 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 189 def builder @builder end |
#scope_stack ⇒ ScopeStack (readonly)
188 189 190 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 188 def scope_stack @scope_stack end |
Instance Method Details
#add_offense_for(definition, in_shared_group:) ⇒ void
This method returns an undefined value.
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 236 def add_offense_for(definition, in_shared_group:) #: void node = definition.node #: untyped = (definition, in_shared_group: in_shared_group) highlight = if definition.def_helper? node.loc.keyword.join(node.loc.name) else node.block_type? ? node.send_node : node end add_offense(highlight, message: ) do |corrector| corrector.remove( range_by_whole_lines(node.source_range, include_final_newline: true) ) end end |
#after_block(node) ⇒ void
This method returns an undefined value.
A group's lets know whether they were referenced once its whole
subtree has been entered, which is complete by the time it is left.
176 177 178 179 180 181 182 183 184 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 176 def after_block(node) #: void return unless spec_group?(node) scope = scope_stack.pop return unless scope shared_groups = scope_stack.shared_groups_for(scope) report(scope, in_shared_group: shared_groups.any?) if reportable_in?(shared_groups) end |
#external_definitions ⇒ ExternalDefinitions
The SharedExamplePaths patterns resolved to file contents, memoized
since RuboCop asks for the checksum and builds the registry from the
same instance during one investigation.
196 197 198 199 200 201 202 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 196 def external_definitions #: ExternalDefinitions @external_definitions ||= ExternalDefinitions.new( patterns: Array(cop_config["SharedExamplePaths"]), base_dir: config.base_dir_for_path_parameters, target_ruby_version: target_ruby_version ) end |
#external_dependency_checksum ⇒ String?
The pre-loaded files' digest for RuboCop's result-cache key, which
otherwise covers no project file but the inspected one. nil when
nothing is pre-loaded. Asked for once per configuration, so reading
each file here is cheap.
156 157 158 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 156 def external_dependency_checksum #: String? external_definitions.checksum end |
#message_for(definition, in_shared_group:) ⇒ String
254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 254 def (definition, in_shared_group:) #: String helper = definition.helper name = definition.name if definition.def_helper? format(in_shared_group ? DEF_MSG_IN_SHARED_GROUP : DEF_MSG, name: name) elsif definition.anonymous? format(in_shared_group ? ANONYMOUS_SUBJECT_MSG_IN_SHARED_GROUP : ANONYMOUS_SUBJECT_MSG, helper: helper) else format(in_shared_group ? MSG_IN_SHARED_GROUP : MSG, helper: helper, name: name) end end |
#on_block(node) ⇒ void
This method returns an undefined value.
RuboCop visits nested groups on their own on_block, so we never
descend manually: the scope stack resolves this group's references
against its ancestry now, and descendant groups resolve against this
one later, as they are entered.
166 167 168 169 170 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 166 def on_block(node) #: void return unless spec_group?(node) scope_stack.push(builder.build_from(node)) end |
#on_new_investigation ⇒ void
This method returns an undefined value.
140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 140 def on_new_investigation #: void super @scope_stack = ScopeStack.new(check_helper_specs: cop_config["CheckHelperSpecs"]) @builder = ScopeBuilder.new( processed_source.file_path, SharedExampleRegistry.new( processed_source.ast, external_definitions.definitions(excluding: processed_source.file_path) ) ) end |
#report(scope, in_shared_group:) ⇒ void
This method returns an undefined value.
225 226 227 228 229 230 231 232 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 225 def report(scope, in_shared_group:) #: void scope.unreferenced_defs.each do |definition| next if definition.bang? && !cop_config["CheckLetBang"] next if definition.subject? && !cop_config["CheckSubject"] add_offense_for(definition, in_shared_group: in_shared_group) end end |
#reportable_in?(shared_groups) ⇒ Boolean
Whether a group enclosed by shared_groups (the shared blocks among it
and its ancestors, in no particular order) can be judged from this
file: only when every one of them carries examples. A shared block
without them is a provider, existing to inject its lets into
whichever group writes include_context, so a let it never
references itself is exactly what it is for, not dead code.
CheckSharedExamples: false puts every shared block — and every group
nested in one — off limits instead, since the groups that include the
block may live in files the cop never reads.
216 217 218 219 220 221 |
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 216 def reportable_in?(shared_groups) #: bool return true if shared_groups.empty? return false unless cop_config["CheckSharedExamples"] shared_groups.all?(&:carries_examples?) end |