diff --git a/gitnexus/src/core/ingestion/languages/python/interpret.ts b/gitnexus/src/core/ingestion/languages/python/interpret.ts index ef7e7f048..1d98ee5e3 100644 --- a/gitnexus/src/core/ingestion/languages/python/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/python/interpret.ts @@ -149,10 +149,22 @@ function stripForwardRefQuotes(text: string): string { * resolution time. */ function stripGeneric(text: string): string { - const match = text.match( + const single = 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; + if (single !== null) return single[1].trim(); + // dict[K, V] / Dict[K, V] / Mapping[K, V] — strip to value type V. + // For-loop destructuring of `for k, v in d.items()` binds `v` to + // `d`; the chain-follow then unwraps the dict annotation to V. + // Single-key dict `dict[K]` is not legal Python, so two args is the + // only shape worth handling. Match a top-level K up to the first + // comma and a V to the closing bracket; nested generics in V (e.g. + // `dict[str, list[User]]`) are left for a downstream strip pass. + const dict = text.match( + /^(?:[A-Za-z_][A-Za-z0-9_]*\.)?(?:dict|Dict|Mapping|MutableMapping|OrderedDict|DefaultDict)\[[^,\]]+,\s*([^\]]+)\]$/, + ); + if (dict !== null) return dict[1].trim(); + return text; } /** diff --git a/gitnexus/src/core/ingestion/languages/python/query.ts b/gitnexus/src/core/ingestion/languages/python/query.ts index 87f2eee39..b5f7a0d0e 100644 --- a/gitnexus/src/core/ingestion/languages/python/query.ts +++ b/gitnexus/src/core/ingestion/languages/python/query.ts @@ -144,6 +144,29 @@ export const PYTHON_SCOPE_QUERY = ` (identifier) @type-binding.type)) (#eq? @_enum "enumerate")) @type-binding.alias +;; for k, v in d.items() — bind v to d. The chain-follow unwraps d's +;; dict[K, V] annotation to V via the dict-aware stripGeneric in +;; interpret.ts. Covers both pattern_list and tuple_pattern shapes. +(for_statement + left: (pattern_list + (identifier) + (identifier) @type-binding.name) + right: (call + function: (attribute + object: (identifier) @type-binding.type + attribute: (identifier) @_items)) + (#eq? @_items "items")) @type-binding.alias + +(for_statement + left: (tuple_pattern + (identifier) + (identifier) @type-binding.name) + right: (call + function: (attribute + object: (identifier) @type-binding.type + attribute: (identifier) @_items)) + (#eq? @_items "items")) @type-binding.alias + ;; Type bindings (variable annotations: \`u: User\` / \`u: User = x\`) (assignment left: (identifier) @type-binding.name diff --git a/gitnexus/src/core/ingestion/languages/python/scopes.scm b/gitnexus/src/core/ingestion/languages/python/scopes.scm index 944cab3d6..f30907819 100644 --- a/gitnexus/src/core/ingestion/languages/python/scopes.scm +++ b/gitnexus/src/core/ingestion/languages/python/scopes.scm @@ -158,6 +158,28 @@ (identifier) @type-binding.type)) (#eq? @_enum "enumerate")) @type-binding.alias +; for k, v in d.items() — bind v to d. The chain-follow unwraps d's +; dict[K, V] annotation to V via the dict-aware stripGeneric. +(for_statement + left: (pattern_list + (identifier) + (identifier) @type-binding.name) + right: (call + function: (attribute + object: (identifier) @type-binding.type + attribute: (identifier) @_items)) + (#eq? @_items "items")) @type-binding.alias + +(for_statement + left: (tuple_pattern + (identifier) + (identifier) @type-binding.name) + right: (call + function: (attribute + object: (identifier) @type-binding.type + attribute: (identifier) @_items)) + (#eq? @_items "items")) @type-binding.alias + ; ─── Type bindings: function return-type annotations ───────────────────── ; ; `def get_user() -> User:` — binds the function's NAME to its return