Class: RuboCop::Cop::Sorbet::BuggyObsoleteStrictMemoization

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
Alignment, LineLengthHelp, MatchRange, RangeHelp, TargetSorbetVersion
Defined in:
lib/rubocop/cop/sorbet/buggy_obsolete_strict_memoization.rb

Overview

Checks for the a mistaken variant of the “obsolete memoization pattern” that used to be required for older Sorbet versions in ‘#typed: strict` files. The mistaken variant would overwrite the ivar with `nil` on every call, causing the memoized value to be discarded and recomputed on every call.

This cop will correct it to read from the ivar instead of ‘nil`, which will memoize it correctly.

The result of this correction will be the “obsolete memoization pattern”, which can further be corrected by the ‘Sorbet/ObsoleteStrictMemoization` cop.

See ‘Sorbet/ObsoleteStrictMemoization` for more details.

Examples:

# bad
sig { returns(Foo) }
def foo
  # This `nil` is likely a mistake, causing the memoized value to be discarded and recomputed on every call.
  @foo = T.let(nil, T.nilable(Foo))
  @foo ||= some_computation
end

# good
sig { returns(Foo) }
def foo
  # This will now memoize the value as was likely intended, so `some_computation` is only ever called once.
  # ⚠️If `some_computation` has side effects, this might be a breaking change!
  @foo = T.let(@foo, T.nilable(Foo))
  @foo ||= some_computation
end

See Also:

  • Sorbet/ObsoleteStrictMemoization

Cop Safety Information:

  • If the computation being memoized had side effects, calling it only once (instead of once on every call to the affected method) can be observed, and might be a breaking change.

Constant Summary collapse

MSG =
"This might be a mistaken variant of the two-stage workaround that used to be needed for memoization " \
"in `#typed: strict` files. See https://sorbet.org/docs/type-assertions#put-type-assertions-behind-memoization."

Instance Method Summary collapse

Methods included from TargetSorbetVersion

#enabled_for_sorbet_static_version?, included, #read_sorbet_static_version_from_bundler_lock_file, #sorbet_enabled?, #target_sorbet_static_version_from_bundler_lock_file

Instance Method Details

#buggy_legacy_memoization_pattern?(node) ⇒ Object



55
56
57
58
59
60
61
62
63
64
# File 'lib/rubocop/cop/sorbet/buggy_obsolete_strict_memoization.rb', line 55

def_node_matcher :buggy_legacy_memoization_pattern?, <<~PATTERN
  (begin
    ...                                                       # Ignore any other lines that come first.
    (ivasgn $_ivar                                           # First line: @_ivar = ...
      (send                                                   # T.let(_ivar, T.nilable(_ivar_type))
        (const {nil? cbase} :T) :let
        $nil
        (send (const {nil? cbase} :T) :nilable _ivar_type))) # T.nilable(_ivar_type)
    (or-asgn (ivasgn _ivar) _initialization_expr))          # Second line: @_ivar ||= _initialization_expr
PATTERN

#on_begin(node) ⇒ Object



66
67
68
69
70
71
72
73
74
75
# File 'lib/rubocop/cop/sorbet/buggy_obsolete_strict_memoization.rb', line 66

def on_begin(node)
  buggy_legacy_memoization_pattern?(node) do |ivar, nil_node|
    add_offense(nil_node) do |corrector|
      corrector.replace(
        range_between(nil_node.source_range.begin_pos, nil_node.source_range.end_pos),
        ivar,
      )
    end
  end
end

#relevant_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/rubocop/cop/sorbet/buggy_obsolete_strict_memoization.rb', line 77

def relevant_file?(file)
  super && sorbet_enabled?
end