fix(dart): a typedef references the types it aliases (#2763)

In

  class Report { final int score; const Report(this.score); }
  typedef ReportCallback = void Function(Report r);

`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.

Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.

Validation, kernel path, arms built from dbb6aa9b and from this change,
each with its own kernel:
- felangel/bloc b9be1e25: 17,875 -> 17,884 edges (+9, 0 removed).
- rrousselGit/riverpod 4ded4f01: 84,726 -> 84,761 (+35, 0 removed).
- jonataslaw/getx e3c8fa04: 8,436 -> 8,456 (+20, 0 removed).
- alibaba/flutter-go a41b2124 (9 legacy typedefs, 6 with a return
  type): 7,994 -> 7,968; 39 wrong links removed (34 `Widget` and 5
  `Future` uses that had gone to typedefs misnamed `Widget` and
  `Future`), 13 added (4 from typedefs, 9 fields typed by the renamed
  ones).
- peng8350/flutter_pulltorefresh 0be096c1 (8 legacy, 5 with a return
  type): 3,186 -> 2,976; 221 wrong `Widget` links removed, 11 added.
All 88 added non-contains edges were checked against the source; every
removed edge pointed at a typedef misnamed after its return type. The
other new typedef refs name SDK or Flutter types and stay unresolved.
gin and zustand dumps are byte-identical across the arms, kernel and
wasm dumps are byte-identical on bloc, riverpod and pulltorefresh, and
the parity sweep over the five Dart repos is 2,330/2,633 files at
parity, 0 diffs, 303 deferred on both arms. Index time is unchanged
within noise (median of 5 interleaved runs: riverpod 12.6 s vs 11.9 s,
bloc 5.0 s vs 5.0 s, getx 2.8 s vs 2.8 s), and the DB grows by 8 to
24 KB (pulltorefresh shrinks by 64 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 20:46:20 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent dbb6aa9ba9
commit bf6f1fa67e
8 changed files with 588 additions and 10 deletions
+1
View File
@@ -42,6 +42,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In COBOL, asking `codegraph_explore` (or `codegraph explore` and `codegraph context`) about a copybook by name, such as `CVACT01Y` or a member brought in with `EXEC SQL INCLUDE`, now answers with the copybook's own source when it is indexed and every `COPY` or `EXEC SQL INCLUDE` statement that includes it, and says so when the copybook's source isn't part of the project. Before, it answered "No relevant code found", or showed an unrelated program whose name was a letter or two away. Thanks @popolusiak for the report. (#2342)
- In Dart, reading a getter such as `box.area` or `status.label` now counts as calling it, so `codegraph callers` and impact list the code that reads it — whether the getter is declared on the class, inherited, or added by an extension — and a method an extension adds to an enum, like `shape.shout()`, now links as well. A plain field, a getter of a type from outside your project, or a value whose type isn't written down still links nothing. Re-index Dart projects after upgrading. Thanks @brookly255-lv for the report. (#2338)
- In Dart, a type is now linked from more of the places it is written, not only from parameters and return types: the type an `extension … on` targets, a field's type, a top-level variable's type, generic arguments such as `Future<Report?>.value(null)` or Riverpod's `final reportProvider = Family<Report?, String>()`, and a local variable's type, a cast or a type check inside a function. So `callers` and impact for a model class now reach the extensions, models and providers that use it. Thanks @mg-mg-mg for the report. (#2327)
- In Dart, a `typedef` now links to the types it is built from, like `Report` in `typedef ReportCallback = void Function(Report r);` or `typedef Reports = List<Report>;`, so `callers` and impact for a model class also reach the callback and collection types defined on it. An old-style typedef with a return type, like `typedef Widget ItemBuilder(BuildContext context);`, is now indexed under its own name instead of its return type's, so code that only uses `Widget` no longer links to it. Thanks @mg-mg-mg for the report. (#2327)
- In Go, calls through a method receiver or parameter of an unexported type, the usual shape of gRPC and HTTP handlers (`s.service.AddItem()` inside `func (s *server) Create()`), now resolve, and always within that type's own package: another package's `server` with a same-named method no longer takes the call, and a method the type gets from a struct or interface it embeds is found too. Re-index Go projects after upgrading. Thanks @GoDiao for the report and the fix. (#2323)
- In Go, a call made through a type assertion, like `srv.(KVServer).Range(ctx, in)` in generated gRPC code, `c.Reader.(*pipe).Close()` or `v.(storage.Store).Fetch(key)`, now links to that method of the asserted type, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, it linked to whichever method shared the name, so every gRPC handler pointed at the `Unimplemented…Server` stub instead of the server interface. Re-index Go projects after upgrading.
- In Go, a call through a variable that holds the result of a type assertion, like `st.Fetch(key)` after `st := v.(storage.Store)` or `rc.Read(p)` after `rc, ok := v.(ReadCloser)`, now links to that method of the asserted type, as a call made through the assertion itself does, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, such a call linked to whichever method shared the name or to nothing, and a variable named like a standard-library package, like grpc-go's `parser, ok := builder.(balancer.ConfigParser)`, was taken for that package. Re-index Go projects after upgrading.
+312
View File
@@ -0,0 +1,312 @@
/**
* A Dart typedef references the types it aliases (#2327).
*
* In
*
* class Report { final int score; const Report(this.score); }
* typedef ReportCallback = void Function(Report r);
*
* `callers Report` did not list `ReportCallback`. Both extractors minted the
* typedef's node and nothing else: the grammar gives `type_alias` no fields,
* so the aliased type was never read, and impact on a model type missed every
* callback type built on it. The legacy form with a return type, `typedef
* Report LegacyMaker(int n);`, was named after that return type, the first
* type name in it, so a `type_alias Report` stood beside the class.
*
* A typedef now references every type its aliased type names, from the
* typedef, on each type's name: a function type's return and parameter types,
* generic arguments, record fields, a type parameter's bound, and the legacy
* form's return and parameter types. A type parameter, the typedef's own or a
* generic function type's, means that parameter and never a project type of
* its name; an import prefix names no type; and the legacy form is named by
* its own name.
*
* 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 os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult, Node } 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';
/** Every typedef shape: new style and legacy, generic, prefixed, annotated, wrapped. */
const SOURCE = `import 'model.dart' as m;
class Report {
final int score;
const Report(this.score);
}
typedef ReportCallback = void Function(Report r);
typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef ReportsByName = Map<String, Report>;
typedef Results = Future<List<Report>?> Function(int page);
typedef void LegacyCallback(Report r);
typedef Report LegacyMaker(int n);
typedef List<Report> LegacyLoader([Report? seed]);
typedef m.Report PrefixedLegacy(m.Report r);
typedef Mapper<T> = T Function(Report r);
typedef Picker = T Function<T>(T a, Report r);
typedef T LegacyPicker<T>(T a, Report r);
typedef Bounded<X extends Report> = List<X>;
typedef Sdk = Future<String>;
typedef Dynamic = dynamic Function(int n);
typedef Prefixed = m.Report;
typedef Named = void Function({required Report report, int? n});
typedef Pair = (Report, {m.Report other});
typedef Nested = void Function(Report Function(int) make);
/// Documented.
@Deprecated('Use ReportCallback')
typedef OldCallback = void Function(Report r);
typedef Formatted =
Map<String, Report> Function(
Report first,
Report second,
);
`;
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
describe('a Dart typedef references the types it aliases (#2327)', () => {
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/typedefs.dart', source, 'dart');
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract('lib/typedefs.dart', source, 'dart');
expect(result, 'kernel extraction').not.toBeNull();
return result!;
}
/** `<typedef> <type>` for every `references` ref out of a typedef, sorted. */
function typedefRefs(result: ExtractionResult): string[] {
const byId = new Map(result.nodes.map((n) => [n.id, n]));
return result.unresolvedReferences
.filter((r) => r.referenceKind === 'references' && byId.get(r.fromNodeId)?.kind === 'type_alias')
.map((r) => `${byId.get(r.fromNodeId)!.name} ${r.referenceName}`)
.sort();
}
const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const);
for (const crlf of [false, true]) {
const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s);
const label = crlf ? ' (CRLF)' : '';
it.each(backends)(`a legacy typedef is named by its own name, not its return type: %s${label}`, (backend) => {
const result = extract(backend, eol(SOURCE));
expect(result.nodes.filter((n) => n.kind === 'type_alias').map((n) => n.name)).toEqual([
'ReportCallback',
'ReportMaker',
'Reports',
'ReportsByName',
'Results',
'LegacyCallback',
'LegacyMaker',
'LegacyLoader',
'PrefixedLegacy',
'Mapper',
'Picker',
'LegacyPicker',
'Bounded',
'Sdk',
'Dynamic',
'Prefixed',
'Named',
'Pair',
'Nested',
'OldCallback',
'Formatted',
]);
// An annotated typedef keeps the dartdoc above its annotation.
expect(result.nodes.find((n) => n.name === 'OldCallback')!.docstring).toBe('Documented.');
});
it.each(backends)(`every type the alias names is referenced from the typedef: %s${label}`, (backend) => {
const source = eol(SOURCE);
const result = extract(backend, source);
// Built-ins (`void`, `int`, `String`, `dynamic`) are no references, an
// import prefix (`m`) is none, and neither is a type parameter (`T`,
// `X`), declared by the typedef or by a generic function type.
expect(typedefRefs(result)).toEqual([
'Bounded List',
'Bounded Report',
'Formatted Map',
'Formatted Report',
'Formatted Report',
'Formatted Report',
'LegacyCallback Report',
'LegacyLoader List',
'LegacyLoader Report',
'LegacyLoader Report',
'LegacyMaker Report',
'LegacyPicker Report',
'Mapper Report',
'Named Report',
'Nested Report',
'OldCallback Report',
'Pair Report',
'Pair Report',
'Picker Report',
'Prefixed Report',
'PrefixedLegacy Report',
'PrefixedLegacy Report',
'ReportCallback Report',
'ReportMaker Report',
'Reports List',
'Reports Report',
'ReportsByName Map',
'ReportsByName Report',
'Results Future',
'Results List',
'Results Report',
'Sdk Future',
]);
// Each sits on the type's name, where resolution reads an import prefix back.
const lines = source.split(/\r?\n/);
const byId = new Map(result.nodes.map((n) => [n.id, n]));
for (const r of result.unresolvedReferences.filter((x) => byId.get(x.fromNodeId)?.kind === 'type_alias')) {
expect(lines[r.line - 1]!.startsWith(r.referenceName, r.column), `${r.referenceName} at ${r.line}:${r.column}`).toBe(true);
}
// The file takes none of them: no type is credited twice.
const file = result.nodes.find((n) => n.kind === 'file')!;
expect(result.unresolvedReferences.filter((r) => r.fromNodeId === file.id && r.referenceKind === 'references')).toEqual([]);
});
}
});
describe('an indexed Dart library links a typedef to the types it aliases (#2327)', () => {
const FILES: Record<string, string> = {
'pubspec.yaml': 'name: repro\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
// The issue's repro.
'lib/report.dart': 'class Report {\n final int score;\n const Report(this.score);\n}\n',
'lib/callback.dart': "import 'report.dart';\n\ntypedef ReportCallback = void Function(Report r);\n",
// A namesake reached only through the `m.` prefix.
'lib/model.dart': 'class Report {}\n',
// Project types named like the type parameters, imported where those are declared.
'lib/params.dart': 'class T {}\n\nclass X {}\n',
// A project `Future` the typedefs' library does not import.
'lib/async.dart': 'class Future {}\n',
'lib/typedefs.dart': `import 'model.dart' as m;
import 'params.dart';
import 'report.dart';
typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef Report LegacyMaker(int n);
typedef Mapper<T> = T Function(Report r);
typedef Picker = T Function<T>(T a, Report r);
typedef Bounded<X extends Report> = List<X>;
typedef Sdk = Future<String>;
typedef Prefixed = m.Report;
typedef Pair = (Report, {m.Report other});
`,
};
let root = '';
let cg: CodeGraph | undefined;
let kernel: string | undefined;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-typedef-refs-'));
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;
});
it.each(['default', 'wasm'])('callers of a type list the typedefs built on it (%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 graph = cg;
const node = (file: string, name: string): Node => {
const found = graph.getNodesInFile(file).filter((n) => n.name === name && n.kind !== 'import');
expect(found, `${name} in ${file}`).toHaveLength(1);
return found[0]!;
};
/** `kind file:qualifiedName` of each `references` target of the typedef `name`, sorted. */
const referenced = (name: string): string[] =>
[...new Set(graph
.getOutgoingEdgesFrom([node('lib/typedefs.dart', name).id], ['references'])
.map((e) => graph.getNode(e.target)!)
.map((n) => `${n.kind} ${n.filePath}:${n.qualifiedName}`))]
.sort();
// `callers Report` lists every typedef that names it, the issue's first.
const callers = graph.getCallers(node('lib/report.dart', 'Report').id).map(({ node: n }) => `${n.kind} ${n.filePath}:${n.name}`);
expect(callers.filter((c) => c.startsWith('type_alias')).sort()).toEqual([
'type_alias lib/callback.dart:ReportCallback',
'type_alias lib/typedefs.dart:Bounded',
'type_alias lib/typedefs.dart:LegacyMaker',
'type_alias lib/typedefs.dart:Mapper',
'type_alias lib/typedefs.dart:Pair',
'type_alias lib/typedefs.dart:Picker',
'type_alias lib/typedefs.dart:ReportMaker',
'type_alias lib/typedefs.dart:Reports',
]);
// A prefixed type is the one its import names.
expect(referenced('Prefixed')).toEqual(['class lib/model.dart:Report']);
expect(referenced('Pair')).toEqual(['class lib/model.dart:Report', 'class lib/report.dart:Report']);
// A type parameter is not the project's `T` or `X`, though both are imported.
expect(referenced('Mapper')).toEqual(['class lib/report.dart:Report']);
expect(referenced('Picker')).toEqual(['class lib/report.dart:Report']);
expect(referenced('Bounded')).toEqual(['class lib/report.dart:Report']);
expect(graph.getCallers(node('lib/params.dart', 'T').id)).toEqual([]);
expect(graph.getCallers(node('lib/params.dart', 'X').id)).toEqual([]);
// The SDK's Future is not a project class the library does not import.
expect(referenced('Sdk')).toEqual([]);
expect(graph.getCallers(node('lib/async.dart', 'Future').id)).toEqual([]);
// The legacy typedef is named LegacyMaker; nothing else is named Report there.
expect(graph.getNodesInFile('lib/typedefs.dart').filter((n) => n.name === 'Report')).toEqual([]);
expect(referenced('LegacyMaker')).toEqual(['class lib/report.dart:Report']);
}, 60_000);
});
@@ -0,0 +1,78 @@
// Typedefs reference the types they alias (#2327): every new-style and legacy
// shape, type parameters (the typedef's own and a generic function type's),
// import prefixes, annotations, comments and wrapped declarations.
import 'package:meta/meta.dart';
import 'model.dart' as m;
import 'model.dart' as M;
class Report {
final int score;
const Report(this.score);
}
class T {}
void tag(Object o) {}
// New style.
typedef ReportCallback = void Function(Report r);
typedef ReportMaker = Report Function();
typedef Reports = List<Report>;
typedef ReportsByName = Map<String, Report>;
typedef Results = Future<List<Report>?> Function(int page);
typedef Opt = Report?;
typedef MaybeMaker = Report Function()?;
typedef Pair = (Report, int);
typedef NamedPair = ({Report first, m.Report second});
typedef Named = void Function({required Report report, int? n});
typedef Positional = void Function([Report? report]);
typedef Unnamed = void Function(Report, m.Report);
typedef Nested = void Function(void Function(Report) inner);
typedef Maker = void Function(Report Function() make);
typedef Dynamic = dynamic Function(Object? o);
typedef Void = void;
typedef Fn = Function;
typedef Prefixed = m.Report;
typedef UpperPrefixed = M.Report;
typedef PrefixedGeneric = m.Box<m.Report>;
// Type parameters: the typedef's own, a bound, a generic function type's.
typedef Mapper<T> = T Function(Report r);
typedef Bounded<X extends Report> = List<X>;
typedef SelfBounded<C extends Comparable<C>> = int Function(C a, C b);
typedef Picker = T Function<T>(T a, Report r);
typedef Outer<S> = S Function<U>(U u, S s, T t);
typedef Shadow<T> = m.T Function(T t);
typedef TwoParams<K, V extends Report> = Map<K, List<V>>;
// Legacy.
typedef void LegacyCallback(Report r);
typedef Report LegacyMaker(int n);
typedef List<Report> LegacyLoader([Report? seed]);
typedef m.Report PrefixedLegacy(m.Report r);
typedef Report? LegacyOptional({Report? fallback});
typedef void Function(Report) LegacyFnReturn(int n);
typedef T LegacyPicker<T>(T a, Report r);
typedef void LegacyFnParam(Report make(int n));
typedef void LegacyGenericFnParam(S make<S>(S s, Report r));
typedef void LegacyCovariant(covariant Report r, final Report s);
// Annotated, documented, commented, wrapped.
/// Documented.
@Deprecated('Use ReportCallback')
typedef OldCallback = void Function(Report r);
@visibleForTesting
@Tag(tag)
typedef Tagged = Report Function(/* the report */ Report r);
typedef Commented = // why
Report Function(Report r);
typedef Wrapped =
Map<String, Report> Function(
Report first,
Report second,
);
class Tag {
const Tag(this.f);
final Object f;
}
+9 -2
View File
@@ -37,8 +37,10 @@
* TortureAnnotatedComments.dart: the comments between annotations and their
* declaration, stepped over by the decorator scan; TortureDirectives.dart /
* TorturePartOfName.dart: `part` as an import of the part's file, `part of`
* as nothing, and the arguments of annotated directives) and their CRLF
* variants (derived in-memory — #1329), plus defer and generated-file pins.
* as nothing, and the arguments of annotated directives;
* TortureTypedefs.dart: the types a typedef aliases, in every form) 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
@@ -167,6 +169,11 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
['TortureDirectives.dart', 20],
// `part of` a library named by its library name records nothing.
['TorturePartOfName.dart', 3],
// Typedefs reference the types they alias (#2327), new style and legacy
// (named by its own name, not its return type), with type parameters —
// the typedef's own and a generic function type's — skipped, prefixed,
// annotated, commented and wrapped declarations.
['TortureTypedefs.dart', 40],
] as const;
for (const [file, minNodes] of FIXTURES) {
+92 -3
View File
@@ -156,6 +156,23 @@ fn next_named<'t>(node: Node<'t>) -> Option<Node<'t>> {
next
}
/// dartTypeAliasName (dart.ts) — the name a typedef declares: the last type
/// name before the `=`, the type parameters or the parameter list. The
/// legacy form writes its return type first (`typedef Report LegacyMaker(int
/// n);`), so the first type name is not always it.
fn type_alias_name<'t>(node: Node<'t>) -> Option<Node<'t>> {
let mut name = None;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"=" | "type_parameters" | "formal_parameter_list" => break,
"type_identifier" => name = Some(child),
_ => {}
}
}
name
}
struct Scope {
row: u32,
kind: &'static str,
@@ -668,6 +685,14 @@ impl<'t> Walker<'t> {
/// name field → the method_signature inner unwrap → identifier-ish
/// child → `<anonymous>` (operators land here).
fn extract_name(&self, node: Node<'t>) -> String {
// resolveName hook (dart.ts): a typedef has no `name` field, and a
// legacy one writes its return type before its name (`typedef Report
// LegacyMaker(int n);`).
if node.kind() == "type_alias" {
if let Some(name) = type_alias_name(node) {
return self.text(name).to_string();
}
}
// resolveName hook (dart.ts:244-260): named ctor/factory → ctor name.
if let Some((class_name, ctor_name)) = self.ctor_info(node) {
if ctor_name != class_name {
@@ -1034,12 +1059,76 @@ impl<'t> Walker<'t> {
return false;
}
let docstring = self.docstring_of(node);
// `value` field is null (type_alias has no fields) → no refs from
// the aliased type; returns false → children re-visited.
self.create_node("type_alias", &name, node, Extra { docstring, ..Default::default() });
// type_alias has no fields (no `value`): the dart branch reads the
// aliased type from the rest of the declaration (#2327). Returns
// false → children re-visited (they match nothing type-related).
let row = self.create_node("type_alias", &name, node, Extra { docstring, ..Default::default() });
if let Some(row) = row {
self.type_alias_refs(node, row);
}
false
}
/// pushDartTypeAliasRefs (dart.ts) — a `references` ref from the typedef
/// for every type its aliased type names; its annotations are not its
/// type, and a type parameter is no type of the project's.
fn type_alias_refs(&mut self, node: Node<'t>, from_row: u32) {
let name = type_alias_name(node);
let scope = self.type_parameter_scope(node, &[]).unwrap_or_default();
let mut cursor = node.walk();
let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
for child in kids {
if child.kind() == "annotation" || Some(child) == name {
continue;
}
self.aliased_type_refs(child, from_row, &scope);
}
}
/// dartTypeParameterScope (dart.ts) — `scope` and the type parameters
/// `node` declares (`Mapper<T>`, `T Function<T>(T a)`), or None when it
/// declares none.
fn type_parameter_scope(&self, node: Node<'t>, scope: &[&'t str]) -> Option<Vec<&'t str>> {
let mut cursor = node.walk();
let parameters = node.named_children(&mut cursor).find(|c| c.kind() == "type_parameters")?;
let mut inner = scope.to_vec();
let mut pc = parameters.walk();
for parameter in parameters.named_children(&mut pc) {
if parameter.kind() != "type_parameter" {
continue;
}
let mut ic = parameter.walk();
let name = parameter.named_children(&mut ic).find(|c| c.kind() == "type_identifier");
if let Some(name) = name {
inner.push(self.text(name));
}
}
Some(inner)
}
/// pushDartAliasedTypeRefs (dart.ts) — type_refs_dart for a typedef's
/// aliased type, where a type parameter in `scope` is no type of its own
/// (but `p.T` is the type an import names).
fn aliased_type_refs(&mut self, node: Node<'t>, from_row: u32, scope: &[&'t str]) {
stack_guard!();
if node.kind() == "type_identifier" {
let text = self.text(node);
let parameter =
scope.contains(&text) && !node.prev_sibling().map(|s| s.kind() == ".").unwrap_or(false);
if !parameter && self.is_type_name_dart(node) {
self.push_ref_at(from_row, text, "references", node);
}
return;
}
let inner = self.type_parameter_scope(node, scope);
let scope = inner.as_deref().unwrap_or(scope);
let mut cursor = node.walk();
let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
for child in kids {
self.aliased_type_refs(child, from_row, scope);
}
}
// --- extractImport (:3170; hook dart.ts extractImport) -----------------
fn extract_import(&mut self, node: Node<'t>) {
+10 -4
View File
@@ -226,7 +226,12 @@ Grammar-shape facts the config leans on (all probed, `mini-cst.txt` /
keyword node, identifier, on-types as bare type_identifiers, class_body).
`enum_declaration`: `name:` + `body:` (enum_body); `enum_constant.name` ✓.
`type_alias` has NO fields (both modern `typedef X = T` and legacy
`typedef void X(int)` are node `type_alias`).
`typedef void X(int)` are node `type_alias`). Its name is the last direct
`type_identifier` before the `=`, `type_parameters` or
`formal_parameter_list` child: a legacy return type comes first (`typedef
Report X(int n)`), and until 2026-10-21 the first-identifier fallback
named such a typedef after its return type (`dartTypeAliasName` /
`type_alias_name` now, via resolveName).
`initialized_variable_definition`: `name:` + one-or-more `value:` fields
(the RHS postfix chain is FLATTENED into multiple value-fielded siblings).
- **In-class member shapes:** methods/getters/setters/operators/factories
@@ -472,7 +477,7 @@ Registration: `EXTRACTORS.dart` (languages/index.ts:56), `FN_REF_SPECS.dart`
| `mixin_declaration`, `extension_declaration` | extraClassNodeTypes:1022 | extractClass(node) → kind **`class`** |
| `method_signature`, `constructor_signature` | methodTypes:1027 | **NOT class-gated at the ladder** — extractMethod:1737 runs anywhere; its own gate :1747 (not class-like, no methodsAreTopLevel, no receiver) falls back: parent `object`/`object_expression`? (never in dart) else **extractFunction** — this is how extension-TYPE members and any stray non-class method_signature become plain `function` nodes (pinned: `extension type MetersT` → function `km`, bare QN) |
| `enum_declaration` | enumTypes:1064 → extractEnum:1914 | §Class family |
| `type_alias` | typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node; `getChildByField(node,'value')` → **null** (no fields) → **NO refs from the aliased type** (`typedef MapAlias = Map<String, WidgetT>` emits nothing); returns false → children re-visited (function_type/formal_parameter_list children match nothing) |
| `type_alias` | typeAliasTypes:1071 → extractTypeAlias:2890 | type_alias node named by the resolveName hook (§Extractor config); `getChildByField(node,'value')` → **null** (no fields), so the dart branch — `pushDartTypeAliasRefs` / `type_alias_refs`, since 2026-10-21 (#2327; nothing before) — emits a `references` ref from the node for each type named in every named child but the name and `annotation`s, through the isDartTypeName gate (`typedef MapAlias = Map<String, WidgetT>` → `Map`, `WidgetT`). A type parameter in scope is skipped unless written `p.T`: the typedef's own, and those of any node that declares `type_parameters` (a generic `function_type`, an old-style generic function-typed `formal_parameter`) within it. Returns false → children re-visited (they match nothing type-related; an annotated typedef's arguments are fn-ref capture points) |
| `import_or_export`, `part_directive` | importTypes:1209 → extractImport:3170 | §Extractor config. No skipChildren: the directive's children are still recursed, so an annotated directive's arguments are fn-ref capture points (`@Tag(f) import 'a.dart';` captures `f`). The kernel arm recurses too — since 2026-10-06; before, its `import_or_export` arm returned and missed those (TortureDirectives.dart pins both) |
| `new_expression` | INSTANTIATION_KINDS:1255 (`new_expression` ∈ :354-361) | extractInstantiation:4610 → ctor field lookups null → namedChild(0) = type_identifier → `instantiates` ref from stack top; `<`-strip + last-`.`-segment apply (`new p.Foo<T>()` → `Foo`). findAnonymousClassBody → always null for dart. Children still recursed |
| `function_body` (sibling of a consumed signature) | **no branch** | recursed → THE DOUBLE-WALK (§below) |
@@ -1299,8 +1304,9 @@ NO unwrap, NO ungatedModes, NO addressOfOnly. Pins:
sealed/base modifiers (plain classes); enum (simple + enhanced: ctor'd
constants at constant positions, args unwalked, `with` GAP vs
`implements` ✓, members after `;`); typedef ×3 (modern/legacy/generic —
zero refs); extension type (container invisible, **members leak as bare
`function`s**); the FULL call matrix (§Calls rows verbatim — incl.
zero refs until 2026-10-21, the aliased types since; every form is in
TortureTypedefs.dart); extension type (container invisible, **members
leak as bare `function`s**); the FULL call matrix (§Calls rows verbatim — incl.
cascade invisibility, `?.`, const-object args, chain re-encodes,
interpolation calls, `$x` vs `${x}`); static reads (`X.member`,
`X.method()` double-emission, 3-segment nothing, case-pattern GAP);
+79
View File
@@ -43,6 +43,82 @@ export function pushDartTypeRefs(
for (const child of node.namedChildren) pushDartTypeRefs(child, fromNodeId, push);
}
/**
* The name a typedef declares. The grammar gives `type_alias` no fields, and
* the first type name is not always it: the legacy form writes its return
* type first (`typedef Report LegacyMaker(int n);`). The name is the last type
* name before the `=`, the type parameters or the parameter list.
*/
export function dartTypeAliasName(node: SyntaxNode): SyntaxNode | undefined {
let name: SyntaxNode | undefined;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (!child || child.type === '=' || child.type === 'type_parameters' || child.type === 'formal_parameter_list') break;
if (child.type === 'type_identifier') name = child;
}
return name;
}
/** `scope` and the type parameters `node` declares, when it declares any (`Mapper<T>`, `T Function<T>(T a)`). */
function dartTypeParameterScope(node: SyntaxNode, scope: ReadonlySet<string>): ReadonlySet<string> {
const parameters = node.namedChildren.find((c: SyntaxNode) => c.type === 'type_parameters');
if (!parameters) return scope;
const inner = new Set(scope);
for (const parameter of parameters.namedChildren) {
const name = parameter.type === 'type_parameter'
? parameter.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier')
: undefined;
if (name) inner.add(name.text);
}
return inner;
}
/** pushDartTypeRefs for a typedef's aliased type: a type parameter in `scope` is no type of its own. */
function pushDartAliasedTypeRefs(
node: SyntaxNode,
fromNodeId: string,
scope: ReadonlySet<string>,
push: (ref: UnresolvedReference) => void,
): void {
if (node.type === 'type_identifier') {
// `p.T` is the type an import names, whatever is declared as `T` here.
const parameter = scope.has(node.text) && node.previousSibling?.type !== '.';
if (!parameter && isDartTypeName(node)) {
push({
fromNodeId,
referenceName: node.text,
referenceKind: 'references',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
return;
}
const inner = dartTypeParameterScope(node, scope);
for (const child of node.namedChildren) pushDartAliasedTypeRefs(child, fromNodeId, inner, push);
}
/**
* A `references` ref from the typedef `node` for every type its aliased type
* names (#2327): a function type's return and parameter types, generic
* arguments, record fields, a type parameter's bound, and the legacy form's
* return and parameter types. A type parameter, the typedef's own or a
* generic function type's (`T Function<T>(T a)`), means that parameter and no
* type of the project's. The typedef's annotations are not its type.
*/
export function pushDartTypeAliasRefs(
node: SyntaxNode,
fromNodeId: string,
push: (ref: UnresolvedReference) => void,
): void {
const name = dartTypeAliasName(node);
const scope = dartTypeParameterScope(node, new Set());
for (const child of node.namedChildren) {
if (child.type === 'annotation' || (name && child.equals(name))) continue;
pushDartAliasedTypeRefs(child, fromNodeId, scope, push);
}
}
/** The bodies a Dart field `declaration` can sit in: a class's or mixin's, an extension's, an enum's. */
const DART_MEMBER_BODIES: ReadonlySet<string> = new Set(['class_body', 'extension_body', 'enum_body']);
@@ -646,6 +722,9 @@ export const dartExtractor: LanguageExtractor = {
return false;
},
resolveName: (node) => {
// A typedef has no `name` field, and a legacy one writes its return type
// before its name (`typedef Report LegacyMaker(int n);`).
if (node.type === 'type_alias') return dartTypeAliasName(node)?.text;
// `class A = B with C;` — a mixin application — names its class inside
// the `mixin_application_class`, not in a `name` field.
if (node.type === 'class_definition') {
+7 -1
View File
@@ -26,7 +26,7 @@ import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './lang
import { appendCppMacroMethods } from './languages/cpp-macro-methods';
import { rustImplItemOwner, rustImplTypeName } from './languages/rust';
import { goAliasTypeNames, goEmbeddedTypeName, goInterfaceMethodTypeNames, goStructFieldTypeNames } from './languages/go';
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart';
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeAliasRefs, pushDartTypeRefs } from './languages/dart';
import { LiquidExtractor } from './liquid-extractor';
import { RazorExtractor } from './razor-extractor';
import { SvelteExtractor } from './svelte-extractor';
@@ -4246,6 +4246,12 @@ export class TreeSitterExtractor {
column: type.startPosition.column,
});
}
} else if (this.language === 'dart') {
// A Dart typedef has no fields either: the type it aliases is the
// rest of the declaration, `void Function(Report r)` in `typedef
// ReportCallback = void Function(Report r);`, or a legacy one's
// return and parameter types (#2327).
pushDartTypeAliasRefs(node, typeAliasNode.id, (ref) => this.unresolvedReferences.push(ref));
}
}
return false;