Skip to content

Remove a dead member's overrides along with it - #63

Merged
PiotrRogulski merged 13 commits into
mainfrom
claude/modest-brahmagupta-g66oad
Sep 19, 2026
Merged

PiotrRogulski merged 13 commits into
mainfrom
claude/modest-brahmagupta-g66oad

Conversation

@PiotrRogulski

@PiotrRogulski PiotrRogulski commented Sep 18, 2026

Copy link
Copy Markdown
Member

Fixes #62.

An @override member is not a candidate, so removing only the member the finder reports left every override of it overriding nothing — override_on_non_overriding_member on code that analyzed cleanly before.

textDocument/implementation answers with every member overriding the one asked about, transitively and across files, so each dead member is asked once and its overrides are coupled to its removal. They stay out of the report, as coupled removals always have.

A member is left report-only, marked unsafe to auto-remove, when an override cannot be deleted: it is a declaring parameter of a primary constructor (deleting one changes the constructor signature at every call site), it is in a file the run did not scan, or it has references of its own.

Two more fixes came out of review:

  • A field override is deleted as a declarator. Coupled removals arrived as whole-node spans, which for a declarator covers only its own text, so final int a = 1, b = 2; could not be touched safely. CoupledRemoval now carries the kind and full range of what it deletes, and the remover's existing declarator handling takes over: the statement goes whole when every declarator does, one declarator comes out with its comma when only some do. One declarator can now reach the remover twice — reported in its own right and coupled to the member it overrides — which threw a RangeError, so it is deduplicated.
  • A field declarator reads its statement's metadata. A statement's doc comment, annotations and modifiers all sit on its first declarator, so b in @override final int a, b; reported none of its own and was checked where an @override member is skipped. It now reads the metadata of the declarator that opened the statement, which covers @pragma('vm:entry-point') the same way.

Verification

On the issue's repro, --remove deletes the member and both of its overrides, and dart analyze stays clean where it previously reported a dangling @override. The same holds for a three-level chain and for overrides in another file.

New fixture (example/lib/scenarios/overrides.dart + overrides_impl.dart) and nine tests cover: a transitive override, an override from another file, a plain body field, a declarator sharing its statement with another dead one and with a live one, the two report-only cases, a member called through the interface staying unreported, and the existing Animal.sound/Dog.sound pair in the default fixture. Two remover tests cover the duplicate declarator and the partial statement. --remove over the whole override scenario leaves dart analyze clean; published 0.5.0 leaves five errors on the same fixture.

On lichess-org/mobile (641 files, 13,043 declarations, -e 'lib/l10n/**'), where this was first hit: same 159 findings and same 13 report-only as before, 89 dead members asked about overrides, 2 coupled — EngineOpponent.displayName and MaiaWeightsService.downloading, each with its subclass implementation. flutter analyze after --remove goes from 18 issues to 16, the two removed being exactly the override_on_non_overriding_member warnings.

Cost

One textDocument/implementation per reported dead member, one textDocument/references per override found, and one textDocument/selectionRange per field override — findings are a small fraction of the declarations a run already queries. The lichess run above spent 0.5s on all 89 members.

🤖 Generated with Claude Code

https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw

An override annotated @OverRide is not a candidate, so removing only the
member the finder reports left every override of it overriding nothing:
override_on_non_overriding_member on code that analyzed cleanly before.

Ask textDocument/implementation for the overrides of each dead member and
couple them to its removal, wherever they live — the request answers
transitively and across files. A member is instead left report-only when an
override cannot be deleted: one in a file the run did not scan, one declared
as a field (it can be a declaring parameter, or constructor-initialized), or
one that has references of its own.

Fixes #62

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
Trim the doc comments, fixture comments, verbose lines and the report-only
hint this change added to what they need to say. The `field` note also had the
reason wrong: a constructor-initialized field is referenced by its own
constructor, so it never reads as dead in the first place; what makes a field
override unsafe to delete is a declaring parameter of a primary constructor,
which the outline reports as a field too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
Comment thread lib/src/finder.dart Outdated
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
@PiotrRogulski
PiotrRogulski marked this pull request as ready for review September 18, 2026 18:16
Comment thread README.md Outdated
A field override was left report-only for being a field. Only two field
shapes actually have to stay: a declaring parameter of a primary constructor,
which the outline also reports as a field and whose removal changes the
constructor signature at every call site, and a declarator sharing a statement
with others, whose span covers only its own text. Both are read from the
selection range already fetched for the override; a plain body field is now
coupled like any other override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
Comment thread example/lib/scenarios/overrides.dart Outdated
A declarator sharing its statement with others was left report-only, but the
remover already handles the shape: it deletes the statement whole when every
declarator goes, and takes one out of the list, comma and all, when only some
do. What it could not do was read a coupled removal, which arrived as a
whole-node span. Give CoupledRemoval the kind and full range of what it
deletes, so a field override goes through the same declarator path as a
reported one.

One declarator can now arrive twice, reported in its own right and coupled to
the member it overrides; the span arithmetic read it twice and threw a
RangeError, so it is deduplicated.

Only a declaring parameter of a primary constructor still blocks removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
Comment thread example/lib/scenarios/overrides.dart
Comment thread lib/src/remover.dart Outdated
Comment thread lib/src/overrides.dart Outdated
`b` in `@override final int a, b;` carries no doc comment, annotations or
modifiers of its own — they all sit on the statement's first declarator — so
it was checked where an @OverRide member is skipped, and reported when its
uses were invisible to a reference search. A declarator that starts at its own
name continues the statement before it, and reads that statement's metadata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw

@Komoszek Komoszek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably cut down most of the comments and we should update PR's description

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YuAi9rcyBRnHD6gyGBRqw
@PiotrRogulski
PiotrRogulski merged commit 7cff84e into main Sep 19, 2026
3 checks passed
@PiotrRogulski
PiotrRogulski deleted the claude/modest-brahmagupta-g66oad branch September 19, 2026 11:25
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.

--remove on an abstract member leaves its overrides with a dangling @override

3 participants