mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
d2fb323e51
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>
168 lines
6.2 KiB
TypeScript
168 lines
6.2 KiB
TypeScript
/**
|
|
* 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.
|
|
]);
|
|
});
|
|
});
|