diff --git a/gitnexus/src/core/ingestion/languages/python/interpret.ts b/gitnexus/src/core/ingestion/languages/python/interpret.ts index a8fa3019e..ef7e7f048 100644 --- a/gitnexus/src/core/ingestion/languages/python/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/python/interpret.ts @@ -122,6 +122,7 @@ export function interpretPythonTypeBinding(captures: CaptureMatch): ParsedTypeBi 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'; + else if (captures['@type-binding.return'] !== undefined) source = 'return-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 4c61b7e28..abfd43766 100644 --- a/gitnexus/src/core/ingestion/languages/python/query.ts +++ b/gitnexus/src/core/ingestion/languages/python/query.ts @@ -113,6 +113,16 @@ export const PYTHON_SCOPE_QUERY = ` left: (identifier) @type-binding.name type: (type) @type-binding.type) @type-binding.annotation +;; Return-type annotation: \`def get_user() -> User:\` binds the +;; FUNCTION'S NAME to its return type in the enclosing scope. Combined +;; with the constructor-inferred + chain-follow path, \`u = get_user()\` +;; then resolves \`u: User\` cross-call. The Python provider hoists the +;; binding via \`pythonBindingScopeFor\` to the function's parent scope +;; so callers in module/class scope see it. +(function_definition + name: (identifier) @type-binding.name + return_type: (type) @type-binding.type) @type-binding.return + ;; References — calls (call function: (identifier) @reference.name) @reference.call.free diff --git a/gitnexus/src/core/ingestion/languages/python/scopes.scm b/gitnexus/src/core/ingestion/languages/python/scopes.scm index 553e32ddf..c92d0ee67 100644 --- a/gitnexus/src/core/ingestion/languages/python/scopes.scm +++ b/gitnexus/src/core/ingestion/languages/python/scopes.scm @@ -125,6 +125,18 @@ left: (identifier) @type-binding.name type: (type) @type-binding.type) @type-binding.annotation +; ─── Type bindings: function return-type annotations ───────────────────── +; +; `def get_user() -> User:` — binds the function's NAME to its return +; type in the enclosing scope. Combined with the constructor-inferred + +; chain-follow path, `u = get_user()` then resolves `u: User` cross- +; call. Python provider hoists the binding via `pythonBindingScopeFor` +; to the function's parent scope so callers in module/class scope see it. + +(function_definition + name: (identifier) @type-binding.name + return_type: (type) @type-binding.type) @type-binding.return + ; ─── References: calls ───────────────────────────────────────────────────── ; ; Free call: `print(x)` — function is a bare identifier diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 172c6f769..071a162d2 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -666,9 +666,19 @@ function pass4CollectTypeBindings( const innermost = draftById.get(innermostId); if (innermost === undefined) continue; + // Auto-hoist for scope-creating type bindings (e.g. Python's + // `@type-binding.return` whose anchor is the function_definition + // itself). Same condition as Pass 2 — when the anchor coincides + // with the innermost scope's range, the binding belongs in the + // enclosing scope (callers, not the function body, look up the + // return type by the function's name). + const autoHostedId = + innermost.parent !== null && rangesEqual(anchor.range, innermost.range) + ? innermost.parent + : innermost.id; // `bindingScopeFor` may hoist the type binding to an outer scope. const hostId = - provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? innermost.id; + provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId; const host = draftById.get(hostId) ?? innermost; const typeRef: TypeRef = {