Repository navigation
fix(dart): an annotation links the constant or constructor it names, even behind a comment - #2390
Merged
Merged
Conversation
…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>
…uriakova-d43580 # Conflicts: # CHANGELOG.md
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>
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.
Summary
Two Dart
decoratesbugs, 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
constvariable (@riverpod,@override,@meta.immutable, a type's static constant or enum value) or a call of aconstconstructor (@Riverpod(keepAlive: true),@Foo.named(…)).decoratesrefs 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@riverpodannotations to riverpod_analyzer_utils' extension getterRiverpodAnnotatedAnnotatedNodeOfX::riverpod(0.4) instead of riverpod_annotation'sconst riverpod = Riverpod();.matchDartAnnotation(name-matcher.ts) now takes every Dartdecoratesref first inresolveOneInner, 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:@x: on a member, a static constant or enum value of the type around it. Otherwise, a top-levelconstantthe file's library can see, using fix(dart): a bare call or type name reaches only what its library can see #2386's model, where the own library shadows imports.@X(…): a visible class, not an extension.@p.x/@p.X(…): the same, through an import prefix. The newisDartImportPrefixin dart-libraries.ts checks the imports of the file's whole library.@T.x: T's own constant or enum value.@T.named(…)is T's named constructor, using fix(dart): index const constructors and redirecting factories; a call through a type stays on that type #2380'sisDartConstructor.@p.T.…works the same through a prefix.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_supervoid 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 fielddecoratorStepOverTypes: ['comment', 'documentation_comment']makes the scan step over both comment kinds. The kernel twin isDECORATOR_STEP_OVERin dart.rs'sextract_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):
decoratesedges retargeted@riverpod: getter 0.4 →const riverpod0.9)@riverpodbehind a comment →const riverpod)decoratesrefs added@override32,@internal6,@riverpod16)@override)decoratesedge is identical: riverpod's$internal,Riverpod,Dependencies,$mustCallSuper,JsonPersist, and so on.T.xforms are covered only by the new test.@overrideon a field, and no Dart extractor records fields.scripts/kernel-parity.mjs --lang dart --max-deferral 0.3reports 0 diffs: bloc 595/616 and riverpod 1037/1303, with the deferred files being the usual parse-error ones.decoratesrefs 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 acrosspart, and an annotation behind a comment.__tests__/dart-annotation-comments.test.ts: extraction on kernel and wasm, with LF and CRLF line endings.TortureAnnotatedComments.dart.CODEGRAPH_KERNEL_EXPECT=1: the Dart, parity, resolution and extraction suites pass (22 files, 1,054 tests).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 asisDartImportPrefix()here, and it also follows a part'spart ofparent where no pubspec is found. Whichever PR lands second should keep one helper. One way:isDartImportPrefix(file, name, context)becomesdartImportPrefixes(file, context).has(name), and thendart-annotation-targets.test.tsis re-run.🤖 Generated with Claude Code