gitnexus 1.6.6-rc.32 → 1.6.6-rc.33

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.
@@ -5,7 +5,12 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
5
5
  * - FastAPI `@app.get("/path")` provider decorators
6
6
  * - `requests.get/post/...("url")` consumer calls
7
7
  * - Generic `requests.request("METHOD", "url")` consumer calls
8
- * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`
8
+ * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`, including
9
+ * aliased imports such as `import httpx as hx`,
10
+ * `from httpx import AsyncClient`, and
11
+ * `from httpx import AsyncClient as HttpxAsyncClient`.
12
+ * Locally rebound names (e.g. `AsyncClient = mock_factory()` inside a
13
+ * function) are excluded to avoid false-positive consumer contracts.
9
14
  */
10
15
  const FASTAPI_VERBS = {
11
16
  get: 'GET',
@@ -67,12 +72,48 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
67
72
  ],
68
73
  });
69
74
  // ─── Consumer: httpx.AsyncClient assignments ────────────────────────
70
- // NOTE: This targeted detector only tracks explicit `httpx.AsyncClient(...)`
71
- // construction. Direct imports (`from httpx import AsyncClient`) and module
72
- // aliases (`import httpx as hx`) and annotated assignments (`client: httpx.AsyncClient = ...`)
73
- // are intentionally left for a follow-up. Module-scope clients are only matched
75
+ // Module-scope clients are only matched
74
76
  // at module scope; calls inside functions require a function/class-local tracked
75
77
  // client to avoid false positives from same-name local variables.
78
+ const HTTPX_MODULE_IMPORT_PATTERNS = compilePatterns({
79
+ name: 'python-httpx-module-imports',
80
+ language: Python,
81
+ patterns: [
82
+ {
83
+ meta: {},
84
+ query: `
85
+ (import_statement
86
+ name: (aliased_import
87
+ name: (dotted_name (identifier) @module)
88
+ alias: (identifier) @alias))
89
+ `,
90
+ },
91
+ ],
92
+ });
93
+ const HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS = compilePatterns({
94
+ name: 'python-httpx-async-client-imports',
95
+ language: Python,
96
+ patterns: [
97
+ {
98
+ meta: {},
99
+ query: `
100
+ (import_from_statement
101
+ module_name: (dotted_name (identifier) @module)
102
+ name: (dotted_name (identifier) @client_class))
103
+ `,
104
+ },
105
+ {
106
+ meta: {},
107
+ query: `
108
+ (import_from_statement
109
+ module_name: (dotted_name (identifier) @module)
110
+ name: (aliased_import
111
+ name: (dotted_name (identifier) @client_class)
112
+ alias: (identifier) @alias))
113
+ `,
114
+ },
115
+ ],
116
+ });
76
117
  const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
77
118
  name: 'python-httpx-async-client-assign',
78
119
  language: Python,
@@ -84,8 +125,23 @@ const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
84
125
  left: (_) @client
85
126
  right: (call
86
127
  function: (attribute
87
- object: (identifier) @module (#eq? @module "httpx")
88
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient"))))
128
+ object: (identifier) @module
129
+ attribute: (identifier) @client_class)))
130
+ `,
131
+ },
132
+ ],
133
+ });
134
+ const HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS = compilePatterns({
135
+ name: 'python-httpx-async-client-direct-assign',
136
+ language: Python,
137
+ patterns: [
138
+ {
139
+ meta: {},
140
+ query: `
141
+ (assignment
142
+ left: (_) @client
143
+ right: (call
144
+ function: (identifier) @client_class))
89
145
  `,
90
146
  },
91
147
  ],
@@ -101,8 +157,23 @@ const HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS = compilePatterns({
101
157
  (as_pattern
102
158
  (call
103
159
  function: (attribute
104
- object: (identifier) @module (#eq? @module "httpx")
105
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient")))
160
+ object: (identifier) @module
161
+ attribute: (identifier) @client_class))
162
+ (as_pattern_target (identifier) @client))
163
+ `,
164
+ },
165
+ ],
166
+ });
167
+ const HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS = compilePatterns({
168
+ name: 'python-httpx-async-client-direct-with-alias',
169
+ language: Python,
170
+ patterns: [
171
+ {
172
+ meta: {},
173
+ query: `
174
+ (as_pattern
175
+ (call
176
+ function: (identifier) @client_class)
106
177
  (as_pattern_target (identifier) @client))
107
178
  `,
108
179
  },
@@ -131,14 +202,120 @@ function trackedClientScopeKey(clientNode) {
131
202
  return getScopeKey(clientNode.parent, clientNode.text.includes('.'));
132
203
  }
133
204
  function callScopeKeys(clientNode) {
134
- const keys = new Set();
135
- const preferClass = clientNode.text.includes('.');
136
- const nearestScope = getScopeKey(clientNode.parent, preferClass);
137
- keys.add(nearestScope);
138
- return [...keys];
205
+ return [getScopeKey(clientNode.parent, clientNode.text.includes('.'))];
206
+ }
207
+ // Returns the scope key that a rebind of an imported alias would shadow under
208
+ // Python LEGB rules, or `null` when the rebind does not shadow anything that
209
+ // could produce a false-positive consumer detection.
210
+ // - Rebind inside a function/method → that function's scope.
211
+ // - Rebind at module top level → 'module' (shadows the whole file).
212
+ // - Rebind in a class body without an enclosing function → null. Python
213
+ // class attributes do not shadow bare-name lookups inside methods (methods
214
+ // see the module binding, not the class attribute), so we must not poison
215
+ // them.
216
+ function shadowScopeKey(node) {
217
+ let current = node;
218
+ let passedThroughClass = false;
219
+ while (current) {
220
+ if (current.type === 'function_definition') {
221
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
222
+ return getScopeKey(current);
223
+ }
224
+ if (current.type === 'class_definition') {
225
+ passedThroughClass = true;
226
+ }
227
+ current = current.parent;
228
+ }
229
+ return passedThroughClass ? null : 'module';
230
+ }
231
+ function collectHttpxImportAliases(tree) {
232
+ const moduleAliases = new Set(['httpx']);
233
+ const asyncClientAliases = new Set();
234
+ // The @module capture is a single identifier inside a `dotted_name`, so for
235
+ // `import package.httpx as hx` the pattern would match the inner `httpx`
236
+ // segment. Check the full `dotted_name` text via `parent` to anchor the match.
237
+ for (const match of runCompiledPatterns(HTTPX_MODULE_IMPORT_PATTERNS, tree)) {
238
+ const moduleNode = match.captures.module;
239
+ const aliasNode = match.captures.alias;
240
+ if (moduleNode?.parent?.text === 'httpx' && aliasNode)
241
+ moduleAliases.add(aliasNode.text);
242
+ }
243
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS, tree)) {
244
+ const moduleNode = match.captures.module;
245
+ const classNode = match.captures.client_class;
246
+ if (moduleNode?.parent?.text !== 'httpx' || classNode?.text !== 'AsyncClient')
247
+ continue;
248
+ asyncClientAliases.add(match.captures.alias?.text ?? classNode.text);
249
+ }
250
+ return { moduleAliases, asyncClientAliases };
251
+ }
252
+ // Tracks local rebindings (`AsyncClient = ...`, `hx = ...`) that shadow an
253
+ // imported alias. We treat the whole enclosing scope (module, class, or
254
+ // function) as shadowed for that alias name, so subsequent constructions in
255
+ // that scope are not falsely detected as httpx consumers. Covers bare-identifier
256
+ // targets and the common tuple / list destructuring shapes.
257
+ const ALIAS_SHADOW_PATTERNS = compilePatterns({
258
+ name: 'python-httpx-alias-shadow',
259
+ language: Python,
260
+ patterns: [
261
+ {
262
+ meta: {},
263
+ query: `(assignment left: (identifier) @name)`,
264
+ },
265
+ {
266
+ meta: {},
267
+ query: `(assignment left: (pattern_list (identifier) @name))`,
268
+ },
269
+ {
270
+ meta: {},
271
+ query: `(assignment left: (tuple_pattern (identifier) @name))`,
272
+ },
273
+ {
274
+ meta: {},
275
+ query: `(assignment left: (list_pattern (identifier) @name))`,
276
+ },
277
+ ],
278
+ });
279
+ function collectAliasShadowScopes(tree, aliases) {
280
+ const shadowed = new Map();
281
+ if (aliases.size === 0)
282
+ return shadowed;
283
+ for (const match of runCompiledPatterns(ALIAS_SHADOW_PATTERNS, tree)) {
284
+ const nameNode = match.captures.name;
285
+ if (!nameNode || !aliases.has(nameNode.text))
286
+ continue;
287
+ const scopeKey = shadowScopeKey(nameNode.parent);
288
+ if (scopeKey === null)
289
+ continue;
290
+ const set = shadowed.get(nameNode.text) ?? new Set();
291
+ set.add(scopeKey);
292
+ shadowed.set(nameNode.text, set);
293
+ }
294
+ return shadowed;
295
+ }
296
+ function isAliasShadowed(shadowed, aliasName, node) {
297
+ const scopes = shadowed.get(aliasName);
298
+ if (!scopes || scopes.size === 0)
299
+ return false;
300
+ let current = node.parent;
301
+ while (current) {
302
+ if (current.type === 'function_definition') {
303
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
304
+ if (scopes.has(getScopeKey(current)))
305
+ return true;
306
+ }
307
+ current = current.parent;
308
+ }
309
+ // A module-level rebind shadows the alias for the entire file.
310
+ return scopes.has('module');
139
311
  }
140
312
  function collectHttpxAsyncClients(tree) {
141
313
  const clients = new Map();
314
+ const { moduleAliases, asyncClientAliases } = collectHttpxImportAliases(tree);
315
+ // Module aliases (`hx`) and AsyncClient aliases (`AsyncClient`,
316
+ // `HttpxAsyncClient`) share disjoint name spaces, so one shadow map keyed by
317
+ // alias name serves both lookups and we only walk the tree for rebinds once.
318
+ const shadowed = collectAliasShadowScopes(tree, new Set([...moduleAliases, ...asyncClientAliases]));
142
319
  const addClient = (clientNode) => {
143
320
  if (!clientNode)
144
321
  return;
@@ -149,9 +326,41 @@ function collectHttpxAsyncClients(tree) {
149
326
  clients.set(clientText, scopes);
150
327
  };
151
328
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS, tree)) {
329
+ const moduleNode = match.captures.module;
330
+ const classNode = match.captures.client_class;
331
+ if (!moduleNode || !classNode)
332
+ continue;
333
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
334
+ continue;
335
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
336
+ continue;
337
+ addClient(match.captures.client);
338
+ }
339
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS, tree)) {
340
+ const classNode = match.captures.client_class;
341
+ if (!classNode || !asyncClientAliases.has(classNode.text))
342
+ continue;
343
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
344
+ continue;
152
345
  addClient(match.captures.client);
153
346
  }
154
347
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS, tree)) {
348
+ const moduleNode = match.captures.module;
349
+ const classNode = match.captures.client_class;
350
+ if (!moduleNode || !classNode)
351
+ continue;
352
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
353
+ continue;
354
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
355
+ continue;
356
+ addClient(match.captures.client);
357
+ }
358
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS, tree)) {
359
+ const classNode = match.captures.client_class;
360
+ if (!classNode || !asyncClientAliases.has(classNode.text))
361
+ continue;
362
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
363
+ continue;
155
364
  addClient(match.captures.client);
156
365
  }
157
366
  return clients;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.32",
3
+ "version": "1.6.6-rc.33",
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",