mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 08:47:04 +08:00
fix(dart): an annotation links the constant or constructor it names, even behind a comment (#2390)
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>
This commit is contained in:
@@ -57,6 +57,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, a comment inside a chain of calls no longer loses part of it: a call with a comment before its arguments, like `box.grow /* by */ (3)`, now links to the method it calls, and a getter read or a static access written after a comment, like `Config //` followed by `.instance` on the next line (the empty `//` keeps `dart format` from joining the lines), now counts as a use. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call, a constructor call or a type written without a receiver now links only to a declaration the file can see: one in its own library (the file and its `part` files) or one exported by a library it imports, with `export` chains, `show`, `hide` and `as` prefixes followed the way Dart follows them. So a generated `.g.dart` file reaches its own library's function or type instead of a same-named one in a neighbouring file, a call to a package function such as `fakeAsync`, `runApp` or `unawaited` no longer links to an unrelated project function of that name, and a bare call is never taken for another type's enum value or static constant. Re-index Dart projects after upgrading.
|
||||
- In Dart, an `import` or `export` now links to the file it names: `package:my_app/src/cart.dart` to that package's `lib/src/cart.dart`, and a relative path to the file it points to from the importing file. Before, only the file name at the end was matched, so `import 'package:flutter/widgets.dart'` linked to whichever project file was called `widgets.dart`, and most imports of your own files didn't link to the file at all, which threw off `codegraph affected` and the files listed as using a file. An import of a Dart SDK library or of a package from outside your project now links nothing, and when packages in one repository share a name, the one the importing app depends on by `path:` is used. Re-index Dart projects after upgrading.
|
||||
- In Dart, an annotation now links to the constant or constructor it names, read the way it is written: `@riverpod` reaches Riverpod's `const riverpod = Riverpod();`, `@Riverpod(keepAlive: true)` the `Riverpod` class, an annotation written through a class, like `@SomeClass.value` or `@SomeClass.named(…)`, that class's own constant, enum value or constructor, and `@meta.immutable` only what the `meta` import brings in. Before, an annotation was matched like a function decorator, so a method, getter or function that only shared its name won: every `@riverpod` provider in Riverpod's own repository was linked to an unrelated getter called `riverpod`, and `callers` and impact for the real annotation missed them. Re-index Dart projects after upgrading.
|
||||
- In Dart, an annotation is no longer lost when a comment sits between it and the declaration, like `@override` followed by `// ignore: must_call_super`, a `///` comment written below the annotations, or `@riverpod` followed by an `// expect_lint: …` line. Before, the comment hid the annotation, so a provider written that way was missing from the annotation's `callers` and impact. Re-index Dart projects after upgrading.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* A comment between a Dart annotation and the declaration it belongs to hid
|
||||
* the annotation:
|
||||
*
|
||||
* @override
|
||||
* // ignore: must_call_super
|
||||
* void dispose() { … }
|
||||
*
|
||||
* Both extractors look for a member's annotations among the siblings before
|
||||
* it, and stopped at the first one that was not an annotation, so the comment
|
||||
* ended the scan and `@override` was recorded nowhere. felangel/bloc hid 7
|
||||
* annotations this way and rrousselGit/riverpod 55, among them its lint
|
||||
* fixtures' `@riverpod` `// expect_lint: …` providers.
|
||||
*
|
||||
* An annotation always belongs to the declaration that follows it, so the
|
||||
* scan now steps over line, block and doc comments of either form, wherever
|
||||
* they sit among stacked annotations. Whatever else comes first (a field, the
|
||||
* previous member's body, a top-level variable, an import) still ends it.
|
||||
*
|
||||
* Runs against the native kernel (when built) and the wasm extractor, which
|
||||
* must agree.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
|
||||
import type { ExtractionResult } from '../src/types';
|
||||
|
||||
const KERNEL_PATH = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'codegraph-kernel',
|
||||
'prebuilds',
|
||||
`${process.platform}-${process.arch}`,
|
||||
'codegraph-kernel.node'
|
||||
);
|
||||
const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';
|
||||
|
||||
const SOURCE = `import 'package:meta/meta.dart';
|
||||
// After the import.
|
||||
@visibleForTesting
|
||||
// Below the annotation.
|
||||
void afterImport() {}
|
||||
|
||||
@override // On the annotation's line.
|
||||
void trailing() {}
|
||||
|
||||
@riverpod
|
||||
// expect_lint: functional_ref
|
||||
int counter(Ref ref) => 0;
|
||||
|
||||
@Deprecated('x')
|
||||
/// Doc below the annotation.
|
||||
/* And a block comment. */
|
||||
void docBelow() {}
|
||||
|
||||
@a
|
||||
// one
|
||||
@b
|
||||
/** two */
|
||||
@c
|
||||
void stacked() {}
|
||||
|
||||
@x
|
||||
final answer = 42;
|
||||
// After a variable.
|
||||
void afterVariable() {}
|
||||
|
||||
class Counter {
|
||||
@override
|
||||
// ignore: must_call_super
|
||||
void dispose() {}
|
||||
|
||||
@protected
|
||||
// A comment.
|
||||
void bodiless();
|
||||
|
||||
@literal
|
||||
// A comment.
|
||||
const Counter.zero();
|
||||
|
||||
@x /* inline */ void inline() {}
|
||||
|
||||
@JsonKey(name: 'value')
|
||||
final int value = 0;
|
||||
// After a field.
|
||||
void afterField() {}
|
||||
|
||||
void before() {} // Trailing the previous member.
|
||||
@override
|
||||
void afterBody() {}
|
||||
}
|
||||
`;
|
||||
|
||||
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
|
||||
|
||||
describe('Dart annotations behind a comment still decorate the declaration', () => {
|
||||
let savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['dart']);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
||||
resetKernelForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const k of ENV_KEYS) {
|
||||
if (savedEnv[k] === undefined) delete process.env[k];
|
||||
else process.env[k] = savedEnv[k];
|
||||
}
|
||||
resetKernelForTests();
|
||||
});
|
||||
|
||||
function extract(backend: 'kernel' | 'wasm', source: string): ExtractionResult {
|
||||
if (backend === 'wasm') {
|
||||
process.env.CODEGRAPH_KERNEL = '0';
|
||||
return extractFromSource('lib/counter.dart', source, 'dart');
|
||||
}
|
||||
delete process.env.CODEGRAPH_KERNEL;
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
const result = tryKernelExtract('lib/counter.dart', source, 'dart');
|
||||
expect(result, 'kernel extraction').not.toBeNull();
|
||||
return result!;
|
||||
}
|
||||
|
||||
const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const);
|
||||
|
||||
for (const crlf of [false, true]) {
|
||||
it.each(backends)(`%s${crlf ? ' (CRLF)' : ''}`, (backend) => {
|
||||
const result = extract(backend, crlf ? SOURCE.replace(/\n/g, '\r\n') : SOURCE);
|
||||
/** The annotation names recorded on a declaration, in the order they were emitted. */
|
||||
const decorators = (qualifiedName: string): string[] => {
|
||||
const node = result.nodes.find((n) => n.qualifiedName === qualifiedName);
|
||||
expect(node, qualifiedName).toBeDefined();
|
||||
return result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'decorates' && r.fromNodeId === node!.id)
|
||||
.map((r) => r.referenceName);
|
||||
};
|
||||
|
||||
// Line, block and doc comments between an annotation and its declaration.
|
||||
expect(decorators('afterImport')).toEqual(['visibleForTesting']);
|
||||
expect(decorators('trailing')).toEqual(['override']);
|
||||
expect(decorators('counter')).toEqual(['riverpod']);
|
||||
expect(decorators('docBelow')).toEqual(['Deprecated']);
|
||||
// Stacked ones still come in reverse source order.
|
||||
expect(decorators('stacked')).toEqual(['c', 'b', 'a']);
|
||||
|
||||
// Members, one with no body and a `const` constructor (both read from
|
||||
// before their `declaration`), and a comment on the member's line.
|
||||
expect(decorators('Counter::dispose')).toEqual(['override']);
|
||||
expect(decorators('Counter::bodiless')).toEqual(['protected']);
|
||||
expect(decorators('Counter::zero')).toEqual(['literal']);
|
||||
expect(decorators('Counter::inline')).toEqual(['x']);
|
||||
expect(decorators('Counter::afterBody')).toEqual(['override']);
|
||||
|
||||
// An annotation before a top-level variable or a field is theirs.
|
||||
expect(decorators('afterVariable')).toEqual([]);
|
||||
expect(decorators('Counter::afterField')).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* A Dart annotation is a constant expression: a reference to a `const`
|
||||
* variable (`@riverpod`, `@override`, `@meta.immutable`, a type's static
|
||||
* constant `@Riverpod.scoped` or enum value) or a call of a `const`
|
||||
* constructor (`@Riverpod(keepAlive: true)`, `@Riverpod.forever()`). It is
|
||||
* never a method, a getter or a function.
|
||||
*
|
||||
* The extractor records only an annotation's last name, and `decorates`
|
||||
* references were ranked like Python decorators, which are functions: a
|
||||
* function or method of that name scored above a class, and a constant scored
|
||||
* nothing. So riverpod's 506 `@riverpod` annotations went to
|
||||
* riverpod_analyzer_utils' extension getter `riverpod` instead of
|
||||
* riverpod_annotation's `const riverpod = Riverpod();`.
|
||||
*
|
||||
* An annotation now means what its library can see, read from how it is
|
||||
* written: a constant or a class (through its import prefix, if it has one),
|
||||
* a constant, enum value or named constructor of the type it is written
|
||||
* through, or, on a member, a static constant of the type around it. A
|
||||
* comment between it and the declaration (riverpod_lint's `@riverpod`
|
||||
* `// expect_lint: …` fixtures) no longer hides it.
|
||||
*
|
||||
* Runs against the native kernel (when built) and the wasm extractor.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const FILES: Record<string, string> = {
|
||||
'packages/riverpod_annotation/pubspec.yaml': 'name: riverpod_annotation\n',
|
||||
'packages/riverpod_annotation/lib/riverpod_annotation.dart': `export 'src/riverpod_annotation.dart';
|
||||
`,
|
||||
'packages/riverpod_annotation/lib/src/riverpod_annotation.dart': `final class Riverpod {
|
||||
const Riverpod({this.keepAlive = false});
|
||||
|
||||
const Riverpod.forever() : keepAlive = true;
|
||||
|
||||
static const scoped = Riverpod();
|
||||
|
||||
final bool keepAlive;
|
||||
}
|
||||
|
||||
const riverpod = Riverpod();
|
||||
|
||||
enum Retry { never, always }
|
||||
`,
|
||||
// A getter and a function sharing annotation names, in a package the app
|
||||
// does not import.
|
||||
'packages/riverpod_analyzer_utils/pubspec.yaml': 'name: riverpod_analyzer_utils\n',
|
||||
'packages/riverpod_analyzer_utils/lib/src/nodes.dart': `part 'nodes/annotation.dart';
|
||||
|
||||
const _ast = Object();
|
||||
|
||||
class AnnotatedNode {}
|
||||
|
||||
bool protected(Object node) => true;
|
||||
`,
|
||||
'packages/riverpod_analyzer_utils/lib/src/nodes/annotation.dart': `part of '../nodes.dart';
|
||||
|
||||
@_ast
|
||||
extension RiverpodAnnotatedAnnotatedNodeOfX on AnnotatedNode {
|
||||
Object? get riverpod => null;
|
||||
}
|
||||
`,
|
||||
'examples/counter/pubspec.yaml': 'name: counter\n',
|
||||
'examples/counter/lib/counter.dart': `import 'package:meta/meta.dart' as meta;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@riverpod
|
||||
int counter(Object ref) => 0;
|
||||
|
||||
@riverpod
|
||||
// expect_lint: functional_ref
|
||||
int linted(Object ref) => 0;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class Todos {
|
||||
static const marker = Riverpod();
|
||||
|
||||
@marker
|
||||
void add() {}
|
||||
|
||||
@meta.protected
|
||||
void reset() {}
|
||||
}
|
||||
|
||||
@Riverpod.forever()
|
||||
int kept(Object ref) => 0;
|
||||
|
||||
@Riverpod.scoped
|
||||
int scopedCounter(Object ref) => 0;
|
||||
|
||||
@Retry.always
|
||||
int retried(Object ref) => 0;
|
||||
|
||||
@meta.immutable
|
||||
class Frozen {}
|
||||
`,
|
||||
// A constant of an annotation's name that counter.dart does not import.
|
||||
'examples/counter/lib/immutable.dart': `const immutable = Object();
|
||||
`,
|
||||
};
|
||||
|
||||
describe('Dart annotations name a constant or a constructor', () => {
|
||||
let root = '';
|
||||
let cg: CodeGraph | undefined;
|
||||
let kernel: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-annotations-'));
|
||||
for (const [rel, content] of Object.entries(FILES)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
kernel = process.env.CODEGRAPH_KERNEL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
|
||||
else process.env.CODEGRAPH_KERNEL = kernel;
|
||||
});
|
||||
|
||||
/** `<decorated> @<refName> -> <target kind> <target file>:<target qualified name>` for each `decorates` edge. */
|
||||
function decorations(graph: CodeGraph): string[] {
|
||||
const files = Object.keys(FILES).filter((f) => f.endsWith('.dart'));
|
||||
const ids = files.flatMap((f) => graph.getNodesInFile(f).map((n) => n.id));
|
||||
return graph.getOutgoingEdgesFrom(ids)
|
||||
.filter((e) => e.kind === 'decorates')
|
||||
.map((e) => {
|
||||
const from = graph.getNode(e.source)!;
|
||||
const to = graph.getNode(e.target)!;
|
||||
return `${from.qualifiedName} @${String(e.metadata?.refName ?? '')} -> ${to.kind} ${to.filePath}:${to.qualifiedName}`;
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
it.each(['default', 'wasm'])('link what the annotation is written as (%s)', async (backend) => {
|
||||
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
|
||||
else delete process.env.CODEGRAPH_KERNEL;
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
|
||||
const annotation = 'packages/riverpod_annotation/lib/src/riverpod_annotation.dart';
|
||||
const app = 'examples/counter/lib/counter.dart';
|
||||
expect(decorations(cg)).toEqual([
|
||||
// A private constant of the library, declared in the file the part belongs to.
|
||||
`RiverpodAnnotatedAnnotatedNodeOfX @_ast -> constant packages/riverpod_analyzer_utils/lib/src/nodes.dart:_ast`,
|
||||
// A constructor call names the class; a named one, its constructor.
|
||||
`Todos @Riverpod -> class ${annotation}:Riverpod`,
|
||||
// On a member, a static constant of the class around it.
|
||||
`Todos::add @marker -> constant ${app}:Todos::marker`,
|
||||
// The constant riverpod_annotation exports — not the analyzer's getter.
|
||||
`counter @riverpod -> constant ${annotation}:riverpod`,
|
||||
`kept @forever -> method ${annotation}:Riverpod::forever`,
|
||||
// A comment between the annotation and the declaration hides nothing.
|
||||
`linted @riverpod -> constant ${annotation}:riverpod`,
|
||||
// An enum value and a static constant, written through their type.
|
||||
`retried @always -> enum_member ${annotation}:Retry::always`,
|
||||
`scopedCounter @scoped -> constant ${annotation}:Riverpod::scoped`,
|
||||
// `@meta.protected` and `@meta.immutable` are package:meta's: not the
|
||||
// project's function `protected`, nor a constant counter.dart cannot see.
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
// An annotation belongs to the declaration after it, whatever comments come
|
||||
// between them; the decorator scan steps over `//`, `/* */`, `///` and
|
||||
// `/** */` comments. Anything else before the annotations still ends it.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
// A comment after the import.
|
||||
@visibleForTesting
|
||||
// And one below the annotation.
|
||||
void afterImport() {}
|
||||
|
||||
@x // A comment on the annotation's line.
|
||||
void trailing() {}
|
||||
|
||||
@override
|
||||
// ignore: must_call_super
|
||||
void lineBelow() {}
|
||||
|
||||
@x
|
||||
/// Doc below an annotation.
|
||||
void docBelow() {}
|
||||
|
||||
@x
|
||||
/** Block doc below an annotation. */
|
||||
void blockDocBelow() {}
|
||||
|
||||
@x
|
||||
/* A block comment. */
|
||||
void blockBelow() {}
|
||||
|
||||
@a
|
||||
// one
|
||||
@b
|
||||
/// two
|
||||
/* three */
|
||||
@c
|
||||
// four
|
||||
void interleaved() {}
|
||||
|
||||
@riverpod
|
||||
// expect_lint: functional_ref
|
||||
int counter(Ref ref) => 0;
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
|
||||
void blankLines() {}
|
||||
|
||||
@pragma('vm:external-name', 'f')
|
||||
// A comment.
|
||||
external int extTop();
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
int get topGetter => 1;
|
||||
|
||||
@x
|
||||
final answer = 42;
|
||||
// A comment.
|
||||
void afterVariable() {}
|
||||
|
||||
void before() {} // A trailing comment.
|
||||
@x
|
||||
void afterBody() {}
|
||||
|
||||
class C {
|
||||
@override
|
||||
// ignore: must_call_super
|
||||
void method() {}
|
||||
|
||||
@x /* inline */ void inline() {}
|
||||
|
||||
@override
|
||||
// A comment.
|
||||
void bodiless();
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
C.named() {}
|
||||
|
||||
@literal
|
||||
// A comment.
|
||||
const C.c();
|
||||
|
||||
@x
|
||||
/// Doc below.
|
||||
factory C.make() => C._();
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
const factory C.r() = C.c;
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
static void s() {}
|
||||
|
||||
@override
|
||||
// A comment.
|
||||
bool operator ==(Object other) => true;
|
||||
|
||||
@override
|
||||
// A comment.
|
||||
int get value => 1;
|
||||
|
||||
@override
|
||||
// A comment.
|
||||
set value(int v) {}
|
||||
|
||||
@x
|
||||
// A comment.
|
||||
external void ext();
|
||||
|
||||
/// Doc above.
|
||||
@a
|
||||
// between
|
||||
@b
|
||||
void docAbove() {}
|
||||
|
||||
@JsonKey(name: 'f')
|
||||
final int f = 0;
|
||||
// A comment after a field.
|
||||
@override
|
||||
void afterField() {}
|
||||
|
||||
void prior() {} // A trailing comment.
|
||||
@override
|
||||
void afterMember() {}
|
||||
|
||||
C._();
|
||||
}
|
||||
|
||||
@immutable
|
||||
// ignore: something
|
||||
class D {}
|
||||
|
||||
@internal
|
||||
// A comment.
|
||||
mixin M on C {
|
||||
@mustCallSuper
|
||||
// A comment.
|
||||
void m() {}
|
||||
}
|
||||
|
||||
@internal
|
||||
// A comment.
|
||||
extension X on C {
|
||||
@pragma('vm:prefer-inline')
|
||||
// A comment.
|
||||
void e() {}
|
||||
}
|
||||
|
||||
@internal
|
||||
/// Doc below.
|
||||
extension type Meters(double value) {
|
||||
@redeclare
|
||||
// A comment.
|
||||
double km() => value / 1000;
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
@JsonValue('on')
|
||||
// A comment.
|
||||
on,
|
||||
off;
|
||||
|
||||
@override
|
||||
// A comment.
|
||||
String toString() => name;
|
||||
}
|
||||
|
||||
void outer() {
|
||||
@pragma('vm:prefer-inline')
|
||||
// A comment.
|
||||
void local() {}
|
||||
local();
|
||||
}
|
||||
@@ -33,9 +33,10 @@
|
||||
* `declaration`-wrapped member with no body, read from before the wrapper;
|
||||
* TortureCommentChains.dart: comments between a member chain's parts skipped
|
||||
* by every sibling step; TortureAnnotatedDocs.dart: the dartdoc above a
|
||||
* member's annotations, read past them, and what still ends the walk) and
|
||||
* their CRLF variants (derived in-memory — #1329), plus defer and
|
||||
* generated-file pins.
|
||||
* member's annotations, read past them, and what still ends the walk;
|
||||
* TortureAnnotatedComments.dart: the comments between annotations and their
|
||||
* declaration, stepped over by the decorator scan) and their CRLF variants
|
||||
* (derived in-memory — #1329), plus defer and generated-file pins.
|
||||
*
|
||||
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
||||
* with --max-deferral 0.3); this suite keeps the invariant alive in
|
||||
@@ -153,6 +154,11 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
|
||||
// import or the previous member's body still ends the walk. Class-like
|
||||
// declarations open with their annotations.
|
||||
['TortureAnnotatedDocs.dart', 40],
|
||||
// Comments of every form between annotations and the declaration they
|
||||
// belong to — stacked, on the annotation's line, before members with no
|
||||
// body — are stepped over by the decorator scan; a field, a variable, an
|
||||
// import or the previous member's body still ends it.
|
||||
['TortureAnnotatedComments.dart', 30],
|
||||
] as const;
|
||||
|
||||
for (const [file, minNodes] of FIXTURES) {
|
||||
|
||||
@@ -51,6 +51,12 @@ const MAX_VALUE_REF_NODES: usize = 20_000;
|
||||
/// `Widget build(…)`), so the docstring walk steps over them.
|
||||
const DOCSTRING_STEP_OVER: &[&str] = &["annotation"];
|
||||
|
||||
/// decoratorStepOverTypes (dart.ts) — an annotation belongs to the next
|
||||
/// declaration, whatever comments come between them (`@override`
|
||||
/// `// ignore: must_call_super` `void f()`), so the decorator scan steps over
|
||||
/// them.
|
||||
const DECORATOR_STEP_OVER: &[&str] = &["comment", "documentation_comment"];
|
||||
|
||||
/// NAME_STOPLIST (function-ref.ts).
|
||||
fn is_stoplisted(name: &str) -> bool {
|
||||
matches!(
|
||||
@@ -1457,7 +1463,8 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
// Scan 2: preceding siblings, backward, stop at the first
|
||||
// non-annotation — stacked annotations emit in REVERSE source order.
|
||||
// A `declaration`-wrapped member's annotations precede the wrapper.
|
||||
// A `declaration`-wrapped member's annotations precede the wrapper,
|
||||
// and the comments between annotations and declaration are stepped over.
|
||||
let anchor = self.declaration_wrapper(decl).unwrap_or(decl);
|
||||
if let Some(parent) = anchor.parent() {
|
||||
let decl_start = anchor.start_byte();
|
||||
@@ -1473,6 +1480,9 @@ impl<'t> Walker<'t> {
|
||||
if let Some(di) = decl_idx {
|
||||
for j in (0..di).rev() {
|
||||
let Some(sib) = parent.named_child(j) else { continue };
|
||||
if DECORATOR_STEP_OVER.contains(&sib.kind()) {
|
||||
continue;
|
||||
}
|
||||
if !matches!(sib.kind(), "decorator" | "annotation" | "marker_annotation") {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -871,12 +871,16 @@ enum emits nothing). Mechanics for dart:
|
||||
decorates `immutable`). Enums and typedefs are built the same way, but
|
||||
nothing scans them.
|
||||
- Scan #2 (preceding siblings :5002-5023): walk BACKWARD from the
|
||||
declaration; `annotation` is in the accepted set (:5017); stop at the
|
||||
first non-annotation sibling. consider(): target = first namedChild of
|
||||
accepted types → the `identifier` (`override`, `deprecated`, `pragma`,
|
||||
`immutable`, `Deprecated`) → `<`-strip + last-`.`-segment (`@ui.Widget`
|
||||
style would strip to `Widget`) → **`decorates` ref {from the decorated
|
||||
node, name, line/col of the ANNOTATION node}**. With-args annotations
|
||||
declaration; `annotation` is in the accepted set (:5017); step over
|
||||
`comment` / `documentation_comment` siblings (since 2026-10-06, below);
|
||||
stop at the first other non-annotation sibling. consider(): target =
|
||||
first namedChild of accepted types → the `identifier` (`override`,
|
||||
`deprecated`, `pragma`, `immutable`, `Deprecated`) or `scoped_identifier`
|
||||
(`meta.immutable`, `Foo.named`, `p.Foo.named`) → `<`-strip + last-`.`-segment
|
||||
(`@meta.immutable` → `immutable`, `@p.Foo.named(…)` → `named`; the strip
|
||||
runs first, so `@Foo<int>.named(…)`, which parses with an ERROR inside the
|
||||
name, → `Foo`) → **`decorates` ref {from the decorated node, name,
|
||||
line/col of the ANNOTATION node, i.e. its `@`}**. With-args annotations
|
||||
emit their NAME; the argument expressions are never visited (no refs from
|
||||
`@Deprecated('use other')`'s string).
|
||||
- **Stacked annotations emit in REVERSE source order** (the backward walk):
|
||||
@@ -885,22 +889,34 @@ enum emits nothing). Mechanics for dart:
|
||||
- For a method: the previous member's function_body (or any declaration)
|
||||
breaks the chain correctly. (The docstring walk steps OVER annotations
|
||||
since 2026-10-06 — §Docstrings.)
|
||||
- **A comment between an annotation and the declaration ends the scan**, so
|
||||
the annotation above it attaches to nothing: `@override` `// ignore:
|
||||
must_call_super` `void f()`, `@x` `/// Doc below.` `void f()` (pinned in
|
||||
TortureAnnotatedDocs.dart). Measured 2026-10-06: bloc @b9be1e2 hides 7
|
||||
annotations this way (all `@override`), riverpod @4ba1be2 55 on 49
|
||||
declarations (`@override` 33, `@riverpod` 16 in the lint fixtures,
|
||||
`@internal` 6). Left as is ON PURPOSE: stepping over comments here is
|
||||
right for Dart (an annotation always belongs to the next declaration), but
|
||||
Dart nodes store no decorator list, so the only effect would be edges, and
|
||||
`override`/`internal` resolve to nothing, while `decorates` scoring prefers
|
||||
function/method candidates (name-matcher.ts), so `@riverpod` lands on the
|
||||
analyzer getter `RiverpodAnnotatedAnnotatedNodeOfX::riverpod` (0.4)
|
||||
instead of `const riverpod = Riverpod()`. That is already true of
|
||||
riverpod's 506 existing `@riverpod` edges. Fix that ranking for Dart (a
|
||||
const or a class, never a method) first, then add `comment` /
|
||||
`documentation_comment` to scan #2's step-over in both arms.
|
||||
- **Comments between the annotations and the declaration are stepped over**
|
||||
(since 2026-10-06). An annotation always belongs to the next declaration,
|
||||
so dart.ts sets `decoratorStepOverTypes: ['comment',
|
||||
'documentation_comment']`, which scan #2 skips, and the kernel's
|
||||
`extract_decorators_for` skips the same `DECORATOR_STEP_OVER` list. No
|
||||
other language sets one. Until then the comment ended the scan and the
|
||||
annotation above it attached to nothing. Now these all attach: `@override`
|
||||
`// ignore: must_call_super` `void f()`, `@x` `/// Doc below.` `void f()`,
|
||||
`@x /* c */ void f()`, a comment on the annotation's own line, and comments
|
||||
between stacked annotations (still in reverse order). Pinned in
|
||||
TortureAnnotatedComments.dart and dart-annotation-comments.test.ts.
|
||||
- Still ends the scan, comments or not: a field's `declaration`, a
|
||||
top-level variable, an import, the previous member's body (`void a() {}
|
||||
// note` `@x void b()` gives `b` only `x`), a class-like declaration.
|
||||
- Class-likes are unaffected. Their leading annotations are scan #1's,
|
||||
which reads every child, comments among them included.
|
||||
- Validation (bloc @b9be1e2 / riverpod @4ba1be2, kernel loaded, on top of
|
||||
the annotation resolution below): `decorates` refs +7 / +54, edges +0 /
|
||||
+16, nothing else in the dumps changed. bloc's 7 are all `@override`.
|
||||
riverpod's 54 (on 48 declarations) are `@override` 32, `@internal` 6 and
|
||||
`@riverpod` 16 (lint fixtures and one website snippet), and each
|
||||
`@riverpod` links `const riverpod`. Kernel and wasm full-index dumps are
|
||||
byte-identical, and the parity sweeps show 0 diffs.
|
||||
- The AST scan's 55th riverpod site is `@override` `// ignore: …` on a
|
||||
FIELD, which no extractor records. A scan that also counts annotation →
|
||||
comment runs INSIDE class-like nodes finds riverpod's 24 `@riverpod`
|
||||
`// ignore: …` `class X` lint fixtures and bloc's `@JsonEnum(…)` `/// …`
|
||||
`enum`. Scan #1 already reads those (an enum's, never: see above).
|
||||
- Members with no body (declaration-wrapped ctors, const ctors, redirecting
|
||||
factories, abstract and `external` members): the signature's PARENT is the
|
||||
`declaration`, so until 2026-10-06 the backward scan ran over the
|
||||
@@ -914,6 +930,41 @@ enum emits nothing). Mechanics for dart:
|
||||
refs, all SDK or package:meta names (`useResult`, `override`, `internal`,
|
||||
`visibleForTesting`, …), so no edges; before #2380, riverpod's 23 new
|
||||
`@internal` refs on `X.internal(…)` ctors resolved to the ctor itself.
|
||||
- **Resolution is TS-side and Dart-specific** (since 2026-10-06):
|
||||
resolveOneInner hands every Dart `decorates` ref to name-matcher.ts's
|
||||
`matchDartAnnotation` before any other strategy, and nothing falls
|
||||
through. A Dart annotation is a const variable or a const constructor
|
||||
call, never a method, getter or function, so the shared decorator ranking
|
||||
(findBestMatch: +25 function/method, +15 class, nothing for a constant)
|
||||
never sees one. That ranking sent riverpod's 506 `@riverpod` to the
|
||||
analyzer's extension getter `RiverpodAnnotatedAnnotatedNodeOfX::riverpod`
|
||||
(0.4) instead of `const riverpod = Riverpod();`.
|
||||
- The matcher re-reads the written form from the `@` in comment-blanked
|
||||
source. It is a call only when `(` follows on the same line, so a record
|
||||
return type on the next line is not taken for arguments.
|
||||
- `@x`: on a member, the enclosing type's own static `constant` /
|
||||
`enum_member` `x` (a class-like's own annotations stand outside its
|
||||
body); else a top-level `constant` the library can see (#2386's model,
|
||||
the own library shadowing imports). `@X(…)`: a visible class `X`, not
|
||||
an extension.
|
||||
- `@p.x` / `@p.X(…)`, when `p` is an import prefix (dart-libraries.ts
|
||||
`isDartImportPrefix`, the imports of the file's whole library): the same
|
||||
through that prefix. Otherwise `@T.x` → T's own `constant` /
|
||||
`enum_member` `x`, and `@T.named(…)` → T's named constructor (#2380's
|
||||
`isDartConstructor`); `@p.T.x` / `@p.T.named(…)` likewise through `p`.
|
||||
- A sole candidate links at 0.9 (`exact-match`, or `qualified-name`
|
||||
through a type). Several (only where the library model can't establish
|
||||
the library) fall back to findBestMatch's proximity at 0.7 / 0.4.
|
||||
- Not modelled: a `final` is not told apart from a `const` (compiling code
|
||||
never annotates with a visible `final`), nor a typedef used to call a
|
||||
constructor.
|
||||
- Validation (main 99246848, kernel loaded): bloc 0 edges changed (no
|
||||
annotation there names a project declaration); riverpod exactly the 506
|
||||
`@riverpod` edges retargeted to `const riverpod` at 0.9, every other
|
||||
`decorates` edge (`$internal`, `Riverpod`, `Dependencies`, …) identical,
|
||||
nodes, refs and files identical. Every extracted annotation in both
|
||||
repos is written with one name, so the prefixed and `T.x` forms are
|
||||
pinned by dart-annotation-targets.test.ts only.
|
||||
|
||||
### Docstrings (tree-sitter-helpers.ts:95-127) — dartdoc is KEPT, both forms
|
||||
|
||||
|
||||
@@ -539,6 +539,10 @@ export const dartExtractor: LanguageExtractor = {
|
||||
// this: its annotations open its own node, and the dartdoc above them is
|
||||
// that node's previous sibling.
|
||||
docstringStepOverTypes: ['annotation'],
|
||||
// An annotation belongs to the next declaration, whatever comments come
|
||||
// between them: `@override` `// ignore: must_call_super` `void f()`, or a
|
||||
// dartdoc written below the annotations. The decorator scan steps over them.
|
||||
decoratorStepOverTypes: ['comment', 'documentation_comment'],
|
||||
resolveBody: (node, bodyField) => {
|
||||
// Dart: function_body is a next sibling of function_signature/method_signature
|
||||
if (node.type === 'function_signature' || node.type === 'method_signature') {
|
||||
|
||||
@@ -181,6 +181,13 @@ export interface LanguageExtractor {
|
||||
* each one is stepped over, and comments on either side of it still join.
|
||||
*/
|
||||
docstringStepOverTypes?: string[];
|
||||
/**
|
||||
* Node types that may stand between a declaration and the decorators written
|
||||
* before it without ending the scan for them. Dart writes comments there
|
||||
* (`@override` `// ignore: must_call_super` `void f()`), and an annotation
|
||||
* always belongs to the declaration that follows it.
|
||||
*/
|
||||
decoratorStepOverTypes?: string[];
|
||||
/** Extract visibility from node */
|
||||
getVisibility?: (node: SyntaxNode) => 'public' | 'private' | 'protected' | 'internal' | undefined;
|
||||
/** Check if node is exported */
|
||||
|
||||
@@ -6213,8 +6213,10 @@ export class TreeSitterExtractor {
|
||||
//
|
||||
// A grammar that wraps the declaration (Dart's `declaration`
|
||||
// around a member with no body) puts the annotations before the
|
||||
// wrapper, so the scan starts there.
|
||||
// wrapper, so the scan starts there. One that lets comments sit
|
||||
// between the annotations and the declaration (Dart) steps over them.
|
||||
const anchor = this.extractor?.getDeclarationWrapper?.(declNode) ?? declNode;
|
||||
const stepOver = this.extractor?.decoratorStepOverTypes;
|
||||
const parent = anchor.parent;
|
||||
if (parent) {
|
||||
const declStart = anchor.startIndex;
|
||||
@@ -6230,6 +6232,7 @@ export class TreeSitterExtractor {
|
||||
for (let j = declIdx - 1; j >= 0; j--) {
|
||||
const sibling = parent.namedChild(j);
|
||||
if (!sibling) continue;
|
||||
if (stepOver?.includes(sibling.type)) continue;
|
||||
if (sibling.type !== 'decorator' && sibling.type !== 'annotation' && sibling.type !== 'marker_annotation') {
|
||||
break; // non-decorator separator → stop consuming
|
||||
}
|
||||
|
||||
@@ -769,3 +769,14 @@ export function inSameDartLibrary(fromFile: string, declFile: string, context: R
|
||||
const scope = scopeOf(fromFile, context, memoFor(context));
|
||||
return scope !== null && scope.own.has(declFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `name` is an import prefix where `fromFile` is written: an import
|
||||
* of its library is `as name` (`p` in `p.Foo`, not the `Foo` of `Foo.bar`).
|
||||
* Where the library is unknown, the file's own imports say.
|
||||
*/
|
||||
export function isDartImportPrefix(fromFile: string, name: string, context: ResolutionContext): boolean {
|
||||
const memo = memoFor(context);
|
||||
const importers = scopeOf(fromFile, context, memo)?.importers ?? [fromFile];
|
||||
return importers.some((file) => (directivesOf(file, context, memo)?.imports ?? []).some((d) => d.prefix === name));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} from './types';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isStdMethodName } from './name-matcher';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
@@ -1149,6 +1149,9 @@ export class ReferenceResolver {
|
||||
// `events.map(f).transform(…)`), which arrives by its bare name: a member
|
||||
// of what the chain's head and links are declared to be, or nothing (#750).
|
||||
if (isDartChainLink(ref, this.context)) return matchDartChainLink(ref, this.context);
|
||||
// A Dart annotation (`@riverpod`, `@Riverpod(…)`) is a constant or a
|
||||
// constructor call, as written — never a method or function by its name.
|
||||
if (isDartAnnotation(ref)) return matchDartAnnotation(ref, this.context);
|
||||
|
||||
// Skip built-in/external references
|
||||
if (this.isBuiltInOrExternal(ref)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSuperty
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
|
||||
import { dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries';
|
||||
import { dartLibrarySees, dartPrefixSees, inSameDartLibrary, isDartImportPrefix } from './dart-libraries';
|
||||
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
|
||||
import { isTestPath } from '../search/query-utils';
|
||||
import { isMinifiedContent } from '../extraction/generated-detection';
|
||||
@@ -4049,6 +4049,130 @@ export function matchDartMemberRead(ref: UnresolvedRef, context: ResolutionConte
|
||||
return { original: ref, targetNodeId: found.node.id, confidence: 0.9, resolvedBy: 'instance-method', edgeKind: 'calls' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a ref is a Dart annotation — `@riverpod`, `@Riverpod(keepAlive:
|
||||
* true)`, `@meta.immutable` — which the extractor records by its last name, at
|
||||
* the `@`.
|
||||
*/
|
||||
export function isDartAnnotation(ref: UnresolvedRef): boolean {
|
||||
return ref.language === 'dart' && ref.referenceKind === 'decorates';
|
||||
}
|
||||
|
||||
/**
|
||||
* A Dart annotation is a constant expression: a `const` variable, or a call of
|
||||
* a `const` constructor. So it links, as it is written, a constant or (called)
|
||||
* a class its library can see, through the import prefix it is written with;
|
||||
* a static constant, an enum value or (called) a named constructor of the
|
||||
* type it is written through (`@Foo.value`, `@Foo.named(…)`); or, on a
|
||||
* member, a static constant of the type around it. Never a method, a
|
||||
* getter or a function: riverpod's 506 `@riverpod` annotations went to
|
||||
* riverpod_analyzer_utils' extension getter `riverpod`, not riverpod_annotation's
|
||||
* `const riverpod = Riverpod();`.
|
||||
*/
|
||||
export function matchDartAnnotation(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
const name = ref.referenceName;
|
||||
if (!context.getNodesByName(name).some((n) => n.language === 'dart')) return null;
|
||||
const written = dartAnnotationAt(ref, context);
|
||||
if (!written) return null;
|
||||
const { names, call } = written;
|
||||
let pool: Node[];
|
||||
let resolvedBy: ResolvedRef['resolvedBy'] = 'exact-match';
|
||||
if (names.length === 1 || (names.length === 2 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
|
||||
const prefix = names.length === 2 ? names[0]! : null;
|
||||
const own = prefix === null && !call ? dartEnclosingStatic(name, ref, context) : null;
|
||||
if (own) return { original: ref, targetNodeId: own.id, confidence: 0.9, resolvedBy };
|
||||
pool = context.getNodesByName(name).filter((n) => isDartLibraryDecl(n) &&
|
||||
(call ? isDartAnnotationClass(n, context) : n.kind === 'constant') && isDartTopLevelVisible(n, ref, context, prefix));
|
||||
if (prefix === null) pool = preferOwnDartLibrary(pool, ref, context);
|
||||
} else if (names.length === 2 || (names.length === 3 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
|
||||
const prefix = names.length === 3 ? names[0]! : null;
|
||||
let owners = context.getNodesByName(names[names.length - 2]!).filter((n) =>
|
||||
isDartLibraryDecl(n) && DART_TYPE_KINDS.has(n.kind) && isDartTopLevelVisible(n, ref, context, prefix));
|
||||
if (prefix === null) owners = preferOwnDartLibrary(owners, ref, context);
|
||||
pool = context.getNodesByName(name).filter((m) => m.language === 'dart' &&
|
||||
owners.some((o) => o.filePath === m.filePath && m.qualifiedName === `${o.qualifiedName}::${name}`) &&
|
||||
(call ? isDartConstructor(m, context) : m.kind === 'constant' || m.kind === 'enum_member'));
|
||||
resolvedBy = 'qualified-name';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (pool.length <= 1) return pool[0] ? { original: ref, targetNodeId: pool[0].id, confidence: 0.9, resolvedBy } : null;
|
||||
// Libraries this cannot establish see every file's declarations.
|
||||
const best = findBestMatch(ref, pool, context);
|
||||
return best
|
||||
? { original: ref, targetNodeId: best.id, confidence: computePathProximity(ref.filePath, best.filePath) >= 30 ? 0.7 : 0.4, resolvedBy }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** A class a Dart annotation can call a constructor of: not an extension, which has none. */
|
||||
function isDartAnnotationClass(n: Node, context: ResolutionContext): boolean {
|
||||
return n.kind === 'class' && dartExtensionDecl(n, context) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The static constant or enum value `name` of the type a Dart annotation is
|
||||
* written in, which a member's annotation names bare. A class's own
|
||||
* annotations stand outside its body.
|
||||
*/
|
||||
function dartEnclosingStatic(name: string, ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
||||
let own: Node | null = null;
|
||||
for (const n of context.getNodesInFile(ref.filePath)) {
|
||||
if (DART_TYPE_KINDS.has(n.kind) && n.id !== ref.fromNodeId && n.startLine <= ref.line && n.endLine >= ref.line &&
|
||||
(!own || n.startLine >= own.startLine)) own = n;
|
||||
}
|
||||
if (!own) return null;
|
||||
const qualifiedName = `${own.qualifiedName}::${name}`;
|
||||
return context.getNodesByName(name).find((m) => m.language === 'dart' && m.filePath === ref.filePath &&
|
||||
m.qualifiedName === qualifiedName && (m.kind === 'constant' || m.kind === 'enum_member')) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a Dart annotation is written — the names of `@p.Foo.named(…)` up to the
|
||||
* one its ref records, and whether it is called — read from its `@`. The
|
||||
* extractor records the last name, or the one a type argument list follows
|
||||
* (`@Foo<int>.named(…)` is `Foo`'s).
|
||||
*/
|
||||
function dartAnnotationAt(ref: UnresolvedRef, context: ResolutionContext): { names: string[]; call: boolean } | null {
|
||||
const code = dartCodeOf(ref.filePath, context);
|
||||
const from = code?.lineStarts[ref.line - 1];
|
||||
if (!code || from === undefined) return null;
|
||||
const to = code.lineStarts[ref.line] ?? code.text.length;
|
||||
// On a line with non-ASCII text before it, the `@` whose names fit stands in.
|
||||
const ats = [from + ref.column];
|
||||
for (let k = code.text.indexOf('@', from); k >= 0 && k < to; k = code.text.indexOf('@', k + 1)) {
|
||||
if (k !== from + ref.column) ats.push(k);
|
||||
}
|
||||
for (const at of ats) {
|
||||
const read = code.text[at] === '@' ? readDartAnnotation(code.text, at + 1) : null;
|
||||
const end = read ? (read.typeArgsAt >= 0 ? read.typeArgsAt : read.names.length - 1) : -1;
|
||||
if (read && read.names[end] === ref.referenceName) return { names: read.names.slice(0, end + 1), call: read.call };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A Dart annotation's dotted names, which of them a type argument list follows, and whether `(` comes next on its line. */
|
||||
function readDartAnnotation(text: string, i: number): { names: string[]; typeArgsAt: number; call: boolean } | null {
|
||||
const names: string[] = [];
|
||||
let typeArgsAt = -1;
|
||||
const identifier = /[ \t]*([A-Za-z_$][\w$]*)[ \t]*/y;
|
||||
while (names.length < 8) {
|
||||
identifier.lastIndex = i;
|
||||
const m = identifier.exec(text);
|
||||
if (!m) return null;
|
||||
names.push(m[1]!);
|
||||
i = identifier.lastIndex;
|
||||
if (text[i] === '<' && typeArgsAt < 0) {
|
||||
const close = dartTypeArgumentsEnd(text, i);
|
||||
if (close < 0) return null;
|
||||
typeArgsAt = names.length - 1;
|
||||
for (i = close + 1; text[i] === ' ' || text[i] === '\t'; i++);
|
||||
}
|
||||
if (text[i] !== '.') break;
|
||||
i++;
|
||||
}
|
||||
return { names, typeArgsAt, call: text[i] === '(' };
|
||||
}
|
||||
|
||||
/**
|
||||
* A member an extension adds to `typeName` — `s.shout()` on an enum `Shape`
|
||||
* through `extension ShapeInfo on Shape` — when the type itself declares none
|
||||
@@ -11530,7 +11654,8 @@ function findBestMatch(
|
||||
|
||||
// For decorator references (`@Foo`), prefer functions. Class
|
||||
// decorators (Python `@SomeClass`, Java annotation interfaces)
|
||||
// also resolve here, hence the smaller class bonus.
|
||||
// also resolve here, hence the smaller class bonus. (A Dart annotation,
|
||||
// a constant or a constructor call, is matchDartAnnotation's.)
|
||||
if (ref.referenceKind === 'decorates') {
|
||||
if (candidate.kind === 'function' || candidate.kind === 'method') {
|
||||
score += 25;
|
||||
|
||||
Reference in New Issue
Block a user