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

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/rspec/unused_let.rb,
sig/rubocop/cop/rspec/unused_let.rbs

Overview

Checks for let definitions 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. This cop flags such definitions.

let! is checked as well by default. Since it is sometimes used purely for its side effects (e.g. let!(:user) { create(:user) }), you can opt out with CheckLetBang: false.

To avoid false positives, the cop deliberately stays silent whenever it cannot see every possible reference:

  • let definitions inside a shared_examples/shared_context block are ignored, because their consumers live in the (possibly external) including example groups.
  • When an example group's subtree contains a shared example inclusion (it_behaves_like, include_examples, include_context, ...), the let definitions visible at that inclusion point are ignored, because the (possibly external) shared block may reference them.
  • let definitions that participate in an override chain (redefined in a nested group, or overriding an outer definition) are ignored, since the outer definition may be reached through super.

Dynamic references such as send(:foo) are treated as usages.

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

CheckLetBang: true (default)

# bad
describe Foo do
  let!(:widget) { create(:widget) }

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

CheckLetBang: false

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

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

Constant Summary collapse

MSG =

Returns:

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

Returns:

  • (Object)
%i[
  send public_send __send__ method respond_to?
].freeze

Instance Method Summary collapse

Methods inherited from Base

#add_offense, #cop_config, def_node_matcher, def_node_search

Instance Method Details

#check_let(group, let) ⇒ void

This method returns an undefined value.

Parameters:

  • group (RuboCop::AST::Node)
  • let (RuboCop::AST::Node)


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

def check_let(group, let) #: void
  helper, name = let_definition(let)
  return unless name
  return if helper == :let! && !cop_config["CheckLetBang"]
  return if override_chain?(group, name)
  return if referenced?(group, name)

  node = let #: untyped
  send_node = node.block_type? ? node.send_node : node
  add_offense(send_node, message: format(MSG, helper: helper, name: name))
end

#contains_inclusion?Boolean

Parameters:

  • node (RuboCop::AST::Node)

Returns:

  • (Boolean)


75
# File 'sig/rubocop/cop/rspec/unused_let.rbs', line 75

def contains_inclusion?: (RuboCop::AST::Node node) -> bool

#dynamically_referenced?(group, name) ⇒ Boolean

Parameters:

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

Returns:

  • (Boolean)


136
137
138
139
140
141
142
143
144
145
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 136

def dynamically_referenced?(group, name) #: bool
  target = name.to_s
  group.each_descendant(:send).any? do |send_node|
    node = send_node #: untyped
    next false unless DYNAMIC_DISPATCH_METHODS.include?(node.method_name)

    arg = node.first_argument
    arg&.type?(:sym, :str) && arg.value.to_s == target
  end
end

#example_group?Boolean

Parameters:

  • node (RuboCop::AST::Node)

Returns:

  • (Boolean)


67
# File 'sig/rubocop/cop/rspec/unused_let.rbs', line 67

def example_group?: (RuboCop::AST::Node node) -> bool

#let_definition[ Symbol, Symbol | String ]?

Parameters:

  • node (RuboCop::AST::Node)

Returns:

  • ([ Symbol, Symbol | String ], nil)


71
# File 'sig/rubocop/cop/rspec/unused_let.rbs', line 71

def let_definition: (RuboCop::AST::Node node) -> [ Symbol, Symbol | String ]?

#method_called?Boolean

Parameters:

  • node (RuboCop::AST::Node)
  • name (Symbol)

Returns:

  • (Boolean)


73
# File 'sig/rubocop/cop/rspec/unused_let.rbs', line 73

def method_called?: (RuboCop::AST::Node node, Symbol name) -> bool

#on_block(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (RuboCop::AST::Node)


100
101
102
103
104
105
106
107
108
109
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 100

def on_block(node) #: void
  return unless example_group?(node)
  # A shared example inclusion anywhere in this group's subtree can
  # consume any `let` visible here, so we cannot judge them.
  return if contains_inclusion?(node)

  RuboCop::RSpec::ExampleGroup.new(node).lets.each do |let|
    check_let(node, let)
  end
end

#override_chain?(group, name) ⇒ Boolean

Parameters:

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

Returns:

  • (Boolean)


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

def override_chain?(group, name) #: bool
  overrides_outer?(group, name) ||
    redefined_in_descendant?(group, name)
end

#overrides_outer?(group, name) ⇒ Boolean

Parameters:

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

Returns:

  • (Boolean)


156
157
158
159
160
161
162
163
164
165
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 156

def overrides_outer?(group, name) #: bool
  group.each_ancestor(:block).any? do |ancestor|
    next false unless spec_group?(ancestor)

    RuboCop::RSpec::ExampleGroup.new(ancestor).lets.any? do |let|
      _, other = let_definition(let)
      other == name
    end
  end
end

#redefined_in_descendant?(group, name) ⇒ Boolean

Parameters:

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

Returns:

  • (Boolean)


169
170
171
172
173
174
175
176
177
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 169

def redefined_in_descendant?(group, name) #: bool
  count = 0
  group.each_descendant(:block, :send) do |descendant|
    _, other = let_definition(descendant)
    count += 1 if other == name
    return true if count > 1
  end
  false
end

#referenced?(group, name) ⇒ Boolean

Parameters:

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

Returns:

  • (Boolean)


129
130
131
132
# File 'lib/rubocop/cop/rspec/unused_let.rb', line 129

def referenced?(group, name) #: bool
  method_called?(group, name.to_sym) ||
    dynamically_referenced?(group, name)
end

#spec_group?Boolean

Parameters:

  • node (RuboCop::AST::Node)

Returns:

  • (Boolean)


69
# File 'sig/rubocop/cop/rspec/unused_let.rbs', line 69

def spec_group?: (RuboCop::AST::Node node) -> bool