Skip to content

Remove sigs attached to methods defined with modifier calls#942

Open
dduugg wants to merge 1 commit into
Shopify:mainfrom
dduugg:deadcode-remove-sigs-with-visibility-modifiers
Open

Remove sigs attached to methods defined with modifier calls#942
dduugg wants to merge 1 commit into
Shopify:mainfrom
dduugg:deadcode-remove-sigs-with-visibility-modifiers

Conversation

@dduugg
Copy link
Copy Markdown

@dduugg dduugg commented Jun 7, 2026

Motivation

When the deadcode remover removes a method defined as the argument of a modifier
call — e.g. private def foo; end, private_class_method def self.foo; end, or
abstract def foo; end — it removes the method but leaves the method's sig
(and any attached comments) behind:

class Foo
  sig { params(value: Integer).void }
  private_class_method def self.bar(value)
    something
  end

  def baz; end
end

Removing bar currently produces:

class Foo
  sig { params(value: Integer).void }

  def baz; end
end

The orphaned sig then attaches to the next method definition (baz). This
goes wrong in a few ways:

  • It's a Sorbet error when the leftover sig doesn't match the following method —
    here baz has no value parameter.
  • When the following method has its own sig, removal leaves two sigs with no
    method definition between them, which is also a Sorbet error.
  • When the leftover sig happens to be compatible with the following method, it's
    arguably worse: it silently re-types an unrelated method with no error at all.

Cause

For private_class_method def self.bar, the def node's parent in the nesting
is the modifier call's arguments rather than the enclosing class/module body.
attached_sigs walks the def's siblings, so it never sees the sig (and
comments) that are declared as siblings of the modifier call.

Fix

When removing a def that is wrapped in a modifier call, target the (outermost)
wrapping call instead of the def, so its attached sigs and comments are removed
together with the method.

The wrapping call is detected structurally (a call that takes the method as an
argument, climbing through nested modifiers) rather than by matching a fixed list of
names. This way user-defined modifiers and future ones — e.g. Sorbet's proposed
abstract def — are handled without
maintaining an allowlist.

To stay safe, a call only counts as a modifier when the def is its sole
argument (and it takes no block). Such a call exists only to wrap that one method,
so removing it whole is correct regardless of the modifier's name. A call that takes
other arguments — e.g. register(:thing, def bar; end) or memoize def bar; end, ttl: 60
— has its own purpose, so we leave the call (and its sibling arguments) in place and
remove only the def. The only remaining ambiguous case, a single-argument
user-defined macro with a side effect, is structurally indistinguishable from a real
modifier and vanishingly rare, so we treat it like one.

After the fix the example above becomes:

class Foo
  def baz; end
end

Tests

Added tests covering:

  • a visibility modifier (private_class_method def self.bar), an arbitrary custom
    modifier, and nested modifiers (private abstract def bar) — the sole-argument
    cases where the whole wrapping call (and its sigs/comments) is removed;
  • a modifier-wrapped def as the last statement in the body;
  • single-line and sig-less modifier forms;
  • a def passed as a non-sole argument (register(:thing, def bar; end)) and a call
    with a trailing keyword argument (memoize(def bar…, ttl: 60)) — confirming the
    call and its sibling arguments are preserved and only the def is removed.

The full remover suite and srb tc/RuboCop pass.

@dduugg dduugg force-pushed the deadcode-remove-sigs-with-visibility-modifiers branch from 294ba0a to 07b6349 Compare June 7, 2026 17:50
@dduugg dduugg changed the title Remove sigs attached to methods defined with visibility modifiers Remove sigs attached to methods defined with modifier calls Jun 7, 2026
@dduugg dduugg marked this pull request as ready for review June 7, 2026 18:18
@dduugg dduugg requested a review from a team as a code owner June 7, 2026 18:18
When the deadcode remover removes a method defined as the argument of a modifier
call — e.g. `private def foo; end`, `private_class_method def self.foo; end`, or
`abstract def foo; end` — it left the method's `sig` (and any attached comments)
behind, producing an orphaned `sig` that Sorbet then flags (`Unused type
annotation`).

The cause: for `private_class_method def self.bar`, the `def` node's parent in the
nesting is the modifier call's arguments rather than the enclosing class/module
body, so `attached_sigs` (which walks the def's siblings) never sees the sig and
comments declared alongside the modifier call.

Fix: when removing a `def` wrapped in a modifier call, target the outermost
wrapping call instead, so its attached sigs and comments are removed together with
the method. The wrapping call is detected structurally (climbing through nested
modifiers) rather than from a fixed list of names, so user-defined and future
modifiers — e.g. Sorbet's proposed `abstract def`
(sorbet/sorbet#10276) — are handled without an allowlist.

To stay safe, a call only counts as a modifier when the def is its sole argument
(and it takes no block): such a call exists only to wrap that one method, so
removing it whole is correct regardless of name. A call with other arguments — e.g.
`register(:thing, def bar; end)` or `memoize def bar; end, ttl: 60` — keeps its
call and sibling arguments; only the def is removed.

Adds tests for visibility, custom, and nested modifiers; a modifier-wrapped def as
the last statement; single-line and sig-less forms; and non-sole-argument calls
whose siblings must be preserved. Regenerates rbi/spoom.rbi for the new method.
@dduugg dduugg force-pushed the deadcode-remove-sigs-with-visibility-modifiers branch from 7576d44 to 5a78009 Compare June 7, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant