mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(dart): declare inference-typed class fields so they can be receivers
`var b = Outer();` produced no `@declaration.property` capture at all — no Property node, and nothing for the capture layer to hang a type binding on — so `b.inner()` could not type its receiver while the annotated twin `Outer b = Outer();` resolved fine (#2807). The gap was in the query, one layer below where the binding is emitted: both class-field patterns require a leading `(type_identifier)` or `(nullable_type)`, i.e. a WRITTEN type. Dart puts the keyword there instead for an inferred field, and spells it two ways — `inferred_type` for `var`, `final_builtin` for `final` and `late final`. Covering only `var` would have left the more idiomatic Dart style broken, so both are matched. With the field declared, the capture layer types it from the constructor its initializer calls, as `constructor-inferred` — the weakest source, and the annotated branch returns before it, so an annotated field is untouched. Only a direct construction is accepted (a bare identifier followed by a `selector` carrying an `argument_part`, the same shape `findDirectCallValue` accepts for locals); a literal, member call or await is left alone rather than guessed at. Note this is the LOCAL/field split that made the gap invisible: `emitVarTypeBinding` already handled `initialized_variable_definition`, but a class field is `declaration(<keyword>, initialized_identifier_list(initialized_identifier))`. `InferredField.run` now emits `Outer.inner`, matching its control. Dart's `var r; C() { r = Outer(); }` shape stays pinned as a known gap: Dart writes the field with no receiver prefix, so binding it means treating assignment to a bare identifier as a field write, indistinguishable from a constructor-local. Refs #2807 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8627541cf
commit
b5f30383c5
2 changed files with 74 additions and 0 deletions
|
|
@ -204,6 +204,19 @@ export function emitDartScopeCaptures(
|
|||
'@type-binding.name': syntheticCapture('@type-binding.name', propNode, fieldName),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', propNode, fieldType),
|
||||
});
|
||||
} else {
|
||||
// No written type, so the field's type comes from the constructor its
|
||||
// initializer calls (#2807). `constructor-inferred` is the weakest
|
||||
// source, and the annotated branch above already returned, so an
|
||||
// annotated field is untouched either way.
|
||||
const callee = dartFieldConstructorCallee(propNode);
|
||||
if (callee !== null) {
|
||||
out.push({
|
||||
'@type-binding.constructor': nodeToCapture('@type-binding.constructor', propNode),
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', propNode, fieldName),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', propNode, callee.text),
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -535,6 +548,46 @@ function findDirectCallValue(initVarDef: SyntaxNode): SyntaxNode | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callee identifier of a class field initialized by a direct constructor call —
|
||||
* `var b = Outer();` / `final b = Outer();` — or `null` for anything else.
|
||||
*
|
||||
* Dart spells a class field as `declaration(<keyword>, initialized_identifier_list(
|
||||
* initialized_identifier))`, NOT the `initialized_variable_definition` that
|
||||
* `emitVarTypeBinding` handles — that is the LOCAL form. So an unannotated field
|
||||
* had no type binding and could not act as a call receiver (#2807), even though
|
||||
* its annotated twin resolved fine.
|
||||
*
|
||||
* Accepts the same construction shape `findDirectCallValue` accepts for locals:
|
||||
* a bare identifier followed by a `selector` carrying an `argument_part`.
|
||||
* Anything else — a literal, a member call, an await — is left alone rather than
|
||||
* guessed at.
|
||||
*/
|
||||
function dartFieldConstructorCallee(propNode: SyntaxNode): SyntaxNode | null {
|
||||
const initialized = firstDescendantOfType(propNode, 'initialized_identifier');
|
||||
if (initialized === null) return null;
|
||||
// namedChild(0) is the field NAME; the initializer starts after it.
|
||||
const value = initialized.namedChild(1);
|
||||
if (value === null || value.type !== 'identifier') return null;
|
||||
const next = value.nextNamedSibling;
|
||||
if (next === null || next.type !== 'selector') return null;
|
||||
return next.namedChild(0)?.type === 'argument_part' ? value : null;
|
||||
}
|
||||
|
||||
/** First strict descendant of `type`, breadth-first, or `null`. */
|
||||
function firstDescendantOfType(root: SyntaxNode, type: string): SyntaxNode | null {
|
||||
const queue: SyntaxNode[] = [root];
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
if (node !== root && node.type === type) return node;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child !== null) queue.push(child);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function emitVarTypeBinding(initVarDef: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const nameNode = initVarDef.childForFieldName('name');
|
||||
if (nameNode === null) return;
|
||||
|
|
|
|||
|
|
@ -163,6 +163,27 @@ const DART_SCOPE_QUERY = `
|
|||
(initialized_identifier
|
||||
. (identifier) @declaration.name))) @declaration.property
|
||||
|
||||
; Inference-typed fields — \`var b = Outer();\`, \`final b = Outer();\`,
|
||||
; \`late final b = Outer();\`, \`static var b = Outer();\` (#2807). The two
|
||||
; patterns above require a written type, so a field whose type comes from its
|
||||
; initializer produced NO property declaration at all — no Property node, and
|
||||
; nothing for captures.ts to hang a type binding on, so \`b.inner()\` could not
|
||||
; resolve its receiver while the annotated twin resolved fine.
|
||||
;
|
||||
; Dart spells the keyword as \`inferred_type\` for \`var\` and \`final_builtin\`
|
||||
; for \`final\` / \`late final\`; both are class fields and both are idiomatic,
|
||||
; so covering only one would leave the more common Dart style broken.
|
||||
(declaration
|
||||
(inferred_type)
|
||||
(initialized_identifier_list
|
||||
(initialized_identifier
|
||||
. (identifier) @declaration.name))) @declaration.property
|
||||
(declaration
|
||||
(final_builtin)
|
||||
(initialized_identifier_list
|
||||
(initialized_identifier
|
||||
. (identifier) @declaration.name))) @declaration.property
|
||||
|
||||
; ── Declarations — closure bindings (#2693) ──────────────────────────────────
|
||||
; \`var f = (x) => x;\` binds a callable. Without a declaration the binding has
|
||||
; no SymbolDefinition, so callable-value-flow has nothing to attach its seed to
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue