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
+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;