mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
feat(python): strip nullable unions + prefer annotations over inference
Two linked changes that together fix the 4 nullable-receiver tests:
1. `stripNullable` in Python's `interpretTypeBinding` unwraps `User | None`,
`None | User`, and `Optional[User]` to `User`, so receiver-typed
resolution treats nullable receivers identically to non-nullable ones.
Three-arm unions (`User | Error | None`) are left unchanged — truly
ambiguous for single-receiver inference.
2. Source-strength ordering in `pass4CollectTypeBindings`. When multiple
matches fire for the same bound name in the same scope — e.g. the
`u: User = find()` idiom where both the annotation and
constructor-inferred patterns match — the explicit annotation now
wins regardless of query-match arrival order. Rank:
explicit (annotation / parameter-annotation / return-annotation / self) > inferred
Also reorders the two Python patterns in query.ts / scopes.scm so the
constructor-inferred pattern appears first — a belt-and-braces fallback
that keeps behavior deterministic if the shared priority ranking is ever
revisited.
Fixes 4 failures (flag-on 63 → 59):
- Python nullable receiver resolution (4 tests)
Flag-off regression check: 191/191 still pass.
This commit is contained in:
parent
2275a1ce97
commit
1a7d1d150f
4 changed files with 96 additions and 24 deletions
|
|
@ -100,8 +100,11 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
if (nameCap === undefined || typeCap === undefined) return null;
|
||||
|
||||
// Strip surrounding quotes for PEP 484 forward references:
|
||||
// `def f(x: "User")`.
|
||||
const rawType = stripForwardRefQuotes(typeCap.text.trim());
|
||||
// `def f(x: "User")`. Then unwrap nullable unions — `User | None`,
|
||||
// `None | User`, `Optional[User]` — to the concrete class name so
|
||||
// receiver-typed resolution treats nullable receivers identically to
|
||||
// non-nullable ones.
|
||||
const rawType = stripNullable(stripForwardRefQuotes(typeCap.text.trim()));
|
||||
|
||||
// Order matters: more specific anchor captures take precedence. `self`
|
||||
// and `cls` are synthesized with their own marker captures; the SCM
|
||||
|
|
@ -129,3 +132,28 @@ function stripForwardRefQuotes(text: string): string {
|
|||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap nullable type annotations so downstream resolution treats
|
||||
* `User | None`, `None | User`, and `Optional[User]` identically to
|
||||
* `User`. A missing/unknown variant returns the input unchanged.
|
||||
*
|
||||
* This is a syntactic strip, not a semantic parse — it handles the
|
||||
* canonical PEP-604 and `typing.Optional` shapes that cover the
|
||||
* overwhelming majority of real-world Python annotations and punts on
|
||||
* exotic unions (e.g. `User | Error`, which is ambiguous and should not
|
||||
* auto-bind to one arm).
|
||||
*/
|
||||
function stripNullable(text: string): string {
|
||||
// `Optional[X]` / `typing.Optional[X]` / `t.Optional[X]`
|
||||
const optMatch = text.match(/^(?:[A-Za-z_][A-Za-z0-9_]*\.)?Optional\[(.+)\]$/);
|
||||
if (optMatch !== null) return optMatch[1].trim();
|
||||
|
||||
// Binary union forms. A three-arm or larger union (`User | None | Error`)
|
||||
// is ambiguous for single-receiver inference, so we leave it alone.
|
||||
const parts = text.split('|').map((p) => p.trim());
|
||||
if (parts.length !== 2) return text;
|
||||
if (parts[0] === 'None') return parts[1];
|
||||
if (parts[1] === 'None') return parts[0];
|
||||
return text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,17 +48,21 @@ export const PYTHON_SCOPE_QUERY = `
|
|||
name: (identifier) @type-binding.name
|
||||
type: (type) @type-binding.type) @type-binding.parameter
|
||||
|
||||
;; Type bindings (variable annotations: \`u: User\` / \`u: User = x\`)
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
type: (type) @type-binding.type) @type-binding.annotation
|
||||
|
||||
;; Type bindings (constructor-inferred: \`u = User(...)\`)
|
||||
;; Listed BEFORE the annotation pattern so \`u: User = find()\` — which
|
||||
;; matches BOTH patterns — has the annotation (later match) overwrite
|
||||
;; the constructor-inferred guess (earlier match) in the typeBindings
|
||||
;; map. Explicit user intent beats inference.
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call
|
||||
function: (identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings (variable annotations: \`u: User\` / \`u: User = x\`)
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
type: (type) @type-binding.type) @type-binding.annotation
|
||||
|
||||
;; References — calls
|
||||
(call
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
|
|
|||
|
|
@ -91,6 +91,26 @@
|
|||
name: (identifier) @type-binding.name
|
||||
type: (type) @type-binding.type) @type-binding.parameter
|
||||
|
||||
; ─── Type bindings: constructor-inferred assignments ───────────────────────
|
||||
;
|
||||
; `u = User("alice")` — `u`'s type is inferred from the RHS call's target.
|
||||
; Python has no `new` keyword, so the pattern matches any `assignment`
|
||||
; whose RHS is a `call` with a bare-identifier function (constructor-
|
||||
; shaped). The registry resolves the raw name through the scope chain at
|
||||
; lookup time, so imported classes, local classes, and aliased imports
|
||||
; all work without query-time knowledge.
|
||||
;
|
||||
; Emits `source: 'constructor-inferred'`.
|
||||
;
|
||||
; Listed BEFORE the annotation pattern so `u: User = find()` — which
|
||||
; matches both patterns — has the annotation (later-processed match)
|
||||
; overwrite the constructor-inferred guess. Explicit user intent wins.
|
||||
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call
|
||||
function: (identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
; ─── Type bindings: variable annotations ───────────────────────────────────
|
||||
;
|
||||
; `u: User` or `u: User = some_value` — `u` is explicitly annotated. Both
|
||||
|
|
@ -105,22 +125,6 @@
|
|||
left: (identifier) @type-binding.name
|
||||
type: (type) @type-binding.type) @type-binding.annotation
|
||||
|
||||
; ─── Type bindings: constructor-inferred assignments ───────────────────────
|
||||
;
|
||||
; `u = User("alice")` — `u`'s type is inferred from the RHS call's target.
|
||||
; Python has no `new` keyword, so the pattern matches any `assignment`
|
||||
; whose RHS is a `call` with a bare-identifier function (constructor-
|
||||
; shaped). The registry resolves the raw name through the scope chain at
|
||||
; lookup time, so imported classes, local classes, and aliased imports
|
||||
; all work without query-time knowledge.
|
||||
;
|
||||
; Emits `source: 'constructor-inferred'`.
|
||||
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
right: (call
|
||||
function: (identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
; ─── References: calls ─────────────────────────────────────────────────────
|
||||
;
|
||||
; Free call: `print(x)` — function is a bare identifier
|
||||
|
|
|
|||
|
|
@ -634,7 +634,43 @@ function pass4CollectTypeBindings(
|
|||
declaredAtScope: host.id,
|
||||
source: parsed.source,
|
||||
};
|
||||
host.typeBindings.set(parsed.boundName, typeRef);
|
||||
// Prefer stronger sources when multiple matches fire for the same
|
||||
// bound name in the same scope. Example: `u: User = find()` matches
|
||||
// both the annotation and constructor-inferred patterns; the explicit
|
||||
// annotation (stronger source) must win over the call-site guess
|
||||
// regardless of query-match arrival order.
|
||||
const existing = host.typeBindings.get(parsed.boundName);
|
||||
if (
|
||||
existing === undefined ||
|
||||
typeBindingStrength(typeRef.source) >= typeBindingStrength(existing.source)
|
||||
) {
|
||||
host.typeBindings.set(parsed.boundName, typeRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority ordering when multiple `TypeRef`s compete for the same bound
|
||||
* name in the same scope. Higher number wins; ties keep the later match
|
||||
* (last-write-wins preserves historical order within a tier).
|
||||
*
|
||||
* Rationale: explicit annotations always beat inferred ones because they
|
||||
* reflect user intent. `self`/`cls` are treated as strongly as annotations
|
||||
* because they are language-required receiver types.
|
||||
*/
|
||||
function typeBindingStrength(source: TypeRef['source']): number {
|
||||
switch (source) {
|
||||
case 'annotation':
|
||||
case 'parameter-annotation':
|
||||
case 'return-annotation':
|
||||
case 'self':
|
||||
return 2;
|
||||
case 'assignment-inferred':
|
||||
case 'constructor-inferred':
|
||||
case 'receiver-propagated':
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue