ucn 5.5.2 → 5.5.3

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.
@@ -62,7 +62,7 @@ Use text search (grep/ripgrep) for simple literals, error messages, configuratio
62
62
  | `--depth=N` | Trace, dependency, or test traversal depth. |
63
63
  | `--direction=<value>` | Trace (`callees`/`callers`) or dependency (`imports`/`importers`/`both`) direction. |
64
64
  | `--all` | Lift result and formatter caps where a command supports it; the truncation notice says when it does. |
65
- | `--max-chars=N` | Text budget in characters (10K targeted / 3K broad by default; 100K ceiling), including notices and trust lines. Trust lines are reserved first; a limit too small for them withholds the answer and names the budget that carries them. MCP: `max_chars`. JSON is not transport-truncated. |
65
+ | `--max-chars=N` | Text budget in characters (10K targeted / 3K broad by default; 100K ceiling), including notices and trust lines. Trust lines are reserved first; a limit too small for them withholds the answer and names the budget that carries them. MCP: `max_chars`. CLI JSON is uncapped even with an explicit budget; `meta.maxChars` is `"not-applied"`. |
66
66
  | `--compact` / `--no-compact` | Token-efficient or full presentation (`show`/`impact` default compact, `usages` full). |
67
67
  | `--range=N-M` | Explicit line range with `source <file>` or `source --file=<path>`. |
68
68
  | `--json` | Stable `{ meta, data }` envelope; `meta.contract` carries the truth boundary, decision safety, and next actions. |
@@ -177,7 +177,7 @@ In JS/TS and Python, where an import runs the imported module, cycles distinguis
177
177
 
178
178
  Route tables count too: Django URLconfs (`path`/`re_path`/`include`, composed from `ROOT_URLCONF`), DRF routers, Starlette `Route`/`Mount`/`Host` lists, Flask `add_url_rule`, aiohttp `add_routes`/`router.add_*`. Methods come from the view (`require_*` decorators, class-view handlers, viewset actions); an unresolved view serves `ALL`. Tags: `[regex]` (a `re_path` shown in path form, an unconvertible part as `{?...}`), `[drf-router]`, `[static]`, and `[include]`/`[mount]` for a target that cannot be resolved (shown as `prefix/{?target}`, matched only as uncertain). Django composes as Django does: an include prefix and the included pattern are concatenated as written (`path("api", include(...))` + `"items/"` serves `/apiitems/`); a `re_path` without `^` or, for a view, without `$` matches any text there, shown as `{?*}` (matched as a catch-all); a regex group that can match `/` is written `<path:name>`.
179
179
 
180
- Client receivers are recognized by evidence (a receiver typed to an HTTP client class, or bound to a pytest fixture that constructs one), not only by name. Request-configuration calls (`request(OpenAPI, { method, url })`, `client.post({ url })`, `session.request(method=..., url=...)`, generated OpenAPI SDKs) count as clients when the callee's definition reaches an HTTP call (fetch, axios, requests, ...). Request-shaped calls on an unrecognized receiver or an unproven helper are listed under `Possible client requests` (JSON `uncertainRequests`, with the reason), counted in `meta`, never in the inventory.
180
+ Client receivers require HTTP import, constructor, or factory evidence; names such as `client`, `session`, or `api` alone stay possible and never create bridges. Python follows returned clients and fixture chains, including imported/class fixtures and fixtures returning factories; JavaScript/TypeScript follows imported client values and factories such as `axios.create`, `ky.create`, and `got.extend`. Request-configuration calls (`request(OpenAPI, { method, url })`, `client.post({ url })`, `session.request(method=..., url=...)`, generated OpenAPI SDKs) count as clients when the callee's definition reaches an HTTP call (fetch, axios, requests, ...). Request-shaped calls on an unrecognized receiver or an unproven helper are listed under `Possible client requests` (JSON `uncertainRequests`, with the reason), counted in `meta`, never in the inventory.
181
181
 
182
182
  A request through an in-process test client built from an app (`TestClient(app)`, a fixture's client, `app.test_client()`, `AsyncClient(transport=ASGITransport(app=app))`) bridges only to routes of that app or mounted under it; an app declared as a function or class serves the request itself and matches no route. A supertest client (`request(app).get('/x')`, `request.agent(app)`, a client bound to a local; the name bound to the `supertest` module, whatever it is called) is one too, its app followed through imports and `app.callback()` / `http.createServer(app)`; `request(this.app)` in a mocha test or hook takes the app its suite's `before`/`beforeEach` hook stores on `this` (the nearest suite assigning it, which writes it nowhere else and registers nothing through `this.app`). When the app does not resolve, every path match stays, marked `[unscoped]` (JSON `unscoped: true`). A HEAD request matches a GET route where the framework serves HEAD with GET (Starlette, Flask, Express).
183
183
 
@@ -187,7 +187,7 @@ Route paths include router mount prefixes composed from the code: FastAPI `inclu
187
187
 
188
188
  Constructor declarations are not claims of their own (`N constructor(s) not listed`, JSON `excludedConstructors`): reflection, frameworks and the initializer the compiler generates when one is deleted can depend on them without a construction call. An unused type still appears as a whole-type candidate.
189
189
 
190
- `deadcode` is a candidate generator; every candidate is review-only. Computed dispatch such as `handlers[key]()` is a reported blind spot; registry members reached by a modeled computed receiver are withheld. Statically named reflection such as `getattr(obj, "run")` is positive liveness evidence, so every matching member spelling is withheld. A reflective name built from literal fragments around a runtime part (`getattr(self, "_get_%s_perms" % src)`, f-strings, `.format`, `"get" + name` in `getMethod`, `MethodByName(fmt.Sprintf(...))`, `obj["on" + evt]`) is a pattern: members it can spell on a receiver that may hold their class are withheld and counted per pattern, and `show`/`impact`/`plan` list the site as an unverified `reflection-pattern` caller. `eval(name)`/`globals()[name]` reach only their own module's top-level names; a fully dynamic one (`eval(code)`) withholds them all and is listed as a caller of none. A reflective name that is a parameter of the enclosing function, never reassigned (`registry[type](..)` in a call, `getattr(self, name)`, `getMethod(name)`), takes the string literals the function's callers pass at that position: those members are withheld and listed as `reflection-pattern` callers. Reflection with no specific literal part is counted and warned because it cannot be attributed to one member.
190
+ `deadcode` is a candidate generator; every candidate is review-only. Computed dispatch such as `handlers[key]()` is a reported blind spot; registry members reached by a modeled computed receiver are withheld. Statically named reflection such as `getattr(obj, "run")` is positive liveness evidence, so every matching member spelling is withheld. A reflective name built from literal fragments around a runtime part (`getattr(self, "_get_%s_perms" % src)`, f-strings, `.format`, `"get" + name` in `getMethod`, `MethodByName(fmt.Sprintf(...))`, `obj["on" + evt]`) is a pattern: members it can spell on a receiver that may hold their class are withheld and counted per pattern, and `show`/`impact`/`plan` list the site as an unverified `reflection-pattern` caller. `eval(name)`/`globals()[name]` reach only their own module's top-level names; a fully dynamic one (`eval(code)`) withholds them all and is listed as a caller of none. A reflective name that is a parameter of the enclosing function, never reassigned (`registry[type](..)` in a call, `getattr(self, name)`, `getMethod(name)`), takes the string literals the function's callers pass at that position: those members are withheld and listed as `reflection-pattern` callers. Reflection with no specific literal part is counted and warned because it cannot be attributed to one member. When a file contains dynamic reflection, `deadcode` also conservatively withholds candidates in that file whose exact spelling occurs as an AST string literal. Text and JSON (`reflection.withheldByTable`) list each name and literal location. This does not prove flow: even an unrelated literal in that file can withhold a candidate; another file without dynamic reflection is unaffected.
191
191
 
192
192
  Members the runtime or compiler invokes by name are hidden as runtime callbacks: Python dunders and Enum hooks (`_generate_next_value_`, `_missing_`) on Enum subclasses, Java serialization callbacks and `serialVersionUID` on Serializable types (also when written `implements java.io.Serializable` or inherited) and, as possible members with the protocol's signature, on classes that derive from another out-of-project type (`extends IOException`, `extends ArrayList`; `plan` keeps their edits and lists the definition for review), C# pattern members (`GetEnumerator`, `GetAwaiter`, `Deconstruct`), C# compiler-required types the project declares for a feature it uses (`System.Runtime.CompilerServices.IsExternalInit` with `init` accessors or positional records, `RequiredMemberAttribute` with `required` members, `System.Index`/`Range` with `^i`/`a..b`; unused when the feature is absent), JS `toJSON`/`then`/`[Symbol.*]`, C++ `begin`/`end`. Unknown decorators/annotations and member-assigned event handlers are also withheld by default because they can be the registration itself. Members of a class deriving from an outside base (underscore hooks such as a stream's `_transform` included), members of an object literal declared with an outside type (`ProxyHandler` traps) and members of impls of an outside Rust trait (`impl FromIterator for A`, however many project types implement it) may be called by that outside code and are hidden as external-contract surface.
193
193
 
package/README.md CHANGED
@@ -220,6 +220,8 @@ records carry a tab-separated reason. An empty `--lines` listing exits 1, an
220
220
  error exits 2, and a successful listing exits 0. `--lines` supports `find`, `show`,
221
221
  `usages`, `search`, and `impact`; `show --lines` lists callers by default.
222
222
  Use `--json` when the script needs structured fields or a tree result.
223
+ CLI JSON is uncapped by `--max-chars`, even when explicitly supplied;
224
+ `meta.maxChars` reports `"not-applied"`. Use command limits to narrow the result.
223
225
 
224
226
  Listings are deterministic for a given source tree: a fresh build, a cache
225
227
  load, and an incremental rebuild return the same records in the same order.
package/cli/index.js CHANGED
@@ -963,6 +963,7 @@ Common flags:
963
963
  --range=N-M (source <file> --range=N-M, or --file=PATH)
964
964
  --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
965
965
  --max-chars=N (text characters, not UTF-8 bytes; default 10K targeted / 3K broad, ceiling 100K)
966
+ JSON is uncapped; meta.maxChars is "not-applied", even with an explicit budget.
966
967
  --lines find/usages/search/show/impact: grep -n shape, one path:line:text
967
968
  record per output line; usages combines occurrences on each source line
968
969
  (tags after a tab: # unverified: <reason>, # import,
package/core/bridge.js CHANGED
@@ -2279,53 +2279,6 @@ function extractNextjsRoutes(index) {
2279
2279
  * Detect HTTP client requests across the project.
2280
2280
  * Cached on `index._endpointsCache.clientRequests`.
2281
2281
  */
2282
- // Python HTTP client types whose instances are receivers of request calls.
2283
- // A receiver typed to one of these (with-binding, constructor assignment) or
2284
- // bound to a pytest fixture that constructs one is a client by evidence, not
2285
- // by name (fix #349: 145 of 625 real `tc.get("/api/...")` sites on one repo
2286
- // were invisible because the receiver was not literally named `client`).
2287
- const PY_CLIENT_TYPES = new Set([
2288
- 'TestClient', 'Client', 'AsyncClient', 'Session', 'FlaskClient',
2289
- 'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
2290
- ]);
2291
- const PY_CLIENT_FACTORIES = new Set(['test_client', 'Session', 'Client', 'AsyncClient']);
2292
- const PY_REQUEST_METHODS = /^(get|post|put|delete|patch|options|head|request)$/;
2293
-
2294
- function _pythonClientFixtures(index) {
2295
- if (index._endpointsCache?.pyClientFixtures) return index._endpointsCache.pyClientFixtures;
2296
- const fixtures = new Set();
2297
- for (const [filePath, fileEntry] of index.files) {
2298
- if (fileEntry.language !== 'python') continue;
2299
- const fixtureDefs = (fileEntry.symbols || []).filter(s =>
2300
- (s.decorators || []).some(d => /(^|\.)fixture$/.test(String(d))));
2301
- if (fixtureDefs.length === 0) continue;
2302
- const calls = getCachedCalls(index, filePath) || [];
2303
- for (const def of fixtureDefs) {
2304
- const constructsClient = calls.some(c =>
2305
- c.enclosingFunction?.name === def.name &&
2306
- c.enclosingFunction?.startLine === def.startLine &&
2307
- (PY_CLIENT_TYPES.has(c.name) || (c.isMethod && PY_CLIENT_FACTORIES.has(c.name))));
2308
- if (constructsClient) fixtures.add(def.name);
2309
- }
2310
- }
2311
- if (!index._endpointsCache) index._endpointsCache = {};
2312
- index._endpointsCache.pyClientFixtures = fixtures;
2313
- return fixtures;
2314
- }
2315
-
2316
- function _pythonClientReceiver(index, fileEntry, call, fixtures) {
2317
- if (!call.isMethod || !call.receiver || !PY_REQUEST_METHODS.test(call.name)) return null;
2318
- if (call.receiverType && PY_CLIENT_TYPES.has(String(call.receiverType).split('.').pop())) {
2319
- return 'python-client';
2320
- }
2321
- if (fixtures.size === 0 || !fixtures.has(call.receiver)) return null;
2322
- const fn = call.enclosingFunction;
2323
- if (!fn) return null;
2324
- const sym = (fileEntry.symbols || []).find(s => s.name === fn.name && s.startLine === fn.startLine);
2325
- const params = String(sym?.params || '').split(',').map(p => p.trim().split(/[:=]/)[0].trim());
2326
- return params.includes(call.receiver) ? 'pytest-client-fixture' : null;
2327
- }
2328
-
2329
2282
  // `fastify().route({...})` - a route declared on a freshly built server.
2330
2283
  const JS_SERVER_FACTORY_CALL = /^(fastify|Fastify|express|Hono|Koa)$/;
2331
2284
 
@@ -2347,7 +2300,7 @@ function extractClientRequests(index) {
2347
2300
  }
2348
2301
  const requests = [];
2349
2302
  const uncertain = [];
2350
- const pyFixtures = _pythonClientFixtures(index);
2303
+ const clientProof = new (require('./client-receivers').ClientReceiverProof)(index, endpointsSession(index));
2351
2304
  let helperProof = null;
2352
2305
 
2353
2306
  for (const [filePath, fileEntry] of index.files) {
@@ -2431,7 +2384,9 @@ function extractClientRequests(index) {
2431
2384
  continue;
2432
2385
  }
2433
2386
  if (!helperProof) helperProof = new RequestHelperProof(index, endpointsSession(index));
2434
- const proof = helperProof.classify(filePath, call);
2387
+ const proof = call.isMethod
2388
+ ? { proven: !!clientProof.classify(filePath, call), reason: 'receiver-unproven' }
2389
+ : helperProof.classify(filePath, call);
2435
2390
  const verb = call.isMethod && /^(get|post|put|delete|patch|options|head)$/i.test(call.name)
2436
2391
  ? call.name.toUpperCase() : null;
2437
2392
  const method = cfg.method || verb || 'ALL';
@@ -2462,7 +2417,7 @@ function extractClientRequests(index) {
2462
2417
  // fix #383: Go request constructors read their method and URL
2463
2418
  // from their own argument positions.
2464
2419
  if (call.requestTarget && lang === 'go' && call.receiver && !call.isPathCall &&
2465
- matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, calls)) {
2420
+ matchClientRequest({ ...call, firstStringArg: call.requestTarget.url }, lang, clientProof.classify(filePath, call))) {
2466
2421
  const target = call.requestTarget;
2467
2422
  if (isPathShaped(target.url)) {
2468
2423
  // A request with a path but no scheme and host cannot be
@@ -2496,26 +2451,22 @@ function extractClientRequests(index) {
2496
2451
  continue;
2497
2452
  }
2498
2453
  if (!call.firstStringArg) continue;
2499
- let r = matchClientRequest(call, lang, calls);
2500
- if (!r && lang === 'python') {
2501
- const framework = _pythonClientReceiver(index, fileEntry, call, pyFixtures);
2502
- if (framework) {
2503
- r = { method: call.name.toUpperCase() === 'REQUEST' ? 'ALL' : call.name.toUpperCase(),
2504
- framework, methodInferred: call.name === 'request' };
2505
- }
2506
- }
2454
+ const conf = CLIENT_PATTERNS[lang];
2455
+ const candidate = conf && (conf.bareCalls.has(call.name) ||
2456
+ conf.receivers.some(p => p.methodPattern.test(call.name)));
2457
+ const r = matchClientRequest(call, lang, candidate ? clientProof.classify(filePath, call) : null);
2507
2458
  if (!r) {
2508
2459
  // Visible uncertainty: request-shaped call on an unrecognized
2509
2460
  // receiver with a path-shaped literal. Listed, never counted.
2510
- const conf = CLIENT_PATTERNS[lang];
2511
2461
  const pathShaped = call.firstStringArg.startsWith('/') || call.firstStringArg.includes('://');
2512
- if (conf && call.isMethod && call.receiver && pathShaped &&
2462
+ if (conf && call.receiver && pathShaped &&
2513
2463
  conf.receivers.some(p => p.methodPattern.test(call.name))) {
2514
2464
  uncertain.push({
2515
2465
  receiver: call.receiver, method: call.name, path: call.firstStringArg,
2516
2466
  file: fileEntry.relativePath || filePath, absoluteFile: filePath, line: call.line,
2517
2467
  callerName: call.enclosingFunction?.name || '<top-level>',
2518
- reason: 'receiver-unrecognized',
2468
+ reason: conf.receivers.some(p => p.receiverPattern.test(call.receiver))
2469
+ ? 'receiver-name-only' : 'receiver-unrecognized',
2519
2470
  });
2520
2471
  }
2521
2472
  continue;
@@ -2640,9 +2591,9 @@ function supertestProducerOf(call, calls, producers, locals) {
2640
2591
  * Match a call against client request patterns.
2641
2592
  * Returns { method, framework, methodInferred } or null.
2642
2593
  */
2643
- function matchClientRequest(call, lang, allCallsInFile) {
2594
+ function matchClientRequest(call, lang, origin) {
2644
2595
  const conf = CLIENT_PATTERNS[lang];
2645
- if (!conf) return null;
2596
+ if (!conf || !origin) return null;
2646
2597
 
2647
2598
  // 1) Bare-call patterns: fetch('/path') or fetch('/path', { method: 'POST' })
2648
2599
  if (!call.isMethod && conf.bareCalls.has(call.name)) {
@@ -2662,7 +2613,6 @@ function matchClientRequest(call, lang, allCallsInFile) {
2662
2613
  const isMethodLike = call.isMethod || (lang === 'go' && !!call.receiver && !call.isPathCall);
2663
2614
  if (isMethodLike && call.receiver) {
2664
2615
  for (const p of conf.receivers) {
2665
- if (!p.receiverPattern.test(call.receiver)) continue;
2666
2616
  if (!p.methodPattern.test(call.name)) continue;
2667
2617
 
2668
2618
  // Determine method
@@ -2697,7 +2647,7 @@ function matchClientRequest(call, lang, allCallsInFile) {
2697
2647
  } else {
2698
2648
  method = methodName.toUpperCase();
2699
2649
  }
2700
- return { method, framework: p.framework, methodInferred: inferred };
2650
+ return { method, framework: origin, methodInferred: inferred };
2701
2651
  }
2702
2652
  }
2703
2653
 
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ // Request verbs and receiver names discover candidates; only the value's
4
+ // origin can confirm HTTP. Shared by positional and configuration requests.
5
+ const rg = require('./route-graph');
6
+ const { RequestHelperProof, PY_HTTP_MODULES, pyImportedFunction } = require('./request-helpers');
7
+ const { PythonClientResolver } = require('./inprocess-clients');
8
+ const { field, namedChildren, unwrap, findDecl } = rg;
9
+
10
+ const PY_CLIENT_MODULES = [...PY_HTTP_MODULES, 'fastapi.testclient',
11
+ 'starlette.testclient', 'flask.testing', 'werkzeug.test', 'django.test'];
12
+ const PY_FACTORIES = new Set(['Session', 'Client', 'AsyncClient', 'TestClient',
13
+ 'FlaskClient', 'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
14
+ 'PoolManager']);
15
+ const NOMINAL_CLIENTS = {
16
+ go: ['net/http.Client', 'net/http'],
17
+ rust: ['reqwest.Client', 'reqwest.blocking.Client', 'reqwest'],
18
+ java: ['org.springframework.web.client.RestTemplate',
19
+ 'org.springframework.web.reactive.function.client.WebClient', 'java.net.http.HttpClient'],
20
+ csharp: ['System.Net.Http.HttpClient'],
21
+ };
22
+ const HTTP_FRAMEWORK = { go: 'go-http', rust: 'reqwest', java: 'spring-client', csharp: 'dotnet-httpclient' };
23
+ const MAX_DEPTH = 12;
24
+
25
+ class ClientReceiverProof {
26
+ constructor(index, sess) {
27
+ this.index = index;
28
+ this.sess = sess;
29
+ this.helpers = new RequestHelperProof(index, sess);
30
+ this.python = new PythonClientResolver(index, sess, () => new Set());
31
+ this.memo = new Map();
32
+ }
33
+
34
+ classify(file, call) {
35
+ const lang = this.sess.lang(file);
36
+ if (['javascript', 'typescript', 'tsx'].includes(lang)) {
37
+ if (!this.helpers.classify(file, call).proven) return null;
38
+ const binding = this.helpers.jsBinding(file, call.receiver);
39
+ const decl = call.receiver && findDecl(this.sess, file, call.receiver,
40
+ this.sess.root(file)?.descendantForIndex(call.callStart, call.callStart));
41
+ const module = binding?.binding.module || this.helpers.requireSpecOf(decl);
42
+ return ['axios', 'ky', 'got'].includes(module) ? module : 'http-client';
43
+ }
44
+ if (lang === 'python') {
45
+ const root = this.sess.root(file);
46
+ const start = call.callSite?.start;
47
+ if (!root || start == null) return null;
48
+ let node = root.descendantForIndex(start, start);
49
+ while (node && node.type !== 'call') node = node.parent;
50
+ const fn = node && field(node, 'function');
51
+ const receiver = fn?.type === 'attribute' ? unwrap(field(fn, 'object')) : null;
52
+ if (!receiver) return null;
53
+ if (PY_HTTP_MODULES.includes(this.pyModule(file, receiver))) return 'requests';
54
+ const values = this.python.clientValues(file, call.line, call.receiver, call.name);
55
+ if (!values?.length || !values.every(v => this.pyValue(v.file, v.expr, 0))) return null;
56
+ return findDecl(this.sess, file, receiver.text, receiver)?.kind === 'param'
57
+ ? 'pytest-client-fixture' : 'python-client';
58
+ }
59
+ return this.nominal(file, call, lang);
60
+ }
61
+
62
+ pyModule(file, expr) {
63
+ if (expr?.type === 'identifier' && !findDecl(this.sess, file, expr.text, expr)) {
64
+ const entry = this.sess.entry(file);
65
+ const candidates = (entry?.importBindings || []).filter(b => (b.alias || b.name) === expr.text);
66
+ const visible = candidates.filter(binding => {
67
+ if (!binding.deferred) return true;
68
+ const row = binding.line - 1;
69
+ const imports = this.sess.root(file)?.descendantsOfType(
70
+ ['import_statement', 'import_from_statement'],
71
+ { row, column: 0 }, { row, column: 1 << 20 }) || [];
72
+ return imports.some(node => {
73
+ let scope = node.parent;
74
+ while (scope && scope.type !== 'function_definition' && scope.type !== 'class_definition') scope = scope.parent;
75
+ return scope?.type === 'function_definition' && scope.startIndex <= expr.startIndex &&
76
+ scope.endIndex >= expr.endIndex;
77
+ });
78
+ });
79
+ if (!visible.length || new Set(visible.map(b => `${b.kind}:${b.module}:${b.name}`)).size !== 1) return null;
80
+ const binding = visible[0];
81
+ if (entry.moduleResolved?.[binding.module]) return null;
82
+ const module = binding.module;
83
+ return PY_CLIENT_MODULES.includes(module)
84
+ ? (binding.kind === 'from' ? `${module}.${binding.name}` : module) : null;
85
+ }
86
+ if (expr?.type === 'attribute') {
87
+ const parent = this.pyModule(file, unwrap(field(expr, 'object')));
88
+ return parent ? `${parent}.${field(expr, 'attribute')?.text}` : null;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ pyValue(file, expr, depth) {
94
+ const e = unwrap(expr);
95
+ if (!e || depth > MAX_DEPTH) return false;
96
+ const key = `${file}:${e.startIndex}:${e.endIndex}`;
97
+ if (this.memo.has(key)) return this.memo.get(key);
98
+ this.memo.set(key, false); // cycles cannot prove an origin
99
+ let proven = false;
100
+ if (e.type === 'identifier') {
101
+ let fn = e.parent;
102
+ while (fn && fn.type !== 'function_definition') fn = fn.parent;
103
+ const value = this.python.followBinding(file, fn, e, 0);
104
+ if (value && (value.file !== file || value.expr?.startIndex !== e.startIndex)) {
105
+ proven = this.pyValue(value.file, value.expr, depth + 1);
106
+ } else {
107
+ const decl = findDecl(this.sess, file, e.text, e);
108
+ if (decl?.count === 1 && decl.valueNode) proven = this.pyValue(file, decl.valueNode, depth + 1);
109
+ }
110
+ } else if (e.type === 'call') {
111
+ const callee = unwrap(field(e, 'function'));
112
+ const identity = this.pyModule(file, callee);
113
+ if (identity && PY_FACTORIES.has(identity.split('.').pop())) proven = true;
114
+ if (!proven) {
115
+ // A helper must RETURN a proven client; merely performing
116
+ // HTTP somewhere in its body does not type its return value.
117
+ const decl = callee?.type === 'identifier' && findDecl(this.sess, file, callee.text, callee);
118
+ let target = decl?.kind === 'function'
119
+ ? { kind: 'function', file, node: decl.valueNode }
120
+ : !decl && callee?.type === 'identifier' ? pyImportedFunction(this.sess, file, callee.text, 0) : null;
121
+ if (decl?.kind === 'param') {
122
+ const fixture = this.python.fixtureDef(file, callee.text, decl.scope);
123
+ const provided = fixture && this.python.fixtureValue(fixture.file, fixture.fnNode, 0);
124
+ if (provided?.expr?.type === 'function_definition') {
125
+ target = { kind: 'function', file: provided.file, node: provided.expr };
126
+ } else if (provided?.expr?.type === 'call') {
127
+ const fn = field(provided.expr, 'function');
128
+ // functools.partial(Client) still constructs the
129
+ // imported client when the fixture is called.
130
+ const binding = fn?.type === 'attribute' &&
131
+ this.helpers.pyBinding(provided.file, field(fn, 'object')?.text);
132
+ if (binding?.module === 'functools' && field(fn, 'attribute')?.text === 'partial') {
133
+ const ctor = namedChildren(field(provided.expr, 'arguments'))[0];
134
+ const identity = this.pyModule(provided.file, ctor);
135
+ proven = !!identity && PY_FACTORIES.has(identity.split('.').pop());
136
+ }
137
+ }
138
+ }
139
+ if (target?.kind === 'function') {
140
+ const fnNode = target.node || target.decl?.valueNode;
141
+ const value = fnNode && this.python.fixtureValue(target.file, fnNode, 0);
142
+ if (value) proven = this.pyValue(value.file, value.expr, depth + 1);
143
+ }
144
+ }
145
+ }
146
+ this.memo.set(key, proven);
147
+ return proven;
148
+ }
149
+
150
+ nominal(file, call, lang) {
151
+ const known = NOMINAL_CLIENTS[lang];
152
+ if (!known || call.localShadow || (call.isPathCall && call.receiverScopeBinding)) return null;
153
+ const entry = this.sess.entry(file);
154
+ const bindings = entry?.importBindings || [];
155
+ const qualifies = name => {
156
+ if (!name) return false;
157
+ const normalized = name.replace(/::/g, '.').replace(/^[*&]+/, '');
158
+ if (known.includes(normalized)) return true;
159
+ return bindings.some(b => {
160
+ const local = b.alias || b.name;
161
+ const module = String(b.module || '').replace(/::/g, '.');
162
+ return (normalized === local && (known.includes(module) || known.includes(`${module}.${b.name}`))) ||
163
+ (normalized.startsWith(local + '.') && known.includes(module + normalized.slice(local.length)));
164
+ });
165
+ };
166
+ if (!call.receiverLocalBinding && !call.isMethod && qualifies(call.receiver)) return HTTP_FRAMEWORK[lang];
167
+ if (call.isPathCall && qualifies(call.receiver)) return HTTP_FRAMEWORK[lang];
168
+ const qualifier = call.receiverTypeQualifier || call.receiverTypeNamespace;
169
+ const type = qualifier ? `${qualifier}.${call.receiverType}` : call.receiverType;
170
+ if (qualifies(type)) return HTTP_FRAMEWORK[lang];
171
+ // Java field receivers carry the enclosing owner rather than the
172
+ // field type in the call record. Read the declared field's AST type.
173
+ if (lang === 'java' && call.receiverRootType && call.receiverField) {
174
+ const root = this.sess.root(file);
175
+ for (const declaration of root?.descendantsOfType('field_declaration') || []) {
176
+ let owner = declaration.parent;
177
+ while (owner && owner.type !== 'class_declaration') owner = owner.parent;
178
+ if (field(owner, 'name')?.text !== call.receiverRootType) continue;
179
+ if (namedChildren(declaration).some(n => n.type === 'variable_declarator' &&
180
+ field(n, 'name')?.text === call.receiverField) && qualifies(field(declaration, 'type')?.text)) {
181
+ return HTTP_FRAMEWORK[lang];
182
+ }
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ }
188
+
189
+ module.exports = { ClientReceiverProof };
package/core/deadcode.js CHANGED
@@ -14,7 +14,7 @@ const { isFrameworkEntrypoint } = require('./entrypoints');
14
14
  const { splitParentList } = require('./graph-build');
15
15
  const { isOverrideMarked, codeUnitCompare, lineInRanges, maskBlockComments, escapeRegExp } = require('./shared');
16
16
  const { projectComputedDispatch, projectReflectionSites } = require('./ast-analysis');
17
- const { reflectionPatternReferences } = require('./reflection');
17
+ const { reflectionPatternReferences, reflectionLiteralNames } = require('./reflection');
18
18
  const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits } = require('./contract-membership');
19
19
 
20
20
  const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
@@ -770,6 +770,8 @@ function deadcode(index, options = {}) {
770
770
  }
771
771
  }
772
772
  const literalReflectionNames = new Set();
773
+ const dynamicReflectionFiles = new Set();
774
+ const reflectionTables = new Map();
773
775
  const reflectionInfo = {
774
776
  count: 0,
775
777
  literalCount: 0,
@@ -781,6 +783,7 @@ function deadcode(index, options = {}) {
781
783
  patterns: [],
782
784
  withheld: 0,
783
785
  withheldByPattern: [],
786
+ withheldByTable: [],
784
787
  };
785
788
  // Reflection is liveness evidence wherever it sits (a test or script
786
789
  // outside --in still reaches the member); the disclosure counts cover the
@@ -790,6 +793,7 @@ function deadcode(index, options = {}) {
790
793
  const fe = index.files.get(reflectionFile);
791
794
  if (!fe || sites.length === 0) continue;
792
795
  for (const site of sites) {
796
+ if (site.dynamic) dynamicReflectionFiles.add(reflectionFile);
793
797
  if (site.name) literalReflectionNames.add(site.name);
794
798
  if (site.patterns && site.patterns.length > 0 && !site.name) {
795
799
  // Exact alternatives of a multi-spelling site are literals.
@@ -1598,6 +1602,25 @@ function deadcode(index, options = {}) {
1598
1602
  continue;
1599
1603
  }
1600
1604
 
1605
+ // A table may feed a dynamic reflection site through a loop
1606
+ // or helper. Withhold exact same-file literal spellings,
1607
+ // conservatively: this is possible dispatch, not proven flow.
1608
+ if (dynamicReflectionFiles.has(symbol.file)) {
1609
+ if (!reflectionTables.has(symbol.file)) {
1610
+ const source = index._readFile(symbol.file);
1611
+ const tree = index._getParsedTree(symbol.file, source,
1612
+ index.files.get(symbol.file).language);
1613
+ reflectionTables.set(symbol.file, reflectionLiteralNames(tree));
1614
+ }
1615
+ const line = reflectionTables.get(symbol.file).get(symbol.name);
1616
+ if (line) {
1617
+ reflectionInfo.withheldByTable.push({ name: symbol.name,
1618
+ file: symbol.relativePath, line, startLine: symbol.startLine,
1619
+ reason: 'literal-name-near-dynamic-reflection' });
1620
+ continue;
1621
+ }
1622
+ }
1623
+
1601
1624
  results.push({
1602
1625
  name: symbol.name,
1603
1626
  type: symbol.type,
@@ -1635,6 +1658,8 @@ function deadcode(index, options = {}) {
1635
1658
  results.excludedConstructors = excludedConstructors;
1636
1659
  results.excludedDynamicDispatch = excludedDynamicDispatch;
1637
1660
  results.computedDispatch = computedDispatchInfo;
1661
+ reflectionInfo.withheldByTable.sort((a, b) => codeUnitCompare(a.file, b.file) ||
1662
+ a.startLine - b.startLine || codeUnitCompare(a.name, b.name));
1638
1663
  reflectionInfo.withheldByPattern = [...reflectionWithheldByPattern]
1639
1664
  .map(([key, count]) => {
1640
1665
  const [pattern, namespaceOf] = key.split('\u0000');
@@ -195,9 +195,23 @@ class PythonClientResolver {
195
195
 
196
196
  /** The fixture named `name` a test in `file` requests: same module first,
197
197
  * then conftest.py files from the test's directory up to the root. */
198
- fixtureDef(file, name) {
199
- const key = `${file}\0${name}`;
198
+ fixtureDef(file, name, requester = null) {
199
+ let owner = requester?.parent;
200
+ while (owner && owner.type !== 'class_definition') owner = owner.parent;
201
+ const key = `${file}\0${name}\0${owner?.startIndex ?? ''}`;
200
202
  if (this.fixtureMemo.has(key)) return this.fixtureMemo.get(key);
203
+ this.fixtureMemo.set(key, null);
204
+ if (owner) {
205
+ const matches = namedChildren(field(owner, 'body')).map(stmt =>
206
+ stmt.type === 'decorated_definition' ? field(stmt, 'definition') : stmt)
207
+ .filter(fn => fn?.type === 'function_definition' && field(fn, 'name')?.text === name);
208
+ if (matches.length) {
209
+ const found = matches.length === 1 && this.isFixture(file, matches[0])
210
+ ? { file, fnNode: matches[0] } : null;
211
+ this.fixtureMemo.set(key, found);
212
+ return found;
213
+ }
214
+ }
201
215
  let found = this.moduleFixture(file, name);
202
216
  if (!found) {
203
217
  for (let dir = path.dirname(file); dir.startsWith(this.index.root); dir = path.dirname(dir)) {
@@ -213,7 +227,8 @@ class PythonClientResolver {
213
227
  return found;
214
228
  }
215
229
 
216
- moduleFixture(file, name) {
230
+ moduleFixture(file, name, depth = 0) {
231
+ if (depth > MAX_HOPS) return null;
217
232
  const root = this.sess.root(file);
218
233
  if (!root) return null;
219
234
  const hits = [];
@@ -222,7 +237,12 @@ class PythonClientResolver {
222
237
  if (fn?.type === 'function_definition' && field(fn, 'name')?.text === name &&
223
238
  this.isFixture(file, fn)) hits.push(fn);
224
239
  }
225
- return hits.length === 1 ? { file, fnNode: hits[0] } : null;
240
+ if (hits.length) return hits.length === 1 ? { file, fnNode: hits[0] } : null;
241
+ const entry = this.sess.entry(file);
242
+ const bindings = (entry?.importBindings || []).filter(b => (b.alias || b.name) === name && !b.deferred);
243
+ if (bindings.length !== 1 || bindings[0].kind !== 'from') return null;
244
+ const relative = entry.moduleResolved?.[bindings[0].module];
245
+ return relative ? this.moduleFixture(path.join(this.index.root, relative), bindings[0].name, depth + 1) : null;
226
246
  }
227
247
 
228
248
  /** The expression a fixture provides: its single yielded/returned value,
@@ -253,11 +273,14 @@ class PythonClientResolver {
253
273
  if (!expr || expr.type !== 'identifier' || hops > MAX_HOPS) return { file, expr };
254
274
  if (fnNode) {
255
275
  const values = boundValues(field(fnNode, 'body'), expr.text, this.bindingMemo);
256
- if (values === null) return null;
276
+ if (values === null) {
277
+ const decl = routeGraph.findDecl(this.sess, file, expr.text, expr);
278
+ return decl?.kind === 'function' ? { file, expr: decl.valueNode } : null;
279
+ }
257
280
  if (values.length === 1) return this.followBinding(file, fnNode, unwrap(values[0].node), hops + 1);
258
281
  if (values.length > 1) return null;
259
282
  if (isParameterOf(fnNode, expr.text)) {
260
- const fixture = this.fixtureDef(file, expr.text);
283
+ const fixture = this.fixtureDef(file, expr.text, fnNode);
261
284
  return fixture ? this.fixtureValue(fixture.file, fixture.fnNode, hops + 1) : null;
262
285
  }
263
286
  }
@@ -319,7 +342,7 @@ class PythonClientResolver {
319
342
  if (expr.type !== 'identifier') return null;
320
343
  const fnNode = enclosingFunctionNode(expr);
321
344
  if (fnNode && isParameterOf(fnNode, expr.text)) {
322
- const fixture = this.fixtureDef(file, expr.text);
345
+ const fixture = this.fixtureDef(file, expr.text, fnNode);
323
346
  if (!fixture) return null;
324
347
  const value = this.fixtureValue(fixture.file, fixture.fnNode, hops + 1);
325
348
  if (!value?.expr) return null;
@@ -345,13 +368,8 @@ class PythonClientResolver {
345
368
  return null;
346
369
  }
347
370
 
348
- /**
349
- * { appKey } for a request `receiver.verb(path)` at `line` of `file` made
350
- * through an in-process client, { inProcess: true } when the client is
351
- * built from an app that did not resolve, else null (not a client built
352
- * from an app).
353
- */
354
- resolve(file, line, receiver, name) {
371
+ /** Values bound to a request receiver, without assuming they are clients. */
372
+ clientValues(file, line, receiver, name) {
355
373
  const root = this.sess.root(file);
356
374
  if (!root) return null;
357
375
  const sites = requestCallNodes(root, line, receiver, name);
@@ -365,7 +383,7 @@ class PythonClientResolver {
365
383
  if (values.length > 0) {
366
384
  clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
367
385
  } else if (isParameterOf(fnNode, receiver)) {
368
- const fixture = this.fixtureDef(file, receiver);
386
+ const fixture = this.fixtureDef(file, receiver, fnNode);
369
387
  const value = fixture ? this.fixtureValue(fixture.file, fixture.fnNode, 1) : null;
370
388
  clients = value ? [value] : [];
371
389
  }
@@ -376,6 +394,14 @@ class PythonClientResolver {
376
394
  clients = this.bindingInEffect(values, receiverNode).map(node => ({ file, expr: unwrap(node) }));
377
395
  }
378
396
  }
397
+ const values = clients?.map(c => this.followBinding(c.file,
398
+ enclosingFunctionNode(c.expr), c.expr, 0));
399
+ return values?.every(Boolean) ? values : null;
400
+ }
401
+
402
+ /** The served app key, an unresolved in-process app, or null. */
403
+ resolve(file, line, receiver, name) {
404
+ const clients = this.clientValues(file, line, receiver, name);
379
405
  if (!clients || clients.length === 0 || clients.some(c => c.expr?.type !== 'call')) return null;
380
406
  const keys = clients.map(c => this.clientAppOnce(c.file, c.expr));
381
407
  if (keys.every(key => key && key === keys[0])) {
@@ -476,6 +476,7 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
476
476
  const envelope = {
477
477
  meta: {
478
478
  command: surfaceCommand,
479
+ ...(execution.surface === 'cli' && { maxChars: 'not-applied' }),
479
480
  ...(surfaceCommand !== command && { canonicalCommand: command }),
480
481
  // A result that self-reports it could not run (check with a bad
481
482
  // base ref / outside git) must be machine-distinguishable from a
@@ -315,6 +315,12 @@ function formatDeadcode(results, options = {}) {
315
315
  if (results.reflection?.dynamicCount > 0) {
316
316
  lines.push(`\nWARNING: ${results.reflection.dynamicCount} dynamic reflection use(s) have no static member spelling. Dead-code results remain review candidates because runtime-selected members cannot be attributed.`);
317
317
  }
318
+ if (results.reflection?.withheldByTable?.length > 0) {
319
+ lines.push('\nConservatively withheld: exact string literals in a file with dynamic reflection (flow to the reflection call is unproven):');
320
+ for (const site of results.reflection.withheldByTable) {
321
+ lines.push(` ${site.name}: named in possible reflection table ${site.file}:${site.line}`);
322
+ }
323
+ }
318
324
  if (results.macroPaste) {
319
325
  const m = results.macroPaste;
320
326
  if (m.withheld > 0) {
@@ -1113,6 +1113,20 @@ function resetReflectionIndex(index) {
1113
1113
  index._reflectionPatternIndex = null;
1114
1114
  }
1115
1115
 
1116
+ /** Exact AST string literals, for conservative same-file dynamic dispatch.
1117
+ * This does not prove that a literal flows to a reflection call. */
1118
+ function reflectionLiteralNames(tree) {
1119
+ const names = new Map();
1120
+ if (!tree) return names;
1121
+ for (const node of tree.rootNode.descendantsOfType([...STRING_NODES])) {
1122
+ const value = classifyNameExpression(node);
1123
+ if (value.name && !value.dynamic && !names.has(value.name)) {
1124
+ names.set(value.name, node.startPosition.row + 1);
1125
+ }
1126
+ }
1127
+ return names;
1128
+ }
1129
+
1116
1130
  module.exports = {
1117
1131
  WILD,
1118
1132
  stringAlternatives,
@@ -1126,5 +1140,6 @@ module.exports = {
1126
1140
  reflectionPatternIndex,
1127
1141
  patternIsAttributable,
1128
1142
  resetReflectionIndex,
1143
+ reflectionLiteralNames,
1129
1144
  resolveKeyParameter,
1130
1145
  };
@@ -43,6 +43,8 @@ const JS_HTTP_PACKAGES = [
43
43
  ];
44
44
  const JS_HTTP_GLOBAL_CALLS = new Set(['fetch']);
45
45
  const JS_HTTP_GLOBAL_CTORS = new Set(['XMLHttpRequest', 'EventSource']);
46
+ const JS_HTTP_CLIENT_TYPES = new Set(['AxiosInstance', 'AxiosStatic', 'KyInstance',
47
+ 'Got', 'GotInstance', 'HttpClient', 'Dispatcher', 'Agent']);
46
48
  const PY_HTTP_MODULES = [
47
49
  'requests', 'httpx', 'aiohttp', 'urllib3', 'urllib.request', 'http.client', 'niquests', 'treq',
48
50
  ];
@@ -173,7 +175,7 @@ class RequestHelperProof {
173
175
  }
174
176
 
175
177
  /** Proof for the function expression being called. */
176
- jsCallee(file, fn, depth, factory = false) {
178
+ jsCallee(file, fn, depth) {
177
179
  if (!fn || depth > MAX_RESOLVE) return unknown('depth');
178
180
  if (fn.type === 'identifier') {
179
181
  const decl = findDecl(this.sess, file, fn.text, fn);
@@ -201,9 +203,7 @@ class RequestHelperProof {
201
203
  // the method reads as one (`client.post`, `instance.request`);
202
204
  // a server built on an HTTP stack (`app.route(...)`) is not a
203
205
  // client.
204
- // A factory call on a client value (`axios.create()`) yields a
205
- // client value too.
206
- if (!factory && (!prop || !CLIENT_METHOD.test(prop))) return unknown('method-not-request');
206
+ if (!prop || !CLIENT_METHOD.test(prop)) return unknown('method-not-request');
207
207
  return this.jsValue(file, obj, depth + 1);
208
208
  }
209
209
  if (fn.type === 'call_expression') return this.jsValue(file, fn, depth + 1);
@@ -296,7 +296,7 @@ class RequestHelperProof {
296
296
  const spec = this.requireSpecOf({ valueNode: e });
297
297
  return isHttpPackage(spec, JS_HTTP_PACKAGES) ? PROVEN : unknown('receiver-unproven');
298
298
  }
299
- return this.jsCallee(file, callee, depth + 1, true);
299
+ return this.jsFactory(file, callee, depth + 1);
300
300
  }
301
301
  default:
302
302
  return unknown('receiver-unproven');
@@ -314,6 +314,59 @@ class RequestHelperProof {
314
314
  return last;
315
315
  }
316
316
 
317
+ /** A request result or server is not a client just because the function
318
+ * that produced it uses an HTTP package. Follow factory return values. */
319
+ jsFactory(file, callee, depth) {
320
+ if (!callee || depth > MAX_RESOLVE) return unknown('receiver-unproven');
321
+ if (callee.type === 'member_expression') {
322
+ const name = field(callee, 'property')?.text;
323
+ if (name === 'create' || name === 'extend') {
324
+ return this.jsValue(file, unwrap(field(callee, 'object')), depth + 1);
325
+ }
326
+ return unknown('factory-unproven');
327
+ }
328
+ if (callee.type !== 'identifier') return unknown('factory-unproven');
329
+ const decl = findDecl(this.sess, file, callee.text, callee);
330
+ let target = decl ? { file, value: decl.count === 1 ? decl.valueNode : null } : null;
331
+ if (!decl) {
332
+ const binding = this.jsBinding(file, callee.text);
333
+ if (binding) {
334
+ if (binding.binding.module?.startsWith('@hey-api/client-') ||
335
+ binding.binding.module === 'openapi-fetch') return PROVEN;
336
+ const rec = this.jsImportRecord(file, binding.binding, binding.original);
337
+ if (rec) target = { file: rec.file, value: rec.decl?.valueNode || rec.valueNode };
338
+ }
339
+ }
340
+ const fn = unwrap(target?.value);
341
+ if (!fn || !JS_FUNCTION_TYPES.has(fn.type)) return unknown('factory-unproven');
342
+ const body = field(fn, 'body');
343
+ if (!body) return unknown('factory-unproven');
344
+ const values = [];
345
+ const visit = node => {
346
+ if (node.type === 'return_statement') {
347
+ values.push(node.namedChild(0));
348
+ return;
349
+ }
350
+ for (const child of namedChildren(node)) {
351
+ if (!JS_FUNCTION_TYPES.has(child.type) && child.type !== 'class_declaration') visit(child);
352
+ }
353
+ };
354
+ if (body.type === 'statement_block') visit(body);
355
+ else values.push(body);
356
+ if (!values.length) return unknown('factory-unproven');
357
+ const proven = values.every(value => {
358
+ const v = unwrap(value);
359
+ if (v?.type !== 'object') return this.jsValue(target.file, v, depth + 1).proven;
360
+ const methods = namedChildren(v).filter(member =>
361
+ CLIENT_METHOD.test((field(member, 'key') || field(member, 'name'))?.text || ''));
362
+ return methods.length > 0 && methods.every(member => {
363
+ const impl = member.type === 'method_definition' ? member : unwrap(field(member, 'value'));
364
+ return impl && JS_FUNCTION_TYPES.has(impl.type) && this.jsFunction(target.file, impl, 0).proven;
365
+ });
366
+ });
367
+ return proven ? PROVEN : unknown('factory-unproven');
368
+ }
369
+
317
370
  /** A parameter proves a client only through its declared type or default. */
318
371
  jsParam(file, decl, depth) {
319
372
  const holder = decl.nameNode.parent;
@@ -321,7 +374,8 @@ class RequestHelperProof {
321
374
  if (typeNode) {
322
375
  const m = typeNode.text.replace(/^:\s*/, '').match(/^([A-Za-z_$][\w$]*)/);
323
376
  const b = m ? this.jsBinding(file, m[1]) : null;
324
- if (b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES)) return PROVEN;
377
+ if (b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES) &&
378
+ JS_HTTP_CLIENT_TYPES.has(b.original)) return PROVEN;
325
379
  }
326
380
  const dflt = holder ? (field(holder, 'value') || field(holder, 'right')) : null;
327
381
  if (dflt) return this.jsValue(file, dflt, depth + 1);
@@ -374,6 +428,8 @@ class RequestHelperProof {
374
428
  return !!b && isHttpPackage(b.binding.module, JS_HTTP_PACKAGES);
375
429
  }
376
430
  if (callee.type === 'member_expression') {
431
+ if (CLIENT_METHOD.test(field(callee, 'property')?.text || '') &&
432
+ this.jsValue(file, unwrap(field(callee, 'object')), 0).proven) return true;
377
433
  let root = unwrap(field(callee, 'object'));
378
434
  while (root && root.type === 'member_expression') root = unwrap(field(root, 'object'));
379
435
  if (!root || root.type !== 'identifier') return false;
@@ -558,4 +614,4 @@ function locateCall(root, start, end, type) {
558
614
  return node;
559
615
  }
560
616
 
561
- module.exports = { RequestHelperProof, JS_HTTP_PACKAGES, PY_HTTP_MODULES };
617
+ module.exports = { RequestHelperProof, JS_HTTP_PACKAGES, PY_HTTP_MODULES, pyImportedFunction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.5.2",
3
+ "version": "5.5.3",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
6
6
  "main": "index.js",
@@ -87,7 +87,7 @@
87
87
  "dependencies": {
88
88
  "node-addon-api": "8.9.2",
89
89
  "node-gyp-build": "4.8.4",
90
- "re2js": "2.8.6",
90
+ "re2js": "2.8.7",
91
91
  "ret": "0.5.0",
92
92
  "safe-regex2": "5.1.1",
93
93
  "tree-sitter": "0.21.1",