mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
fix(dart): type locals from prefixed-import factory calls
`import 'mod.dart' as rp; final x = rp.make(); x.m()` gave `x` no type: local call inference accepted only a bare callee (`Callee()`), and the prefix-stripping type normalizer would have reduced `rp.make` to `make`. Bind a local initialized by exactly `prefix.member(...)` (direct or awaited) to the dotted callee and keep its prefix, so the shared chain follow types it from the namespace function's return annotation. Only call-derived bindings keep the prefix; annotated `prefix.Type` spellings are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cddbe135c0
commit
a65bd7bae3
6 changed files with 105 additions and 4 deletions
|
|
@ -619,6 +619,32 @@ function findDirectCallValue(initVarDef: SyntaxNode): SyntaxNode | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `prefix.member(…)` as the whole initializer, written directly or awaited: the
|
||||
* dotted callee. The shared chain follow types a namespace function from its
|
||||
* return annotation, and a member call through the receiver's class (#3498).
|
||||
* Longer chains (`rp.make().other`) are left alone.
|
||||
*/
|
||||
function qualifiedCallValue(initVarDef: SyntaxNode): string | null {
|
||||
let head = initVarDef.childForFieldName('value');
|
||||
if (head?.type === 'unary_expression') head = head.namedChild(0);
|
||||
if (head?.type === 'await_expression') head = head.namedChild(0);
|
||||
if (head?.type !== 'identifier') return null;
|
||||
const member = head.nextNamedSibling;
|
||||
const call = member?.nextNamedSibling;
|
||||
const inner = member?.type === 'selector' ? member.namedChild(0) : null;
|
||||
const name = inner?.type === 'unconditional_assignable_selector' ? selectorName(inner) : null;
|
||||
if (
|
||||
name === null ||
|
||||
call?.type !== 'selector' ||
|
||||
call.namedChild(0)?.type !== 'argument_part' ||
|
||||
call.nextNamedSibling !== null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `${head.text}.${name.text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callee identifier of a class field initialized by a direct constructor call —
|
||||
* `var b = Outer();` / `final b = Outer();` — or `null` for anything else.
|
||||
|
|
@ -644,13 +670,13 @@ function dartFieldConstructorCallee(nameNode: SyntaxNode): SyntaxNode | null {
|
|||
function emitVarTypeBinding(initVarDef: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const nameNode = initVarDef.childForFieldName('name');
|
||||
if (nameNode === null) return;
|
||||
const calleeId = findDirectCallValue(initVarDef);
|
||||
if (calleeId === null) return;
|
||||
const callee = findDirectCallValue(initVarDef)?.text ?? qualifiedCallValue(initVarDef);
|
||||
if (callee === null) return;
|
||||
|
||||
out.push({
|
||||
'@type-binding.constructor': nodeToCapture('@type-binding.constructor', initVarDef),
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', initVarDef, nameNode.text),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', initVarDef, calleeId.text),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', initVarDef, callee),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,12 @@ export function interpretDartTypeBinding(captures: CaptureMatch): ParsedTypeBind
|
|||
const typeCap = captures['@type-binding.type'];
|
||||
if (nameCap === undefined || typeCap === undefined) return null;
|
||||
|
||||
const rawType = normalizeDartType(typeCap.text);
|
||||
// A call-derived `prefix.member` names a namespace function or a member
|
||||
// call, not a library-prefixed type, so it keeps its prefix (#3498).
|
||||
const rawType =
|
||||
captures['@type-binding.constructor'] !== undefined && typeCap.text.includes('.')
|
||||
? typeCap.text
|
||||
: normalizeDartType(typeCap.text);
|
||||
if (
|
||||
rawType === '' ||
|
||||
rawType === 'void' ||
|
||||
|
|
|
|||
18
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/lib/callers.dart
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/lib/callers.dart
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import 'mod.dart' as rp;
|
||||
|
||||
int finalCall() {
|
||||
final x = rp.make();
|
||||
return x.m();
|
||||
}
|
||||
|
||||
int varCall() {
|
||||
var x = rp.make();
|
||||
return x.m();
|
||||
}
|
||||
|
||||
int inlineCall() => rp.make().m();
|
||||
|
||||
int untypedCall() {
|
||||
final x = rp.untyped();
|
||||
return x.m();
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/lib/mod.dart
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/lib/mod.dart
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class C {
|
||||
int m() => 1;
|
||||
}
|
||||
|
||||
class Other {
|
||||
int m() => 2;
|
||||
}
|
||||
|
||||
C make() => C();
|
||||
|
||||
untyped() => C();
|
||||
1
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/pubspec.yaml
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/dart-prefixed-factory-return/pubspec.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
name: prefixed_factory_return
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
getRelationships,
|
||||
getNodesByLabel,
|
||||
getNodesByLabelFull,
|
||||
getResolutionOutcomes,
|
||||
edgeSet,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
|
|
@ -149,6 +150,45 @@ describe.skipIf(!dartAvailable)('Dart field-type resolution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Prefixed-import factory results (#3498) ─────────────────────────────
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart prefixed-import factory result types (#3498)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'dart-prefixed-factory-return'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
const methodCalls = (source: string) =>
|
||||
getRelationships(result, 'CALLS')
|
||||
.filter((edge) => edge.source === source && edge.target === 'm')
|
||||
.map((edge) => edge.rel.targetId);
|
||||
|
||||
it.each(['finalCall', 'varCall', 'inlineCall'])('resolves %s to C.m', (source) => {
|
||||
expect(methodCalls(source)).toEqual(['Method:lib/mod.dart:C.m#0']);
|
||||
});
|
||||
|
||||
it('does not guess a receiver type for an untyped factory result', () => {
|
||||
expect(methodCalls('untypedCall')).toEqual([]);
|
||||
});
|
||||
|
||||
it('records the untyped factory result as a dropped site', () => {
|
||||
expect(
|
||||
getResolutionOutcomes(result)
|
||||
.filter(
|
||||
(outcome) =>
|
||||
outcome.kind === 'suppressed' &&
|
||||
outcome.reason === 'receiver-unresolved' &&
|
||||
outcome.filePath === 'lib/callers.dart',
|
||||
)
|
||||
.map((outcome) => `${outcome.range.startLine}:${outcome.name}`),
|
||||
).toEqual(['17:m']);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 9: Call-result binding ────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart call-result binding', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue