mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
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:
co-authored by
Claude Opus 5.5
parent
dbb6aa9ba9
commit
bf6f1fa67e
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user