mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(dart): index const constructors and redirecting factories; a call through a type stays on that type (#2380)
* fix(dart): index const constructors and redirecting factories; a call through a type stays on that type Both Dart extractors (the TS walker and the native kernel) skipped `const` constructors and redirecting factories (`const Foo.bar(...)`, `const factory Foo.bar() = _Bar;`), which parse as signature kinds of their own. A call such as flutter_bloc's `BlocProvider.value(...)` then found no method on BlocProvider, and the resolver guessed another type's `value`: on felangel/bloc, 89 calls linked to `RepositoryProvider::value`. Extraction (TS + kernel, at parity): - Named const constructors and redirecting factories are methods named by the constructor, returning their class, with a parameters-only signature; the unnamed ones stay unindexed. The name is read only from before the parameter list, so a redirect target (`= _Impl.named`) never names it. - A redirecting factory references the class it redirects to, not the target constructor's name or an import prefix. - A constructor called with type arguments keeps its type: `BlocProvider<CounterCubit>.value(...)` is `BlocProvider.value`, as a statement and as an expression (`constructor_invocation`, which produced no call at all before). Resolver: - A Dart call through a type's name reaches only that type's own constructor, static member or static constant, or nothing: no guess by name, no inherited lookup, no same-file look-alike, no instance member of the same name. SDK calls like `Uri.parse` no longer land on a project `parse` past an `extension on Uri`. - A named constructor is never reached by a bare name, the last link of a chain, a function value or an annotation (riverpod's 210 `@internal` annotations). Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2 with the kernel loaded: bloc 18,338 -> 18,495 edges, riverpod 85,606 -> 85,854, every removed edge triaged. Afterwards every edge into a named constructor is written `Owner.ctor`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dart): read a type's static members one way for calls and chains - A call through a type's name now finds its member with dartStaticMember, the lookup a chain's static link uses since #2376, so both pick the same member: the call site's own file first, then the nearest copy. - isDartStaticMember's constructor test is isDartConstructor, anchored at the declaration: an instance getter declared just above a same-named factory (`Object? get error` over `const factory AsyncValue.error(...)`) read the factory from the next lines and counted as static. - clearNameMatcherMemos drops DART_CONSTRUCTORS with the other source-derived Dart memos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4d6168a701
commit
652c36b72a
@@ -100,6 +100,16 @@ function dartInnerSignature(node: SyntaxNode): SyntaxNode {
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructors that never have a body — `const Foo.bar();` and a
|
||||
* redirecting factory `const factory Foo.bar() = _Bar;` — each parse as a
|
||||
* signature kind of its own, inside a `declaration`.
|
||||
*/
|
||||
const DART_BODILESS_CTORS: ReadonlySet<string> = new Set([
|
||||
'constant_constructor_signature',
|
||||
'redirecting_factory_constructor_signature',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The factory/named-constructor signature inside a node, if any. A constructor
|
||||
* parses as `method_signature > {factory_,}constructor_signature` (e.g.
|
||||
@@ -107,7 +117,10 @@ function dartInnerSignature(node: SyntaxNode): SyntaxNode {
|
||||
* and — for a named ctor — the constructor-name identifier.
|
||||
*/
|
||||
function dartConstructorSignature(node: SyntaxNode): SyntaxNode | undefined {
|
||||
if (node.type === 'factory_constructor_signature' || node.type === 'constructor_signature') {
|
||||
if (
|
||||
node.type === 'factory_constructor_signature' || node.type === 'constructor_signature' ||
|
||||
DART_BODILESS_CTORS.has(node.type)
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
if (node.type === 'method_signature') {
|
||||
@@ -146,7 +159,13 @@ function dartEnclosingTypeName(node: SyntaxNode): string | undefined {
|
||||
function dartCtorInfo(node: SyntaxNode): { className: string; ctorName: string } | undefined {
|
||||
const ctor = dartConstructorSignature(node);
|
||||
if (!ctor) return undefined;
|
||||
const ids = ctor.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier');
|
||||
// The names before the parameters: a redirecting factory names its target
|
||||
// after them (`factory Foo() = _Impl.named;`).
|
||||
const ids: SyntaxNode[] = [];
|
||||
for (const c of ctor.namedChildren) {
|
||||
if (c.type === 'formal_parameter_list') break;
|
||||
if (c.type === 'identifier') ids.push(c);
|
||||
}
|
||||
const className = dartEnclosingTypeName(node);
|
||||
if (!className || !ids[0]) return undefined;
|
||||
if (ids[0].text !== className) return undefined; // misparsed method, not a ctor
|
||||
@@ -176,6 +195,19 @@ function extractDartReturnType(node: SyntaxNode, source: string): string | undef
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type a call written with type arguments goes through — `BlocProvider`
|
||||
* in `BlocProvider<CounterCubit>.value(…)`, when `selector` is the `<…>`
|
||||
* between the type and `.value`. Only a constructor is called that way.
|
||||
*/
|
||||
function dartTypeArgumentsReceiver(selector: SyntaxNode | null): string | undefined {
|
||||
if (selector?.type !== 'selector' || selector.namedChildCount !== 1 || selector.namedChild(0)?.type !== 'type_arguments') {
|
||||
return undefined;
|
||||
}
|
||||
const type = selector.previousNamedSibling;
|
||||
return type?.type === 'identifier' ? type.text : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callee name of the Dart call whose `argument_part` selector is `argPart`
|
||||
* — mirrors the main extractBareCall accessor logic so a chained receiver
|
||||
@@ -194,6 +226,8 @@ function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
||||
if (methodId) {
|
||||
const accessorPrev = prev.previousNamedSibling;
|
||||
if (accessorPrev?.type === 'identifier') return accessorPrev.text + '.' + methodId.text;
|
||||
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
||||
if (typeName) return typeName + '.' + methodId.text;
|
||||
return methodId.text;
|
||||
}
|
||||
}
|
||||
@@ -208,7 +242,11 @@ export const dartExtractor: LanguageExtractor = {
|
||||
// constructor `Foo._()` parses as a bare `constructor_signature`, so include
|
||||
// it too — resolveName names it by the ctor name and getReturnType gives it
|
||||
// the class as its return type, so `Foo._().bar()` chains resolve (#750).
|
||||
methodTypes: ['method_signature', 'constructor_signature'],
|
||||
// `const` constructors and redirecting factories have signature kinds of
|
||||
// their own: flutter_bloc's `const BlocProvider.value(…)` is one, and
|
||||
// without its node a `BlocProvider.value(…)` call went to another class's
|
||||
// `value`.
|
||||
methodTypes: ['method_signature', 'constructor_signature', ...DART_BODILESS_CTORS],
|
||||
interfaceTypes: [],
|
||||
structTypes: [],
|
||||
enumTypes: ['enum_declaration'],
|
||||
@@ -314,7 +352,10 @@ export const dartExtractor: LanguageExtractor = {
|
||||
// tree-sitter misparsed as a ctor (`@override (T) m()`) is NOT skipped here.
|
||||
// (isMisparsedFunction skips node creation but still visits the body.)
|
||||
const ctor = dartCtorInfo(node);
|
||||
return ctor != null && ctor.ctorName === ctor.className;
|
||||
// A `const` constructor or redirecting factory is nothing else, so one
|
||||
// that names no enclosing type is error recovery's, and skipped.
|
||||
if (!ctor) return DART_BODILESS_CTORS.has(node.type);
|
||||
return ctor.ctorName === ctor.className;
|
||||
},
|
||||
getSignature: (node, source) => {
|
||||
// For function_signature: extract params + return type
|
||||
@@ -327,7 +368,9 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (inner) sig = inner;
|
||||
}
|
||||
const params = sig.namedChildren.find((c: SyntaxNode) => c.type === 'formal_parameter_list');
|
||||
const retType = sig.namedChildren.find((c: SyntaxNode) =>
|
||||
// A constructor has no return type: the type a redirecting factory names
|
||||
// is its target (`= _Impl`).
|
||||
const retType = DART_BODILESS_CTORS.has(sig.type) ? undefined : sig.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'type_identifier' || c.type === 'void_type'
|
||||
);
|
||||
if (!params && !retType) return undefined;
|
||||
@@ -467,6 +510,10 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (accessorPrev?.type === 'identifier') {
|
||||
return accessorPrev.text + '.' + methodId.text;
|
||||
}
|
||||
// A constructor called with type arguments names its type all the
|
||||
// same: `BlocProvider<CounterCubit>.value(…)` → `BlocProvider.value`.
|
||||
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
||||
if (typeName) return typeName + '.' + methodId.text;
|
||||
// Chained static-factory / fluent call: the receiver is itself a call
|
||||
// (`Foo.create().bar()`), so accessorPrev is that call's argument_part
|
||||
// selector. Encode `<innerCallee>().<method>` so resolution can infer
|
||||
@@ -512,6 +559,17 @@ export const dartExtractor: LanguageExtractor = {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// `=> BlocProvider<CounterCubit>.value(…)` — a constructor called with type
|
||||
// arguments, as most expressions parse it. The type is the last
|
||||
// `type_identifier`: an import prefix (`p.X<T>.named`) is one too.
|
||||
if (node.type === 'constructor_invocation') {
|
||||
const typeId = node.namedChildren.filter((c: SyntaxNode) => c.type === 'type_identifier').pop();
|
||||
const nameId = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (typeId && nameId) return typeId.text + '.' + nameId.text;
|
||||
if (typeId) return typeId.text;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
extractMemberRead: dartMemberRead,
|
||||
|
||||
@@ -7280,6 +7280,18 @@ export class TreeSitterExtractor {
|
||||
c.type === 'factory_constructor_signature'
|
||||
) ?? node;
|
||||
}
|
||||
// A redirecting factory names the class it constructs after `=`, then
|
||||
// perhaps that class's constructor (`= _$QuestionImpl.fromJson`, which
|
||||
// parses as two type_identifiers). Neither the constructor nor an import
|
||||
// prefix is a type, and Dart writes types UpperCamel.
|
||||
if (sig.type === 'redirecting_factory_constructor_signature') {
|
||||
for (const child of sig.namedChildren) {
|
||||
if (child.type !== 'type_identifier' || /^[_$]*[A-Z]/.test(child.text)) {
|
||||
this.extractTypeRefsFromSubtree(child, nodeId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.extractTypeRefsFromSubtree(sig, nodeId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1106,7 +1106,9 @@ export function matchFunctionRef(
|
||||
// defines inside `test_custom_auth`. Those still count against a lone
|
||||
// cross-file guess below — a name several functions use for themselves is
|
||||
// as likely a local's.
|
||||
let candidates = named.filter((n) => isLexicallyReachable(n, ref, context));
|
||||
let candidates = named.filter((n) => isLexicallyReachable(n, ref, context) &&
|
||||
// A Dart constructor is torn off through its class, never by a bare name.
|
||||
!isDartConstructor(n, context));
|
||||
if (candidates.length === 0) return null;
|
||||
// A Python name the function around it binds — a parameter, an assignment —
|
||||
// is that local's value: httpx's `auth_flow(self, request)` handing `request`
|
||||
@@ -2866,6 +2868,32 @@ function isDartMethodInScope(method: Node, ref: UnresolvedRef, context: Resoluti
|
||||
return dartMemberDepth(method, ref, context) < Infinity;
|
||||
}
|
||||
|
||||
const DART_CONSTRUCTORS = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/**
|
||||
* Whether a Dart method is a named constructor — `Foo.named(…)`, `factory
|
||||
* Foo.of(…)`, `const Foo.value(…)`, `const factory Foo.x() = _X;` — read from
|
||||
* its declaration. Dart reaches one only through its class (`Foo.value(…)`,
|
||||
* the tear-off `Foo.value`), never by a bare name: a parameter `value` passed
|
||||
* on is not `const BlocProvider.value(…)`.
|
||||
*/
|
||||
function isDartConstructor(n: Node, context: ResolutionContext): boolean {
|
||||
if (n.language !== 'dart' || n.kind !== 'method') return false;
|
||||
let memo = DART_CONSTRUCTORS.get(context);
|
||||
if (!memo) DART_CONSTRUCTORS.set(context, (memo = new Map()));
|
||||
const hit = memo.get(n.id);
|
||||
if (hit !== undefined) return hit;
|
||||
const cut = n.qualifiedName.lastIndexOf('::');
|
||||
const owner = cut > 0 ? n.qualifiedName.slice(0, cut).split('::').pop()! : '';
|
||||
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
|
||||
const head = (lines[n.startLine - 1] ?? '').slice(n.startColumn);
|
||||
const escape = (name: string): string => name.replace(/\$/g, '\\$');
|
||||
const constructor = owner !== '' &&
|
||||
new RegExp(String.raw`^(?:(?:const|factory|external)\s+)*${escape(owner)}\s*\.\s*${escape(n.name)}(?![\w$])`).test(head);
|
||||
memo.set(n.id, constructor);
|
||||
return constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many supertype steps separate the class a Dart call is written in from
|
||||
* `method`'s owner: 0 for its own member, Infinity when the owner is not in
|
||||
@@ -3461,25 +3489,21 @@ function dartStaticMember(typeName: string, name: string, ref: UnresolvedRef, co
|
||||
const found = context.getNodesByName(name).filter((m) => m.language === 'dart' &&
|
||||
(m.kind === 'method' || m.kind === 'function' || m.kind === 'constant' || m.kind === 'enum_member') &&
|
||||
owners.some((o) => o.filePath === m.filePath && m.qualifiedName === `${o.qualifiedName}::${name}`) &&
|
||||
!isDartSetter(m, context) && isDartStaticMember(m, typeName, context));
|
||||
!isDartSetter(m, context) && isDartStaticMember(m, context));
|
||||
if (found.length <= 1) return found[0] ?? null;
|
||||
return found.find((m) => m.filePath === ref.filePath) ??
|
||||
found.reduce((a, b) => (computePathProximity(ref.filePath, b.filePath) > computePathProximity(ref.filePath, a.filePath) ? b : a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a member of the Dart type `owner` is reached through the type
|
||||
* itself: a static member, constant or enum value, or a named constructor or
|
||||
* factory (`factory ProviderContainer.test(…)`) — not an instance member
|
||||
* sharing a constructor's name (`AsyncValue`'s `error` getter beside its
|
||||
* `AsyncValue.error(…)` factory).
|
||||
* Whether a member of a Dart type is reached through the type itself: a
|
||||
* static member, constant or enum value, or a named constructor or factory
|
||||
* (`factory ProviderContainer.test(…)`) — not an instance member sharing a
|
||||
* constructor's name (`AsyncValue`'s `error` getter beside its
|
||||
* `AsyncValue.error(…)` factory, even declared on the line above it).
|
||||
*/
|
||||
function isDartStaticMember(m: Node, owner: string, context: ResolutionContext): boolean {
|
||||
if (m.isStatic || m.kind === 'constant' || m.kind === 'enum_member') return true;
|
||||
const lines = context.getFileLines?.(m.filePath) ?? context.readFile(m.filePath)?.split(/\r?\n/) ?? [];
|
||||
const head = `${(lines[m.startLine - 1] ?? '').slice(m.startColumn)} ${lines[m.startLine] ?? ''} ${lines[m.startLine + 1] ?? ''}`;
|
||||
const esc = (s: string): string => s.replace(/\$/g, '\\$');
|
||||
return new RegExp(`(?:^|[^\\w$.])${esc(owner)}\\s*\\.\\s*${esc(m.name)}\\s*[(<]`).test(head);
|
||||
function isDartStaticMember(m: Node, context: ResolutionContext): boolean {
|
||||
return m.isStatic === true || m.kind === 'constant' || m.kind === 'enum_member' || isDartConstructor(m, context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3969,6 +3993,31 @@ function dartExtensionMemberOf(typeName: string, name: string, ref: UnresolvedRe
|
||||
return found?.viaExtension ? found.node : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Dart call through a type's name — `BlocProvider.value(…)`, riverpod's
|
||||
* `FutureProvider.autoDispose(…)` — is to one of that type's own constructors
|
||||
* or static members, or to nothing the project declares. Dart inherits none
|
||||
* of them, so a guess past the type is always wrong: flutter_bloc's
|
||||
* `BlocProvider.value(…)` went to `RepositoryProvider`'s `value` 89 times,
|
||||
* riverpod's `Family2Family._()` to the `EmptyFamily2Family._` declared above
|
||||
* it, and the SDK's `Uri.parse(…)`, past bloc_lint's `extension on Uri`, to a
|
||||
* project `parse`. A static constant's callee is the value it holds (`static
|
||||
* const autoDispose = AutoDisposeFutureProviderBuilder();`). The member is
|
||||
* found as a chain's static link finds it (dartStaticMember). Undefined when
|
||||
* the receiver names no Dart type of the project.
|
||||
*/
|
||||
function matchDartTypeMemberCall(
|
||||
typeName: string,
|
||||
member: string,
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext,
|
||||
): ResolvedRef | null | undefined {
|
||||
if (!context.getNodesByName(typeName).some((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind))) return undefined;
|
||||
const own = dartStaticMember(typeName, member, ref, context);
|
||||
return own && own.kind !== 'enum_member'
|
||||
? { original: ref, targetNodeId: own.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a bare Rust or Go name is written at its call: `path` after `::` (left
|
||||
* to the path strategies), `chained` after a `.` — with the receiver it is
|
||||
@@ -6683,6 +6732,11 @@ export function matchByExactName(
|
||||
!isKotlinNumberBitwise(n, ref) &&
|
||||
!(solidityBare && !isSolidityMemberInScope(n, ref, context)) &&
|
||||
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
|
||||
// A Dart constructor is reached through its class (`Foo.named(…)`,
|
||||
// `@Foo.named(…)`), never by its name alone: not a bare call, not the
|
||||
// last link of a chain, not an annotation (riverpod's 210 `@internal`
|
||||
// annotations went to its providers' `internal` constructors).
|
||||
!(ref.language === 'dart' && isDartConstructor(n, context)) &&
|
||||
!(phpSelf && (n.kind !== 'method' || !isPhpMethodInScope(n, ref, phpSelf, context))) &&
|
||||
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
|
||||
!(rustBare && !isRustNameInScope(n, ref, context)) &&
|
||||
@@ -8387,6 +8441,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
DART_LINEAGES.delete(context);
|
||||
DART_EXTENSION_OWNERS.delete(context);
|
||||
DART_GETTERS.delete(context);
|
||||
DART_CONSTRUCTORS.delete(context);
|
||||
DART_SETTERS.delete(context);
|
||||
DART_CODE.delete(context);
|
||||
DART_RETURN_TYPES.delete(context);
|
||||
@@ -9514,6 +9569,13 @@ export function matchMethodCall(
|
||||
// receiver type we can try to infer from its local declaration.
|
||||
const inferableReceiver = dotMatch || luaColonMatch || rDollarMatch;
|
||||
|
||||
// A Dart call through a type's name is to that type's own member, or to
|
||||
// nothing in the project.
|
||||
if (ref.language === 'dart' && dotMatch) {
|
||||
const typeMember = nmTimedT('mc-darttype', ref, () => matchDartTypeMemberCall(objectOrClass!, methodName!, ref, context));
|
||||
if (typeMember !== undefined) return typeMember;
|
||||
}
|
||||
|
||||
// Infer the receiver's type from its local declaration/initializer in the
|
||||
// enclosing scope, then resolve the method on that type (#1108). C++ keeps its
|
||||
// dedicated inferrer (header scan + `auto`); every other language uses the
|
||||
@@ -9739,24 +9801,6 @@ export function matchMethodCall(
|
||||
if (classNode.language !== ref.language) continue;
|
||||
|
||||
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
||||
// Dart: a call through a type's static constant invokes the value the
|
||||
// constant holds. riverpod's `FutureProvider.autoDispose(…)` calls
|
||||
// `static const autoDispose = AutoDisposeFutureProviderBuilder();`,
|
||||
// which the lookups below gave to another builder's `autoDispose`
|
||||
// method. A Dart type can't also declare a method of that name, and its
|
||||
// static members are not inherited, so its own constant is the callee.
|
||||
if (ref.language === 'dart') {
|
||||
const holder = nodesInFile.find((n) =>
|
||||
n.kind === 'constant' && n.name === methodName && n.qualifiedName === `${classNode.qualifiedName}::${methodName}`);
|
||||
if (holder) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: holder.id,
|
||||
confidence: 0.85,
|
||||
resolvedBy: 'qualified-name',
|
||||
};
|
||||
}
|
||||
}
|
||||
const methodNode = nodesInFile.find(
|
||||
(n) =>
|
||||
n.kind === 'method' &&
|
||||
@@ -11480,6 +11524,7 @@ export function matchFuzzy(
|
||||
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
|
||||
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
|
||||
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
|
||||
!(ref.language === 'dart' && isDartConstructor(n, context)) &&
|
||||
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
|
||||
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
|
||||
!isKotlinNumberBitwise(n, ref) &&
|
||||
|
||||
Reference in New Issue
Block a user