Skip to content

fix(dart): an annotation links the constant or constructor it names, even behind a comment - #2390

Merged
colbymchenry merged 2 commits into
mainfrom
claude/blissful-proskuriakova-d43580
Oct 6, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/blissful-proskuriakova-d43580

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two Dart decorates bugs, fixed in that order.

Resolution: annotations went to a method or function by name. A Dart annotation is a constant expression: a reference to a const variable (@riverpod, @override, @meta.immutable, a type's static constant or enum value) or a call of a const constructor (@Riverpod(keepAlive: true), @Foo.named(…)). decorates refs were ranked like Python decorators (+25 for a function or method, +15 for a class, nothing for a constant). On rrousselGit/riverpod that sent all 506 @riverpod annotations to riverpod_analyzer_utils' extension getter RiverpodAnnotatedAnnotatedNodeOfX::riverpod (0.4) instead of riverpod_annotation's const riverpod = Riverpod();.

matchDartAnnotation (name-matcher.ts) now takes every Dart decorates ref first in resolveOneInner, with no fallthrough. It reads the annotation as written from its @, in comment-blanked source, and counts it as a call only when ( follows on the same line. It links:

Extraction: a comment hid the annotation. extractDecoratorsFor's preceding-sibling scan (scan #2) stopped at the first sibling that wasn't an annotation. So in @override // ignore: must_call_super void f(), @x /// Doc below. void f(), or riverpod_lint's @riverpod // expect_lint: … fixtures, the annotation attached to nothing. In Dart an annotation always belongs to the next declaration, so the Dart-only extractor field decoratorStepOverTypes: ['comment', 'documentation_comment'] makes the scan step over both comment kinds. The kernel twin is DECORATOR_STEP_OVER in dart.rs's extract_decorators_for. No other language sets it.

Validation

Validated on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2, with the kernel loaded, against main at 631df01. The diff keys edges by (source, target, kind, line, col, metadata):

riverpod bloc
decorates edges retargeted 506 (@riverpod: getter 0.4 → const riverpod 0.9) 0
edges added 16 (@riverpod behind a comment → const riverpod) 0
edges removed 0 0
decorates refs added 54 on 48 declarations (@override 32, @internal 6, @riverpod 16) 7 (all @override)
nodes, files identical identical
  • Every other decorates edge is identical: riverpod's $internal, Riverpod, Dependencies, $mustCallSuper, JsonPersist, and so on.
  • Every annotation the extractors record in both repos is written with a single name, so the prefixed and T.x forms are covered only by the new test.
  • An earlier AST scan counted 55 hidden riverpod annotations. The 55th is @override on a field, and no Dart extractor records fields.
  • Kernel and wasm full-index dumps are byte-identical on both repos.
  • scripts/kernel-parity.mjs --lang dart --max-deferral 0.3 reports 0 diffs: bloc 595/616 and riverpod 1037/1303, with the deferred files being the usual parse-error ones.
  • Timing: resolving riverpod's 12,061 decorates refs takes about 450 ms per warm round both before and after. Interleaved whole-index timings show no regression.

Tests

  • __tests__/dart-annotation-targets.test.ts: resolution, end to end, on both backends. It covers the riverpod getter decoy, a function decoy, a constant the file can't see, prefixed @meta.…, a named constructor, a static constant, an enum value, a member's enclosing static constant, a private constant across part, and an annotation behind a comment.
  • __tests__/dart-annotation-comments.test.ts: extraction on kernel and wasm, with LF and CRLF line endings.
  • New parity fixture TortureAnnotatedComments.dart.
  • Both new test files fail on unmodified main with main's kernel (6/6 tests fail).
  • CODEGRAPH_KERNEL_EXPECT=1: the Dart, parity, resolution and extraction suites pass (22 files, 1,054 tests).
  • Full suite: 6,240 passed and 80 skipped. 7 tests timed out under load in git-index-currency, mcp-status-freshness and oversize-file-not-read; all 20 tests in those files pass when run alone.

Docs: two [Unreleased] Fixes bullets in CHANGELOG, and docs/design/dart-kernel-port-checklist.md §Decorators updated (the scan #2 step-over and the new resolution rules).

Overlap

Open PR #2389 (Dart calls through an import prefix) adds dartImportPrefixes() to dart-libraries.ts. It answers the same question as isDartImportPrefix() here, and it also follows a part's part of parent where no pubspec is found. Whichever PR lands second should keep one helper. One way: isDartImportPrefix(file, name, context) becomes dartImportPrefixes(file, context).has(name), and then dart-annotation-targets.test.ts is re-run.

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 6, 2026 12:19
…even behind a comment

A Dart annotation is a constant expression: a const variable (`@riverpod`,
`@meta.immutable`, a type's static constant or enum value) or a call of a
const constructor (`@Riverpod(keepAlive: true)`, `@Foo.named(…)`). It was
ranked like a Python decorator, which favours a function or method of that
name over a class and scores a constant not at all, so riverpod's 506
`@riverpod` annotations went to riverpod_analyzer_utils' extension getter
`riverpod` (0.4) instead of riverpod_annotation's `const riverpod = Riverpod();`.

matchDartAnnotation (name-matcher.ts) now takes every Dart `decorates` ref
before any other strategy, and nothing falls through. It reads the
annotation as written from its `@` and links a constant or (called) a class
the file's library can see, through the import prefix it is written with
(isDartImportPrefix, dart-libraries.ts); a type's own static constant, enum
value or (called) named constructor (isDartConstructor); or, on a member, a
static constant of the type around it. Never a method, getter or function.

The decorator scan also stopped at the first sibling that was not an
annotation, so a comment between an annotation and its declaration
(`@override` `// ignore: must_call_super` `void f()`, riverpod_lint's
`@riverpod` `// expect_lint: …` fixtures) hid the annotation. Dart's
decoratorStepOverTypes lists its two comment kinds, which the scan now
steps over, and the kernel's extract_decorators_for steps over the same.

Validated on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2 with
the kernel loaded: riverpod's 506 `@riverpod` edges move to `const riverpod`
and 16 annotations behind a comment add an edge each, with nothing removed;
bloc gains 7 `@override` refs and no edge changes. Kernel and wasm full-index
dumps are byte-identical, and the parity sweeps show 0 diffs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit d2fb323 into main Oct 6, 2026
colbymchenry added a commit that referenced this pull request Oct 6, 2026
…ort exports

A Dart call written through an import prefix - `http.get(uri)` after
`import 'package:http/http.dart' as http;` - was resolved like a method call
on a receiver named `http`, so it linked to any project member that fit the
name: riverpod's docs' `http.get(...)` / `http.post(...)` calls went to a docs
example's `Http::get` / `Http::post` by the receiver's capitalized name, and
`fmt.describe(...)` to a class's `describe` method.

Now `p.name(...)` calls a top-level declaration a library imported with that
prefix exports (export chains, show / hide and parts followed through the
library model), or nothing when the prefixed library is a package outside the
repository. A constructor call (`p.Widget()`) instantiates the prefixed
library's class. The same holds where the prefix heads a chain -
`p.Box.create()`, `p.Report.empty()` - and for `p.Box<int>.named(...)`, which
arrives as `Box.named`: the type is the one the prefix brings in. A
parameter, local or member named like the prefix still hides it, and
`const p.Box.named()`, which the extractors record as `p.named`, links
nothing.

The library model's one prefix reader is `dartImportPrefixes`: Dart
annotations (#2390's `matchDartAnnotation`) read prefixes through it too, in
place of the `isDartImportPrefix` it had alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 6, 2026
…ort exports (#2389)

A Dart call written through an import prefix - `http.get(uri)` after
`import 'package:http/http.dart' as http;` - was resolved like a method call
on a receiver named `http`, so it linked to any project member that fit the
name: riverpod's docs' `http.get(...)` / `http.post(...)` calls went to a docs
example's `Http::get` / `Http::post` by the receiver's capitalized name, and
`fmt.describe(...)` to a class's `describe` method.

Now `p.name(...)` calls a top-level declaration a library imported with that
prefix exports (export chains, show / hide and parts followed through the
library model), or nothing when the prefixed library is a package outside the
repository. A constructor call (`p.Widget()`) instantiates the prefixed
library's class. The same holds where the prefix heads a chain -
`p.Box.create()`, `p.Report.empty()` - and for `p.Box<int>.named(...)`, which
arrives as `Box.named`: the type is the one the prefix brings in. A
parameter, local or member named like the prefix still hides it, and
`const p.Box.named()`, which the extractors record as `p.named`, links
nothing.

The library model's one prefix reader is `dartImportPrefixes`: Dart
annotations (#2390's `matchDartAnnotation`) read prefixes through it too, in
place of the `isDartImportPrefix` it had alongside.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry deleted the claude/blissful-proskuriakova-d43580 branch October 10, 2026 06:15
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