mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(dart): type a field from a constructor assigned to it
`var r; C() { r = Outer(); }` bound nothing, so `r.inner()` had no receiver
type — the last inference-typed field shape still failing after the initializer
form was fixed (#2807).
Dart is the one language here that writes a field with NO receiver prefix, so
`r = Outer()` inside a constructor is syntactically identical to assigning a
constructor-local. That ambiguity is why this was initially left pinned — but
the field set IS knowable: the class body declares `var r`, which the
initializer fix already turned into a property declaration. So a bare name binds
exactly when Dart itself resolves it to the field: the enclosing class declares
it AND the enclosing body declares no local of that name. A `this.`-prefixed
write is unambiguous and needs neither test.
The shadowing case is asserted, not assumed: with a body-local `var s = Outer()`
in scope, the field stays unbound while the local still resolves on its own.
Binds `constructor-inferred` (weakest source, so an annotation still wins), and
only for a direct construction — an identifier followed by a `selector` carrying
an `argument_part`, the same shape accepted for locals. The narrow
`@type-binding.dart-field` marker drives the Class-scope hoist in
`dartBindingScopeFor`; gating on it rather than on `@type-binding.constructor`
at large is what keeps genuine locals in their own scope.
All three shapes now match their control: bare `r = Outer()`, `this.s = …`, and
a non-constructor `setUp()` assignment.
Refs #2807
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d34690857
commit
90c72e49f8
2 changed files with 114 additions and 0 deletions
|
|
@ -247,6 +247,7 @@ export function emitDartScopeCaptures(
|
|||
}
|
||||
if (node.type === 'class_definition') {
|
||||
emitHeritage(node, out);
|
||||
emitDartFieldAssignmentBindings(node, out);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'extension_declaration') {
|
||||
|
|
@ -603,6 +604,104 @@ function emitVarTypeBinding(initVarDef: SyntaxNode, out: CaptureMatch[]): void {
|
|||
|
||||
// ─── Heritage ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Type an inference-typed field from a constructor call ASSIGNED to it —
|
||||
* `var r; C() { r = Outer(); }` and `this.r = Outer();` (#2807).
|
||||
*
|
||||
* Dart is the one language here that writes a field with NO receiver prefix, so
|
||||
* `r = Outer()` is syntactically identical to assigning a constructor-local. The
|
||||
* discriminator is the class's own declared field set: a bare name binds only
|
||||
* when the enclosing class declares it AND the enclosing function body does not
|
||||
* declare a local of the same name, which is exactly when Dart itself resolves
|
||||
* `r` to the field. A `this.`-prefixed write is unambiguous and needs neither
|
||||
* test.
|
||||
*
|
||||
* Emitted as `constructor-inferred`, the weakest source, so a field that also
|
||||
* carries an annotation keeps it. The narrow `@type-binding.dart-field` marker
|
||||
* rides the name node for `dartBindingScopeFor` to hoist on — the binding has to
|
||||
* land on the Class scope, since the assignment sits inside a constructor's own
|
||||
* Function scope where `typeOfMemberOnClass` never looks.
|
||||
*/
|
||||
function emitDartFieldAssignmentBindings(classNode: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const body = classNode.namedChildren.find((c) => c !== null && c.type === 'class_body');
|
||||
if (body === undefined || body === null) return;
|
||||
|
||||
// Field names this class declares, from `declaration(... initialized_identifier)`.
|
||||
const fields = new Set<string>();
|
||||
for (const member of body.namedChildren) {
|
||||
if (member === null || member.type !== 'declaration') continue;
|
||||
const list = member.namedChildren.find((c) => c?.type === 'initialized_identifier_list');
|
||||
if (list === undefined || list === null) continue;
|
||||
for (const init of list.namedChildren) {
|
||||
if (init === null || init.type !== 'initialized_identifier') continue;
|
||||
const nameNode = init.namedChild(0);
|
||||
if (nameNode !== null && nameNode.type === 'identifier') fields.add(nameNode.text);
|
||||
}
|
||||
}
|
||||
if (fields.size === 0) return;
|
||||
|
||||
for (const member of body.namedChildren) {
|
||||
if (member === null || member.type !== 'function_body') continue;
|
||||
// Locals declared anywhere in this body shadow the field for the whole
|
||||
// body — Dart hoists a local's name over the enclosing scope, so a
|
||||
// conservative body-wide check is the right granularity here.
|
||||
const locals = new Set<string>();
|
||||
walkNamedTree(member, (n) => {
|
||||
if (n.type !== 'initialized_variable_definition') return;
|
||||
const nameNode = n.childForFieldName('name');
|
||||
if (nameNode !== null) locals.add(nameNode.text);
|
||||
});
|
||||
|
||||
walkNamedTree(member, (node) => {
|
||||
if (node.type !== 'assignment_expression') return;
|
||||
const target = node.namedChild(0);
|
||||
if (target === null || target.type !== 'assignable_expression') return;
|
||||
|
||||
const first = target.namedChild(0);
|
||||
if (first === null) return;
|
||||
let fieldNameNode: SyntaxNode | null = null;
|
||||
if (first.type === 'identifier' && target.namedChildCount === 1) {
|
||||
// Bare `r = …`: a field only when declared here and not shadowed.
|
||||
if (!fields.has(first.text) || locals.has(first.text)) return;
|
||||
fieldNameNode = first;
|
||||
} else if (first.type === 'this') {
|
||||
const selector = target.namedChild(1);
|
||||
if (selector === null || selector.type !== 'unconditional_assignable_selector') return;
|
||||
const nameNode = selector.namedChild(0);
|
||||
if (nameNode === null || nameNode.type !== 'identifier') return;
|
||||
fieldNameNode = nameNode;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (fieldNameNode === null) return;
|
||||
|
||||
// RHS must be a direct construction: `Outer()` is an identifier followed
|
||||
// by a `selector` carrying an `argument_part`. Anything else is left
|
||||
// alone rather than guessed at.
|
||||
const callee = node.namedChild(1);
|
||||
if (callee === null || callee.type !== 'identifier') return;
|
||||
const selector = node.namedChild(2);
|
||||
if (selector === null || selector.type !== 'selector') return;
|
||||
if (selector.namedChild(0)?.type !== 'argument_part') return;
|
||||
|
||||
out.push({
|
||||
'@type-binding.constructor': nodeToCapture('@type-binding.constructor', node),
|
||||
'@type-binding.dart-field': syntheticCapture(
|
||||
'@type-binding.dart-field',
|
||||
fieldNameNode,
|
||||
'1',
|
||||
),
|
||||
'@type-binding.name': syntheticCapture(
|
||||
'@type-binding.name',
|
||||
fieldNameNode,
|
||||
fieldNameNode.text,
|
||||
),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', callee, callee.text),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function emitHeritage(classNode: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const nameNode = classNode.childForFieldName('name');
|
||||
if (nameNode === null) return;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,21 @@ export function dartBindingScopeFor(
|
|||
return null;
|
||||
}
|
||||
|
||||
// (1b) A field typed from a constructor assigned to it (`r = Outer();` in a
|
||||
// constructor body) must live on the CLASS scope — the assignment sits inside
|
||||
// the constructor's own Function scope, where `typeOfMemberOnClass` never
|
||||
// looks (#2807). Gated on the dedicated marker, never on
|
||||
// `@type-binding.constructor` at large, which also fires for genuine locals.
|
||||
if (decl['@type-binding.dart-field'] !== undefined) {
|
||||
let cur: Scope | undefined = innermost;
|
||||
while (cur !== undefined) {
|
||||
if (cur.kind === 'Class') return cur.id;
|
||||
if (cur.parent === null) break;
|
||||
cur = tree.getScope(cur.parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// (2) Function/method/constructor names are visible in the enclosing scope.
|
||||
if (
|
||||
decl['@declaration.function'] !== undefined ||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue