mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
feat(python): chain type bindings + strip list[T] generic for for-loop
Adds two capture patterns and a shared transitive-closure pass that together handle Python's variable-aliasing and for-loop-over-typed- iterable patterns: 1. `(assignment left: (identifier) right: (identifier))` — `alias = u`. 2. `(for_statement left: (identifier) right: (identifier))` — `for u in users`. Both emit `@type-binding.alias` with the RHS identifier as rawName. The shared `pass4CollectTypeBindings` now runs a final transitive-closure walk that follows identifier-chain TypeRefs through the declaring scope and its ancestors (depth-capped, cycle-guarded) so `alias` ultimately points at the class type instead of another local variable name. Generic stripping in `interpret.ts` unwraps single-arg collection wrappers — `list[User]`, `set[User]`, `Iterable[User]`, etc. — to the element type. Multi-arg generics (`dict[str, User]`, `Callable[...]`) are left alone; their semantics aren't unambiguous. Fixes 8 failures (flag-on 57 → 49): - Python assignment chain propagation (4) - Python nullable + assignment chain (2) - Python walrus operator (:=) assignment chain (2) Flag-off still 191/191.
This commit is contained in:
parent
989669f6e3
commit
7104b8eac5
3 changed files with 94 additions and 2 deletions
|
|
@ -103,8 +103,10 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
// `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()));
|
||||
// non-nullable ones. Finally strip single-arg generic wrappers so
|
||||
// `list[User]` / `Iterable[User]` behave like `User` for iterable
|
||||
// for-loop chain propagation.
|
||||
const rawType = stripGeneric(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
|
||||
|
|
@ -119,6 +121,7 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
else if (captures['@type-binding.cls'] !== undefined) source = 'self';
|
||||
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';
|
||||
|
||||
return { boundName: nameCap.text, rawTypeName: rawType, source };
|
||||
}
|
||||
|
|
@ -133,6 +136,24 @@ function stripForwardRefQuotes(text: string): string {
|
|||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a single-arg generic collection wrapper — `list[User]`,
|
||||
* `set[User]`, `Iterable[User]`, `Sequence[User]`, `Iterator[User]`,
|
||||
* `Generator[User, ...]` — to its element type.
|
||||
*
|
||||
* Point: for-loop and cross-file chain propagation need the element
|
||||
* type, not the container. Multi-arg generics (`dict[str, User]`,
|
||||
* `Callable[[int], User]`) are left alone — the element semantics
|
||||
* aren't unambiguous and the scope-chain fallback handles them at
|
||||
* resolution time.
|
||||
*/
|
||||
function stripGeneric(text: string): string {
|
||||
const match = text.match(
|
||||
/^(?:[A-Za-z_][A-Za-z0-9_]*\.)?(?:list|List|set|Set|tuple|Tuple|Iterable|Iterator|Sequence|Generator|AsyncIterable|AsyncIterator)\[([^,\]]+)\]$/,
|
||||
);
|
||||
return match !== null ? match[1].trim() : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap nullable type annotations so downstream resolution treats
|
||||
* `User | None`, `None | User`, and `Optional[User]` identically to
|
||||
|
|
|
|||
|
|
@ -90,6 +90,24 @@ export const PYTHON_SCOPE_QUERY = `
|
|||
(dotted_name) @type-binding.type))
|
||||
(identifier) @type-binding.name) @type-binding.constructor
|
||||
|
||||
;; Assignment chain: \`alias = user\` — the new name inherits the
|
||||
;; RHS-identifier's type. The pattern emits a TypeRef whose rawName is
|
||||
;; the RHS identifier's text; the scope-extractor's post-pass follows
|
||||
;; the chain so \`alias\` ends up pointing at whatever type \`user\` has.
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
right: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; For-loop iterable of an already-typed variable:
|
||||
;; def f(users: list[User]):
|
||||
;; for u in users: # u: users (chained via post-pass)
|
||||
;; u.save()
|
||||
;; The chain post-pass resolves \`users\` → its own type \`User\` via
|
||||
;; the generic-arg stripping in \`interpret.ts\`.
|
||||
(for_statement
|
||||
left: (identifier) @type-binding.name
|
||||
right: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; Type bindings (variable annotations: \`u: User\` / \`u: User = x\`)
|
||||
(assignment
|
||||
left: (identifier) @type-binding.name
|
||||
|
|
|
|||
|
|
@ -647,6 +647,59 @@ function pass4CollectTypeBindings(
|
|||
host.typeBindings.set(parsed.boundName, typeRef);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transitive closure over identifier-chain type bindings ─────────
|
||||
// Captures like `(assignment left: (ident) right: (ident))` emit a
|
||||
// TypeRef whose `rawName` is the RHS identifier. When the RHS name is
|
||||
// itself a bound variable with a known type in the same scope (or a
|
||||
// parent scope), follow the chain so `alias` ultimately points at the
|
||||
// class type — not at another local variable name. Without this,
|
||||
// `resolveTypeRef` hits the chained name, sees it's a local Variable
|
||||
// (non-type kind), and strict-returns null.
|
||||
for (const draft of drafts) {
|
||||
for (const [name, ref] of draft.typeBindings) {
|
||||
const resolved = followChainedRef(ref, draftById);
|
||||
if (resolved !== ref) draft.typeBindings.set(name, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Max chain depth: practical programs rarely exceed 4-5 re-bindings;
|
||||
* the cap just prevents runaway loops when providers emit cycles. */
|
||||
const CHAIN_MAX_DEPTH = 16;
|
||||
|
||||
/**
|
||||
* Follow an identifier-chain TypeRef through successive typeBindings
|
||||
* lookups in the declaring scope and its ancestors. Returns the terminal
|
||||
* TypeRef (or the original if the chain dead-ends or cycles).
|
||||
*/
|
||||
function followChainedRef(start: TypeRef, draftById: ReadonlyMap<ScopeId, ScopeDraft>): TypeRef {
|
||||
let current = start;
|
||||
const visited = new Set<string>();
|
||||
for (let depth = 0; depth < CHAIN_MAX_DEPTH; depth++) {
|
||||
// A rawName containing a dot (`models.User`) goes through
|
||||
// `QualifiedNameIndex` at resolution time — don't follow it here.
|
||||
if (current.rawName.includes('.')) return current;
|
||||
|
||||
// Look up the current rawName in the declaring scope and walk up
|
||||
// the chain until we hit a scope that has a binding for it.
|
||||
let scopeId: ScopeId | null = current.declaredAtScope;
|
||||
let next: TypeRef | undefined;
|
||||
while (scopeId !== null) {
|
||||
const scope = draftById.get(scopeId);
|
||||
if (scope === undefined) break;
|
||||
next = scope.typeBindings.get(current.rawName);
|
||||
if (next !== undefined) break;
|
||||
scopeId = scope.parent;
|
||||
}
|
||||
|
||||
if (next === undefined) return current; // dead end — nothing to chain to
|
||||
if (next === current) return current; // self-ref
|
||||
if (visited.has(next.rawName)) return current; // cycle guard
|
||||
visited.add(next.rawName);
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue