mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(python): infer an instance field's type from the constructor it calls
`self.outer = Outer()` in `__init__` bound nothing, so `self.outer.inner()` had no receiver type and the fold declined the whole chain — the Python half of #2807. An annotated field (`self.outer: Outer = ...`) or one assigned from an annotated parameter already worked. `synthesizeConstructorFieldTypeBindings` deliberately refused to infer "from arbitrary unannotated RHS expressions ... not a name-only guess". A CALL is not that: Python has no `new`, so a call to a plain (or dotted) name is the only syntactic construction form there is, and it is the same positive evidence every other language reads from `= new X()`. A bare name, subscript, await or comprehension is still refused. Adds it as a THIRD and weakest tier. The existing explicit/parameter boolean becomes a rank, so precedence is now explicit annotation > parameter annotation > construction, and a later same-tier assignment still wins (the last write in `__init__` is the live one). `interpretPythonTypeBinding` maps the new marker to `constructor-inferred` (strength 1) — checked before the parameter branch, which would otherwise have read the absent parameter marker as `annotation` and promoted a guess to the strongest tier. The Class-scope hoist needed no change: `@type-binding.instance-field` already carries it in `pythonBindingScopeFor`. Measured: `AssignedField.run` now emits `Outer.inner`, exact parity with the annotated-field and local-const rows. Refs #2807 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18a48c7313
commit
6c99f5488c
2 changed files with 85 additions and 19 deletions
|
|
@ -120,8 +120,16 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
// `Registry.lookup` Step 2 treats them identically.
|
||||
else if (captures['@type-binding.cls'] !== undefined) source = 'self';
|
||||
else if (captures['@type-binding.instance-field'] !== undefined) {
|
||||
source =
|
||||
captures['@type-binding.parameter'] !== undefined ? 'parameter-annotation' : 'annotation';
|
||||
// Three tiers, weakest last: `self.x = Outer()` is inferred from a
|
||||
// CONSTRUCTOR CALL (#2807) and must stay below both an explicit field
|
||||
// annotation and a parameter annotation, so a class that both annotates
|
||||
// the field and constructs it keeps the annotation. Checked before the
|
||||
// parameter branch because the constructor form carries no
|
||||
// `@type-binding.parameter` marker and would otherwise read as 'annotation'.
|
||||
if (captures['@type-binding.constructor'] !== undefined) source = 'constructor-inferred';
|
||||
else
|
||||
source =
|
||||
captures['@type-binding.parameter'] !== undefined ? 'parameter-annotation' : 'annotation';
|
||||
} else if (captures['@type-binding.constructor'] !== undefined) source = 'constructor-inferred';
|
||||
else if (captures['@type-binding.annotation'] !== undefined) source = 'annotation';
|
||||
else if (captures['@type-binding.alias'] !== undefined) source = 'assignment-inferred';
|
||||
|
|
|
|||
|
|
@ -114,17 +114,40 @@ export function synthesizeReceiverTypeBinding(fnNode: SyntaxNode): CaptureMatch
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The class name of a direct constructor call — `Outer()` or `pkg.Outer()` —
|
||||
* or `undefined` for anything else. Python has no `new`, so a call is the only
|
||||
* syntactic construction form, and a call to a plain name is the shape every
|
||||
* other language spells `= new X()`.
|
||||
*
|
||||
* A dotted callee keeps its full text: `resolveTypeRef` sends dotted names
|
||||
* through `QualifiedNameIndex`, exactly as the module-level
|
||||
* `u = models.User()` pattern in `query.ts` already relies on. Any other RHS
|
||||
* (subscript, await, comprehension, bare name) is left alone — that is the
|
||||
* "name-only guess" this module deliberately refuses.
|
||||
*/
|
||||
function constructorCallTypeName(right: SyntaxNode | null): string | undefined {
|
||||
if (right === null || right.type !== 'call') return undefined;
|
||||
const callee = right.childForFieldName('function');
|
||||
if (callee === null) return undefined;
|
||||
if (callee.type !== 'identifier' && callee.type !== 'attribute') return undefined;
|
||||
const text = callee.text.trim();
|
||||
return text.length > 0 ? text : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize class-scope field bindings for the common Python constructor
|
||||
* injection pattern:
|
||||
* injection patterns:
|
||||
*
|
||||
* def __init__(self, service: Service):
|
||||
* self.service = service
|
||||
* self.service = service # from the PARAMETER's annotation
|
||||
* self.cache: Cache = build() # from the FIELD's own annotation
|
||||
* self.outer = Outer() # from the CONSTRUCTOR called (#2807)
|
||||
*
|
||||
* An explicit field annotation (`self.service: Service = ...`) is also
|
||||
* accepted and takes precedence over a parameter annotation. Deliberately do
|
||||
* not infer from arbitrary unannotated RHS expressions: the receiver resolver
|
||||
* needs a declared type, not a name-only guess.
|
||||
* The three tiers rank in that order — an explicit field annotation beats a
|
||||
* parameter annotation, which beats a construction. Anything else is still
|
||||
* refused: the receiver resolver needs positive evidence, not a name-only
|
||||
* guess from an arbitrary RHS.
|
||||
*/
|
||||
export function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): CaptureMatch[] {
|
||||
if (fnNode.childForFieldName('name')?.text !== '__init__') return [];
|
||||
|
|
@ -148,7 +171,14 @@ export function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): Capt
|
|||
if (name !== null && annotation !== null) parameterTypes.set(name, annotation.text);
|
||||
}
|
||||
|
||||
type Candidate = { readonly match: CaptureMatch; readonly explicit: boolean };
|
||||
// Three evidence tiers for one field, strongest first. A later assignment of
|
||||
// the SAME tier still wins (last write in `__init__` is the live one), but a
|
||||
// weaker one never displaces a stronger: `self.x: Outer = make()` keeps its
|
||||
// annotation even if a later branch does `self.x = Other()`.
|
||||
const TIER_EXPLICIT = 2;
|
||||
const TIER_PARAMETER = 1;
|
||||
const TIER_CONSTRUCTOR = 0;
|
||||
type Candidate = { readonly match: CaptureMatch; readonly tier: number };
|
||||
const candidates = new Map<string, Candidate>();
|
||||
|
||||
const stack: SyntaxNode[] = [body];
|
||||
|
|
@ -178,13 +208,30 @@ export function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): Capt
|
|||
const explicitType = node.childForFieldName('type');
|
||||
const parameterType =
|
||||
right?.type === 'identifier' ? parameterTypes.get(right.text) : undefined;
|
||||
const typeName = explicitType?.text ?? parameterType;
|
||||
// `self.x = Outer()` — the type comes from the CONSTRUCTOR being
|
||||
// called (#2807). This is not the "arbitrary unannotated RHS" the
|
||||
// header warns against: a call to a name that resolves to a class is
|
||||
// the same positive evidence every other language reads from
|
||||
// `= new X()`, and without it an unannotated instance field could
|
||||
// never act as a call receiver at all. Weakest of the three tiers, so
|
||||
// an explicit annotation or a parameter annotation still wins.
|
||||
const constructedType =
|
||||
explicitType === null && parameterType === undefined
|
||||
? constructorCallTypeName(right)
|
||||
: undefined;
|
||||
const typeName = explicitType?.text ?? parameterType ?? constructedType;
|
||||
if (typeName !== undefined) {
|
||||
const explicit = explicitType !== null;
|
||||
const inferredFromConstructor = constructedType !== undefined;
|
||||
const tier = explicit
|
||||
? TIER_EXPLICIT
|
||||
: inferredFromConstructor
|
||||
? TIER_CONSTRUCTOR
|
||||
: TIER_PARAMETER;
|
||||
const existing = candidates.get(field.text);
|
||||
if (existing === undefined || explicit || !existing.explicit) {
|
||||
if (existing === undefined || tier >= existing.tier) {
|
||||
candidates.set(field.text, {
|
||||
explicit,
|
||||
tier,
|
||||
match: {
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', field, field.text),
|
||||
'@type-binding.type': syntheticCapture(
|
||||
|
|
@ -192,15 +239,26 @@ export function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): Capt
|
|||
explicitType ?? right ?? field,
|
||||
typeName,
|
||||
),
|
||||
// The marker that tells `interpretPythonTypeBinding` which
|
||||
// tier this is; an explicit annotation carries neither and
|
||||
// reads as `annotation`.
|
||||
...(explicit
|
||||
? {}
|
||||
: {
|
||||
'@type-binding.parameter': syntheticCapture(
|
||||
'@type-binding.parameter',
|
||||
right ?? field,
|
||||
'1',
|
||||
),
|
||||
}),
|
||||
: inferredFromConstructor
|
||||
? {
|
||||
'@type-binding.constructor': syntheticCapture(
|
||||
'@type-binding.constructor',
|
||||
right ?? field,
|
||||
'1',
|
||||
),
|
||||
}
|
||||
: {
|
||||
'@type-binding.parameter': syntheticCapture(
|
||||
'@type-binding.parameter',
|
||||
right ?? field,
|
||||
'1',
|
||||
),
|
||||
}),
|
||||
'@type-binding.instance-field': syntheticCapture(
|
||||
'@type-binding.instance-field',
|
||||
node,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue