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

Inherits:
Base
  • Object
show all
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.

Examples:

# bad
describe Foo do
  let(:used) { 1 }
  let(:unused) { 2 }

  it { expect(used).to eq(1) }
end

# good
describe Foo do
  let(:used) { 1 }

  it { expect(used).to eq(1) }
end

# bad - a subject and a helper method are checked the same way
describe Foo do
  subject(:widget) { described_class.new }

  def unused
    1
  end

  it { expect(Foo.count).to eq(0) }
end

# good
describe Foo do
  subject(:widget) { described_class.new }

  def expected_size
    1
  end

  it { is_expected.to be_valid }
  it { expect(widget.size).to eq(expected_size) }
end

CheckLetBang: false

# good - `let!`/`subject!` is assumed to be used for its side effects
describe Foo do
  let!(:widget) { create(:widget) }

  it { expect(Widget.count).to eq(1) }
end

CheckSubject: false

# good - a `subject` is left alone even when nothing references it
describe Foo do
  subject(:widget) { described_class.new }

  it { expect(Foo.count).to eq(0) }
end

CheckHelperSpecs: true

# bad - a helper spec, left alone by default, is checked like any
# other group
describe MyHelper, type: :helper do
  let(:unused) { 1 }

  it { expect(helper.greeting).to eq("Hi") }
end

CheckSharedExamples: false

# good - every `let` inside a shared block is left alone, in case a
# group including the block references it from another file
RSpec.shared_examples "a thing" do
  let(:unused) { 1 }

  it { expect(Foo.count).to eq(1) }
end

Defined Under Namespace

Modules: Matchers, References Classes: ExternalDefinitions, Scope, ScopeBuilder, ScopeStack, SharedExampleRegistry

Constant Summary collapse

MSG =

Returns:

  • (::String)
"`%<helper>s(:%<name>s)` is not referenced anywhere. " \
"Remove it or reference it in an example."
DEF_MSG =

Returns:

  • (::String)
"`def %<name>s` is not referenced anywhere. " \
"Remove it or reference it in an example."
ANONYMOUS_SUBJECT_MSG =

Returns:

  • (::String)
"`%<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.

Returns:

  • (::String)
"`%<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 =

Returns:

  • (::String)
"`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 =

Returns:

  • (::String)
"`%<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

Instance Method Summary collapse

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

#builderScopeBuilder (readonly)

Signature:

  • ScopeBuilder

Returns:



189
190
191
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 189

def builder
  @builder
end

#scope_stackScopeStack (readonly)

Signature:

  • ScopeStack

Returns:



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.

RBS:

  • definition: Scope::Definition

  • in_shared_group: bool

Parameters:



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
  message = message_for(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: 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.

RBS:

  • node: RuboCop::AST::Node

Parameters:

  • node (RuboCop::AST::Node)


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_definitionsExternalDefinitions

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.

Returns:



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_checksumString?

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.

Returns:

  • (String, nil)


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

RBS:

  • definition: Scope::Definition

  • in_shared_group: bool

Parameters:

Returns:

  • (String)


254
255
256
257
258
259
260
261
262
263
264
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 254

def message_for(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.

RBS:

  • node: RuboCop::AST::Node

Parameters:

  • node (RuboCop::AST::Node)


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_investigationvoid

This method returns an undefined value.

Signature:

  • void



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.

RBS:

  • scope: Scope

  • in_shared_group: bool

Parameters:

  • scope (Scope)
  • in_shared_group: (Boolean)


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.

RBS:

  • shared_groups: Array[Scope]

Parameters:

  • shared_groups (Array[Scope])

Returns:

  • (Boolean)


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