gitnexus 1.6.10-rc.87 → 1.6.10-rc.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,9 @@
8
8
  * 1. **Per-name import statements** — `import a, b` and
9
9
  * `from m import x, y` decompose to one match per imported name
10
10
  * (see `import-decomposer.ts`).
11
- * 2. **Receiver type bindings** — each `function_definition` inside a
12
- * class body emits a `@type-binding.self` (or `@type-binding.cls`
13
- * for `@classmethod`) capture so Pass-4 attaches the implicit
14
- * receiver (see `receiver-binding.ts`).
11
+ * 2. **Receiver type bindings** — methods emit an implicit `self` / `cls`
12
+ * binding, and `__init__` assignments from annotated parameters emit
13
+ * class-scoped instance-field bindings (see `receiver-binding.ts`).
15
14
  *
16
15
  * Pure given the input source text. No I/O, no globals consulted.
17
16
  */
@@ -8,17 +8,16 @@
8
8
  * 1. **Per-name import statements** — `import a, b` and
9
9
  * `from m import x, y` decompose to one match per imported name
10
10
  * (see `import-decomposer.ts`).
11
- * 2. **Receiver type bindings** — each `function_definition` inside a
12
- * class body emits a `@type-binding.self` (or `@type-binding.cls`
13
- * for `@classmethod`) capture so Pass-4 attaches the implicit
14
- * receiver (see `receiver-binding.ts`).
11
+ * 2. **Receiver type bindings** — methods emit an implicit `self` / `cls`
12
+ * binding, and `__init__` assignments from annotated parameters emit
13
+ * class-scoped instance-field bindings (see `receiver-binding.ts`).
15
14
  *
16
15
  * Pure given the input source text. No I/O, no globals consulted.
17
16
  */
18
17
  import { nodeToCapture, syntheticCapture, walkNamedTree, } from '../../utils/ast-helpers.js';
19
18
  import { splitImportStatement } from './import-decomposer.js';
20
19
  import { getPythonParser, getPythonScopeQuery } from './query.js';
21
- import { synthesizeReceiverTypeBinding } from './receiver-binding.js';
20
+ import { synthesizeConstructorFieldTypeBindings, synthesizeReceiverTypeBinding, } from './receiver-binding.js';
22
21
  import { synthesizeDependsReferences } from './depends-references.js';
23
22
  import { computePythonArityMetadata } from './arity-metadata.js';
24
23
  import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
@@ -122,6 +121,7 @@ export function emitPythonScopeCaptures(sourceText, _filePath, cachedTree) {
122
121
  const synth = synthesizeReceiverTypeBinding(fnNode);
123
122
  if (synth !== null)
124
123
  out.push(synth);
124
+ out.push(...synthesizeConstructorFieldTypeBindings(fnNode));
125
125
  for (const depRef of synthesizeDependsReferences(fnNode))
126
126
  out.push(depRef);
127
127
  }
@@ -118,6 +118,10 @@ export function interpretPythonTypeBinding(captures) {
118
118
  // `Registry.lookup` Step 2 treats them identically.
119
119
  else if (captures['@type-binding.cls'] !== undefined)
120
120
  source = 'self';
121
+ else if (captures['@type-binding.instance-field'] !== undefined) {
122
+ source =
123
+ captures['@type-binding.parameter'] !== undefined ? 'parameter-annotation' : 'annotation';
124
+ }
121
125
  else if (captures['@type-binding.constructor'] !== undefined)
122
126
  source = 'constructor-inferred';
123
127
  else if (captures['@type-binding.annotation'] !== undefined)
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Synthesize `@type-binding.self` / `@type-binding.cls` captures for
3
- * methods.
2
+ * Synthesize implicit receiver and constructor-assigned field type bindings
3
+ * for methods.
4
4
  *
5
5
  * Tree-sitter can't easily express "the first parameter of a function
6
6
  * defined directly inside a class body" via a single static query.
@@ -19,3 +19,16 @@ import { type SyntaxNode } from '../../utils/ast-helpers.js';
19
19
  * 'function_definition'`.
20
20
  */
21
21
  export declare function synthesizeReceiverTypeBinding(fnNode: SyntaxNode): CaptureMatch | null;
22
+ /**
23
+ * Synthesize class-scope field bindings for the common Python constructor
24
+ * injection pattern:
25
+ *
26
+ * def __init__(self, service: Service):
27
+ * self.service = service
28
+ *
29
+ * An explicit field annotation (`self.service: Service = ...`) is also
30
+ * accepted and takes precedence over a parameter annotation. Deliberately do
31
+ * not infer from arbitrary unannotated RHS expressions: the receiver resolver
32
+ * needs a declared type, not a name-only guess.
33
+ */
34
+ export declare function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): CaptureMatch[];
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Synthesize `@type-binding.self` / `@type-binding.cls` captures for
3
- * methods.
2
+ * Synthesize implicit receiver and constructor-assigned field type bindings
3
+ * for methods.
4
4
  *
5
5
  * Tree-sitter can't easily express "the first parameter of a function
6
6
  * defined directly inside a class body" via a single static query.
@@ -114,3 +114,97 @@ export function synthesizeReceiverTypeBinding(fnNode) {
114
114
  '@type-binding.type': syntheticCapture('@type-binding.type', first, className),
115
115
  };
116
116
  }
117
+ /**
118
+ * Synthesize class-scope field bindings for the common Python constructor
119
+ * injection pattern:
120
+ *
121
+ * def __init__(self, service: Service):
122
+ * self.service = service
123
+ *
124
+ * An explicit field annotation (`self.service: Service = ...`) is also
125
+ * accepted and takes precedence over a parameter annotation. Deliberately do
126
+ * not infer from arbitrary unannotated RHS expressions: the receiver resolver
127
+ * needs a declared type, not a name-only guess.
128
+ */
129
+ export function synthesizeConstructorFieldTypeBindings(fnNode) {
130
+ if (fnNode.childForFieldName('name')?.text !== '__init__')
131
+ return [];
132
+ if (findEnclosingClassDefinition(fnNode) === null)
133
+ return [];
134
+ if (hasDecorator(fnNode, 'staticmethod') || hasDecorator(fnNode, 'classmethod'))
135
+ return [];
136
+ const receiver = synthesizeReceiverTypeBinding(fnNode);
137
+ const receiverName = receiver?.['@type-binding.self']?.text;
138
+ if (receiverName === undefined)
139
+ return [];
140
+ const parameters = fnNode.childForFieldName('parameters');
141
+ const body = fnNode.childForFieldName('body');
142
+ if (parameters === null || body === null)
143
+ return [];
144
+ const parameterTypes = new Map();
145
+ for (let i = 0; i < parameters.namedChildCount; i++) {
146
+ const parameter = parameters.namedChild(i);
147
+ if (parameter === null)
148
+ continue;
149
+ const name = firstParameterName(parameter);
150
+ const annotation = parameter.childForFieldName('type');
151
+ if (name !== null && annotation !== null)
152
+ parameterTypes.set(name, annotation.text);
153
+ }
154
+ const candidates = new Map();
155
+ const stack = [body];
156
+ while (stack.length > 0) {
157
+ const node = stack.pop();
158
+ if (node !== body &&
159
+ (node.type === 'function_definition' ||
160
+ node.type === 'lambda' ||
161
+ node.type === 'class_definition' ||
162
+ node.type === 'if_statement' ||
163
+ node.type === 'for_statement' ||
164
+ node.type === 'while_statement' ||
165
+ node.type === 'try_statement' ||
166
+ node.type === 'match_statement')) {
167
+ continue;
168
+ }
169
+ if (node.type === 'assignment') {
170
+ const left = node.childForFieldName('left');
171
+ const right = node.childForFieldName('right');
172
+ if (left?.type === 'attribute') {
173
+ const object = left.childForFieldName('object');
174
+ const field = left.childForFieldName('attribute');
175
+ if (object?.type === 'identifier' && object.text === receiverName && field !== null) {
176
+ const explicitType = node.childForFieldName('type');
177
+ const parameterType = right?.type === 'identifier' ? parameterTypes.get(right.text) : undefined;
178
+ const typeName = explicitType?.text ?? parameterType;
179
+ if (typeName !== undefined) {
180
+ const explicit = explicitType !== null;
181
+ const existing = candidates.get(field.text);
182
+ if (existing === undefined || explicit || !existing.explicit) {
183
+ candidates.set(field.text, {
184
+ explicit,
185
+ match: {
186
+ '@type-binding.name': syntheticCapture('@type-binding.name', field, field.text),
187
+ '@type-binding.type': syntheticCapture('@type-binding.type', explicitType ?? right ?? field, typeName),
188
+ ...(explicit
189
+ ? {}
190
+ : {
191
+ '@type-binding.parameter': syntheticCapture('@type-binding.parameter', right ?? field, '1'),
192
+ }),
193
+ '@type-binding.instance-field': syntheticCapture('@type-binding.instance-field', node, '1'),
194
+ },
195
+ });
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ // Push in reverse so the LIFO walk visits source order. That keeps Map
202
+ // insertion order (and therefore emitted capture order) deterministic.
203
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
204
+ const child = node.namedChild(i);
205
+ if (child !== null)
206
+ stack.push(child);
207
+ }
208
+ }
209
+ return [...candidates.values()].map(({ match }) => match);
210
+ }
@@ -9,11 +9,11 @@ import type { CaptureMatch, NodeLabel, ParsedImport, Scope, ScopeId, ScopeTree,
9
9
  import type { SyntaxNode } from 'tree-sitter';
10
10
  export declare function pythonFunctionDefinitionLabel(functionNode: SyntaxNode, defaultLabel: NodeLabel): NodeLabel;
11
11
  /** Python has no block scope, so the central extractor's "innermost
12
- * enclosing scope" default is already correct: `for x in …` creates
13
- * `x` in the enclosing function/module scope (because we never emit a
14
- * `@scope.block` for the for-loop body), comprehension variables stay
15
- * in their expression context, etc. Returns `null` to delegate. */
16
- export declare function pythonBindingScopeFor(_decl: CaptureMatch, _innermost: Scope, _tree: ScopeTree): ScopeId | null;
12
+ * enclosing scope" default is already correct for ordinary bindings.
13
+ * Constructor-injected instance fields are the exception: their marker is
14
+ * anchored inside `__init__`, but compound receiver resolution needs the
15
+ * field type on the enclosing Class scope. */
16
+ export declare function pythonBindingScopeFor(decl: CaptureMatch, innermost: Scope, tree: ScopeTree): ScopeId | null;
17
17
  /** Function-local `from x import Y` should attach the binding to the
18
18
  * function scope, not the module. Class-body imports (rare but legal —
19
19
  * `class A: import x` makes `x` a class attribute) attach to the class.
@@ -15,11 +15,21 @@ export function pythonFunctionDefinitionLabel(functionNode, defaultLabel) {
15
15
  }
16
16
  // ─── bindingScopeFor ──────────────────────────────────────────────────────
17
17
  /** Python has no block scope, so the central extractor's "innermost
18
- * enclosing scope" default is already correct: `for x in …` creates
19
- * `x` in the enclosing function/module scope (because we never emit a
20
- * `@scope.block` for the for-loop body), comprehension variables stay
21
- * in their expression context, etc. Returns `null` to delegate. */
22
- export function pythonBindingScopeFor(_decl, _innermost, _tree) {
18
+ * enclosing scope" default is already correct for ordinary bindings.
19
+ * Constructor-injected instance fields are the exception: their marker is
20
+ * anchored inside `__init__`, but compound receiver resolution needs the
21
+ * field type on the enclosing Class scope. */
22
+ export function pythonBindingScopeFor(decl, innermost, tree) {
23
+ if (decl['@type-binding.instance-field'] !== undefined) {
24
+ let current = innermost;
25
+ while (current !== undefined) {
26
+ if (current.kind === 'Class')
27
+ return current.id;
28
+ if (current.parent === null)
29
+ break;
30
+ current = tree.getScope(current.parent);
31
+ }
32
+ }
23
33
  return null;
24
34
  }
25
35
  // ─── importOwningScope ────────────────────────────────────────────────────
@@ -770,13 +770,15 @@ function followChainedRef(start, draftById) {
770
770
  * name in the same scope. Higher number wins; ties keep the later match
771
771
  * (last-write-wins preserves historical order within a tier).
772
772
  *
773
- * Rationale: explicit annotations always beat inferred ones because they
774
- * reflect user intent. `self`/`cls` are treated as strongly as annotations
775
- * because they are language-required receiver types.
773
+ * Rationale: explicit variable and field annotations always beat bindings
774
+ * derived from parameter annotations or inference because they reflect the
775
+ * most specific user intent. `self`/`cls` are treated as strongly as other
776
+ * declared types because they are language-required receiver types.
776
777
  */
777
778
  function typeBindingStrength(source) {
778
779
  switch (source) {
779
780
  case 'annotation':
781
+ return 3;
780
782
  case 'parameter-annotation':
781
783
  case 'return-annotation':
782
784
  case 'self':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.87",
3
+ "version": "1.6.10-rc.89",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -56,7 +56,7 @@
56
56
  "version": "node scripts/sync-plugin-manifests.mjs"
57
57
  },
58
58
  "dependencies": {
59
- "@ladybugdb/core": "^0.18.0",
59
+ "@ladybugdb/core": "^0.18.3",
60
60
  "@modelcontextprotocol/sdk": "^1.0.0",
61
61
  "@scarf/scarf": "^1.4.0",
62
62
  "busboy": "^1.6.0",