feat(python): capture constructor-inferred + annotated type bindings

Extends the Python scope-extractor with two new type-binding capture
patterns so receiver-typed method dispatch has concrete type bindings
to work from:

1. `u: User = ...` / `u: User` — variable annotations. `@type-binding.annotation`
   anchor, `source: 'annotation'`.
2. `u = User("alice")` — assignment RHS is a bare-identifier call (Python
   has no `new` keyword; constructor-shaped calls are syntactically
   identical to function calls). `@type-binding.constructor` anchor,
   `source: 'constructor-inferred'`.

The runtime query lives in `query.ts` (the `.scm` file is documentation
per the comment at its top); both are updated.

Fixes 19 failures across these resolver fixtures (flag-on 82 → 63):
- Python constructor-inferred type resolution (3)
- Python class-level annotation resolution (3)
- Python nullable receiver resolution (3)
- Python member-call / receiver-constrained / constructor-call (3)
- Python assignment chain propagation (2)
- Python walrus / match-case / chained method (3)
- Python member access iterable for-loop (2)
This commit is contained in:
Gergo Magyar 2026-04-19 12:46:19 +01:00
parent 8a7d42c1fd
commit 2275a1ce97
3 changed files with 51 additions and 10 deletions

View file

@ -11,12 +11,7 @@
* already attached) so these functions are straight-line tag readers.
*/
import type {
CaptureMatch,
ParsedImport,
ParsedTypeBinding,
TypeRef,
} from 'gitnexus-shared';
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
// ─── interpretImport ──────────────────────────────────────────────────────
@ -108,11 +103,19 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi
// `def f(x: "User")`.
const rawType = stripForwardRefQuotes(typeCap.text.trim());
// Order matters: more specific anchor captures take precedence. `self`
// and `cls` are synthesized with their own marker captures; the SCM
// anchor topic captures (`@type-binding.parameter`,
// `@type-binding.annotation`, `@type-binding.constructor`) distinguish
// the variable-annotation and constructor-inferred forms from the
// classic parameter annotation.
let source: TypeRef['source'] = 'parameter-annotation';
if (captures['@type-binding.self'] !== undefined) source = 'self';
// `cls` is a self-like receiver; share the source label so downstream
// `Registry.lookup` Step 2 treats them identically.
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';
return { boundName: nameCap.text, rawTypeName: rawType, source };
}

View file

@ -48,6 +48,17 @@ 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(...)\`)
(assignment
left: (identifier) @type-binding.name
right: (call
function: (identifier) @type-binding.type)) @type-binding.constructor
;; References calls
(call
function: (identifier) @reference.name) @reference.call.free
@ -71,10 +82,7 @@ export function getPythonParser(): Parser {
export function getPythonScopeQuery(): Parser.Query {
if (_query === null) {
_query = new Parser.Query(
Python as Parameters<Parser['setLanguage']>[0],
PYTHON_SCOPE_QUERY,
);
_query = new Parser.Query(Python as Parameters<Parser['setLanguage']>[0], PYTHON_SCOPE_QUERY);
}
return _query;
}

View file

@ -91,6 +91,36 @@
name: (identifier) @type-binding.name
type: (type) @type-binding.type) @type-binding.parameter
; ─── Type bindings: variable annotations ───────────────────────────────────
;
; `u: User` or `u: User = some_value` — `u` is explicitly annotated. Both
; forms parse under tree-sitter-python as `(assignment left: type:)` with
; an optional `right:`. Module-, class-, and function-scope annotations
; all land here; scope attachment is handled by the central extractor
; via the anchor's innermost-containing scope.
;
; Emits `source: 'annotation'`.
(assignment
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