Remove sigs attached to methods defined with modifier calls#942
Open
dduugg wants to merge 1 commit into
Open
Conversation
294ba0a to
07b6349
Compare
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.
7576d44 to
5a78009
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, orabstract def foo; end— it removes the method but leaves the method'ssig(and any attached comments) behind:
Removing
barcurrently produces:The orphaned
sigthen attaches to the next method definition (baz). Thisgoes wrong in a few ways:
here
bazhas novalueparameter.sigs with nomethod definition between them, which is also a Sorbet error.
arguably worse: it silently re-types an unrelated method with no error at all.
Cause
For
private_class_method def self.bar, thedefnode's parent in the nestingis the modifier call's arguments rather than the enclosing class/module body.
attached_sigswalks thedef's siblings, so it never sees thesig(andcomments) that are declared as siblings of the modifier call.
Fix
When removing a
defthat is wrapped in a modifier call, target the (outermost)wrapping call instead of the
def, so its attached sigs and comments are removedtogether 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 withoutmaintaining an allowlist.
To stay safe, a call only counts as a modifier when the
defis its soleargument (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)ormemoize 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-argumentuser-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:
Tests
Added tests covering:
private_class_method def self.bar), an arbitrary custommodifier, and nested modifiers (
private abstract def bar) — the sole-argumentcases where the whole wrapping call (and its sigs/comments) is removed;
defpassed as a non-sole argument (register(:thing, def bar; end)) and a callwith a trailing keyword argument (
memoize(def bar…, ttl: 60)) — confirming thecall and its sibling arguments are preserved and only the
defis removed.The full remover suite and
srb tc/RuboCop pass.