diff --git a/gitnexus/src/core/ingestion/languages/python/interpret.ts b/gitnexus/src/core/ingestion/languages/python/interpret.ts index 5d10bd219..c89121d5f 100644 --- a/gitnexus/src/core/ingestion/languages/python/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/python/interpret.ts @@ -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 }; } diff --git a/gitnexus/src/core/ingestion/languages/python/query.ts b/gitnexus/src/core/ingestion/languages/python/query.ts index 07dba26ac..d6ba56740 100644 --- a/gitnexus/src/core/ingestion/languages/python/query.ts +++ b/gitnexus/src/core/ingestion/languages/python/query.ts @@ -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[0], - PYTHON_SCOPE_QUERY, - ); + _query = new Parser.Query(Python as Parameters[0], PYTHON_SCOPE_QUERY); } return _query; } diff --git a/gitnexus/src/core/ingestion/languages/python/scopes.scm b/gitnexus/src/core/ingestion/languages/python/scopes.scm index b40f8faff..935a51e35 100644 --- a/gitnexus/src/core/ingestion/languages/python/scopes.scm +++ b/gitnexus/src/core/ingestion/languages/python/scopes.scm @@ -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