ucn 5.3.1 → 5.3.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.
- package/.claude/skills/ucn/SKILL.md +19 -3
- package/core/account.js +4 -4
- package/core/bridge.js +89 -2
- package/core/cache.js +3 -1
- package/core/callers.js +195 -6
- package/core/execute.js +1 -0
- package/core/output/endpoints.js +23 -2
- package/core/project.js +26 -2
- package/core/search.js +11 -9
- package/core/shared.js +13 -1
- package/languages/java.js +24 -4
- package/languages/python.js +3 -0
- package/languages/rust.js +8 -0
- package/package.json +1 -1
|
@@ -157,9 +157,25 @@ definition, and `--raw` when the next step is an edit.
|
|
|
157
157
|
|
|
158
158
|
Treat `deadcode` as a candidate generator. Before deletion, inspect `usages`, `impact`, `entrypoints`, `api`, and `repo --sections=health --deep`; then corroborate with the compiler/type checker and tests. 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; recognized dynamic reflection is counted and warned because it cannot be attributed to one member. Unknown decorators/annotations and member-assigned event handlers are also withheld by default because they can be the registration itself. All remaining candidates are still review-only. Never delete solely from `deadcode` or an observed-text-zero result.
|
|
159
159
|
|
|
160
|
-
`usages` includes comment/string/docstring occurrences
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
`usages` includes comment/string/docstring occurrences and non-code text
|
|
161
|
+
(JSX children, HTML markup and attributes) in an `OTHER TEXT` section unless
|
|
162
|
+
`--code-only` is set, so it lists every line the `ACCOUNT` counts. This is a
|
|
163
|
+
literal-name inventory, not exact target binding. Identifier boundaries are
|
|
164
|
+
Unicode-aware and match `grep -w`: `hit` never matches inside `hitΔ`, while
|
|
165
|
+
`$` is a boundary (`buy${...}`, `$fail`, `ws$close()` all count).
|
|
166
|
+
|
|
167
|
+
Aliased and qualified calls resolve in every language: Rust `use m::f as g; g()`
|
|
168
|
+
is a caller of `f` (listed as a beyond-text caller, since the line holds no
|
|
169
|
+
target token); Java `pkg.Type.method()` and C# `Ns.Type.Method()` /
|
|
170
|
+
`using T = Ns.Type; T.Method()` pick the type the qualifier names when several
|
|
171
|
+
same-name types exist. A qualifier the resolver cannot place stays visible as
|
|
172
|
+
`method-ambiguous`, never confirmed by first-definition order.
|
|
173
|
+
|
|
174
|
+
`endpoints` recognizes client receivers by evidence (a receiver typed to an
|
|
175
|
+
HTTP client class, or bound to a pytest fixture that constructs one), not only
|
|
176
|
+
by name. Request-shaped calls with a path literal on an unrecognized receiver
|
|
177
|
+
are listed under `Possible client requests` (JSON `uncertainRequests`),
|
|
178
|
+
counted in `meta`, never in the inventory. `search` treats its term literally by default; pass `--regex` only
|
|
163
179
|
when regular-expression semantics are intended. Ordinary regex patterns run
|
|
164
180
|
through an RE2-compatible linear-time engine; unsafe nested repetition is
|
|
165
181
|
rejected, and unsupported advanced syntax should be handed to ripgrep.
|
package/core/account.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const path = require('path');
|
|
55
|
-
const {
|
|
55
|
+
const { codeUnitCompare, literalNameRegex } = require('./shared');
|
|
56
56
|
|
|
57
57
|
// Unsupported-language site listings are capped so a Rails-sized repo cannot
|
|
58
58
|
// flood the account object; the counts always cover the full set.
|
|
@@ -83,7 +83,7 @@ function computeGroundSet(index, name) {
|
|
|
83
83
|
index._groundSetCache.set(name, cached);
|
|
84
84
|
return cached.result;
|
|
85
85
|
}
|
|
86
|
-
const wordRe =
|
|
86
|
+
const wordRe = literalNameRegex(name);
|
|
87
87
|
const perFile = new Map();
|
|
88
88
|
let total = 0;
|
|
89
89
|
let fileCount = 0;
|
|
@@ -158,7 +158,7 @@ function scanFailedFiles(index, name) {
|
|
|
158
158
|
if (!index.failedFiles || index.failedFiles.size === 0) {
|
|
159
159
|
return { unparsed, unreadableFiles };
|
|
160
160
|
}
|
|
161
|
-
const wordRe =
|
|
161
|
+
const wordRe = literalNameRegex(name);
|
|
162
162
|
for (const failedPath of index.failedFiles) {
|
|
163
163
|
if (index.files.has(failedPath)) continue;
|
|
164
164
|
let content;
|
|
@@ -204,7 +204,7 @@ function scanUnsupportedFiles(index, name, opts = {}) {
|
|
|
204
204
|
if (!Array.isArray(index.unsupportedFiles) || index.unsupportedFiles.length === 0) {
|
|
205
205
|
return unsupported;
|
|
206
206
|
}
|
|
207
|
-
const wordRe =
|
|
207
|
+
const wordRe = literalNameRegex(name);
|
|
208
208
|
const languageCounts = new Map();
|
|
209
209
|
for (const skipped of index.unsupportedFiles) {
|
|
210
210
|
const absPath = path.join(index.root, skipped.relativePath);
|
package/core/bridge.js
CHANGED
|
@@ -1066,11 +1066,60 @@ function extractNextjsRoutes(index) {
|
|
|
1066
1066
|
* Detect HTTP client requests across the project.
|
|
1067
1067
|
* Cached on `index._endpointsCache.clientRequests`.
|
|
1068
1068
|
*/
|
|
1069
|
+
// Python HTTP client types whose instances are receivers of request calls.
|
|
1070
|
+
// A receiver typed to one of these (with-binding, constructor assignment) or
|
|
1071
|
+
// bound to a pytest fixture that constructs one is a client by evidence, not
|
|
1072
|
+
// by name (fix #349: 145 of 625 real `tc.get("/api/...")` sites on one repo
|
|
1073
|
+
// were invisible because the receiver was not literally named `client`).
|
|
1074
|
+
const PY_CLIENT_TYPES = new Set([
|
|
1075
|
+
'TestClient', 'Client', 'AsyncClient', 'Session', 'FlaskClient',
|
|
1076
|
+
'ClientSession', 'AsyncSession', 'HTTPConnection', 'HTTPSConnection',
|
|
1077
|
+
]);
|
|
1078
|
+
const PY_CLIENT_FACTORIES = new Set(['test_client', 'Session', 'Client', 'AsyncClient']);
|
|
1079
|
+
const PY_REQUEST_METHODS = /^(get|post|put|delete|patch|options|head|request)$/;
|
|
1080
|
+
|
|
1081
|
+
function _pythonClientFixtures(index) {
|
|
1082
|
+
if (index._endpointsCache?.pyClientFixtures) return index._endpointsCache.pyClientFixtures;
|
|
1083
|
+
const fixtures = new Set();
|
|
1084
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
1085
|
+
if (fileEntry.language !== 'python') continue;
|
|
1086
|
+
const fixtureDefs = (fileEntry.symbols || []).filter(s =>
|
|
1087
|
+
(s.decorators || []).some(d => /(^|\.)fixture$/.test(String(d))));
|
|
1088
|
+
if (fixtureDefs.length === 0) continue;
|
|
1089
|
+
const calls = getCachedCalls(index, filePath) || [];
|
|
1090
|
+
for (const def of fixtureDefs) {
|
|
1091
|
+
const constructsClient = calls.some(c =>
|
|
1092
|
+
c.enclosingFunction?.name === def.name &&
|
|
1093
|
+
c.enclosingFunction?.startLine === def.startLine &&
|
|
1094
|
+
(PY_CLIENT_TYPES.has(c.name) || (c.isMethod && PY_CLIENT_FACTORIES.has(c.name))));
|
|
1095
|
+
if (constructsClient) fixtures.add(def.name);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (!index._endpointsCache) index._endpointsCache = {};
|
|
1099
|
+
index._endpointsCache.pyClientFixtures = fixtures;
|
|
1100
|
+
return fixtures;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function _pythonClientReceiver(index, fileEntry, call, fixtures) {
|
|
1104
|
+
if (!call.isMethod || !call.receiver || !PY_REQUEST_METHODS.test(call.name)) return null;
|
|
1105
|
+
if (call.receiverType && PY_CLIENT_TYPES.has(String(call.receiverType).split('.').pop())) {
|
|
1106
|
+
return 'python-client';
|
|
1107
|
+
}
|
|
1108
|
+
if (fixtures.size === 0 || !fixtures.has(call.receiver)) return null;
|
|
1109
|
+
const fn = call.enclosingFunction;
|
|
1110
|
+
if (!fn) return null;
|
|
1111
|
+
const sym = (fileEntry.symbols || []).find(s => s.name === fn.name && s.startLine === fn.startLine);
|
|
1112
|
+
const params = String(sym?.params || '').split(',').map(p => p.trim().split(/[:=]/)[0].trim());
|
|
1113
|
+
return params.includes(call.receiver) ? 'pytest-client-fixture' : null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1069
1116
|
function extractClientRequests(index) {
|
|
1070
1117
|
if (index._endpointsCache && index._endpointsCache.clientRequests) {
|
|
1071
1118
|
return index._endpointsCache.clientRequests;
|
|
1072
1119
|
}
|
|
1073
1120
|
const requests = [];
|
|
1121
|
+
const uncertain = [];
|
|
1122
|
+
const pyFixtures = _pythonClientFixtures(index);
|
|
1074
1123
|
|
|
1075
1124
|
for (const [filePath, fileEntry] of index.files) {
|
|
1076
1125
|
const lang = fileEntry.language;
|
|
@@ -1079,8 +1128,30 @@ function extractClientRequests(index) {
|
|
|
1079
1128
|
|
|
1080
1129
|
for (const call of calls) {
|
|
1081
1130
|
if (!call.firstStringArg) continue;
|
|
1082
|
-
|
|
1083
|
-
if (!r)
|
|
1131
|
+
let r = matchClientRequest(call, lang, calls);
|
|
1132
|
+
if (!r && lang === 'python') {
|
|
1133
|
+
const framework = _pythonClientReceiver(index, fileEntry, call, pyFixtures);
|
|
1134
|
+
if (framework) {
|
|
1135
|
+
r = { method: call.name.toUpperCase() === 'REQUEST' ? 'ALL' : call.name.toUpperCase(),
|
|
1136
|
+
framework, methodInferred: call.name === 'request' };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (!r) {
|
|
1140
|
+
// Visible uncertainty: request-shaped call on an unrecognized
|
|
1141
|
+
// receiver with a path-shaped literal. Listed, never counted.
|
|
1142
|
+
const conf = CLIENT_PATTERNS[lang];
|
|
1143
|
+
const pathShaped = call.firstStringArg.startsWith('/') || call.firstStringArg.includes('://');
|
|
1144
|
+
if (conf && call.isMethod && call.receiver && pathShaped &&
|
|
1145
|
+
conf.receivers.some(p => p.methodPattern.test(call.name))) {
|
|
1146
|
+
uncertain.push({
|
|
1147
|
+
receiver: call.receiver, method: call.name, path: call.firstStringArg,
|
|
1148
|
+
file: fileEntry.relativePath || filePath, absoluteFile: filePath, line: call.line,
|
|
1149
|
+
callerName: call.enclosingFunction?.name || '<top-level>',
|
|
1150
|
+
reason: 'receiver-unrecognized',
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1084
1155
|
|
|
1085
1156
|
// Python's common `session.get("key")` / `s.get("key")`
|
|
1086
1157
|
// dictionary and ORM idioms are not HTTP requests. Without
|
|
@@ -1114,6 +1185,9 @@ function extractClientRequests(index) {
|
|
|
1114
1185
|
}
|
|
1115
1186
|
|
|
1116
1187
|
// Stable sort
|
|
1188
|
+
uncertain.sort((a, b) => a.file !== b.file ? codeUnitCompare(a.file, b.file) : a.line - b.line);
|
|
1189
|
+
if (!index._endpointsCache) index._endpointsCache = {};
|
|
1190
|
+
index._endpointsCache.uncertainRequests = uncertain;
|
|
1117
1191
|
requests.sort((a, b) => {
|
|
1118
1192
|
if (a.file !== b.file) return codeUnitCompare(a.file, b.file);
|
|
1119
1193
|
if (a.line !== b.line) return a.line - b.line;
|
|
@@ -1418,6 +1492,13 @@ function endpoints(index, options = {}) {
|
|
|
1418
1492
|
|
|
1419
1493
|
let routes = opts.clientOnly ? [] : extractServerRoutes(index);
|
|
1420
1494
|
let requests = (opts.serverOnly ? [] : extractClientRequests(index));
|
|
1495
|
+
let uncertainRequests = opts.serverOnly ? [] : (index._endpointsCache?.uncertainRequests || []);
|
|
1496
|
+
if (uncertainRequests.length > 0) {
|
|
1497
|
+
// A server route registration (`@app.get("/x")`, `router.get("/x", h)`)
|
|
1498
|
+
// is request-shaped too; the route inventory already owns those lines.
|
|
1499
|
+
const routeLines = new Set(extractServerRoutes(index).map(r => `${r.absoluteFile}:${r.line}`));
|
|
1500
|
+
uncertainRequests = uncertainRequests.filter(r => !routeLines.has(`${r.absoluteFile}:${r.line}`));
|
|
1501
|
+
}
|
|
1421
1502
|
|
|
1422
1503
|
// Apply filters
|
|
1423
1504
|
if (opts.method) {
|
|
@@ -1427,6 +1508,10 @@ function endpoints(index, options = {}) {
|
|
|
1427
1508
|
if (opts.prefix) {
|
|
1428
1509
|
routes = routes.filter(r => r.path.startsWith(opts.prefix) || r.normalizedPath.startsWith(opts.prefix));
|
|
1429
1510
|
requests = requests.filter(r => r.path.startsWith(opts.prefix) || r.normalizedPath.startsWith(opts.prefix));
|
|
1511
|
+
uncertainRequests = uncertainRequests.filter(r => r.path.startsWith(opts.prefix));
|
|
1512
|
+
}
|
|
1513
|
+
if (opts.method) {
|
|
1514
|
+
uncertainRequests = uncertainRequests.filter(r => r.method.toUpperCase() === opts.method || r.method === 'request');
|
|
1430
1515
|
}
|
|
1431
1516
|
|
|
1432
1517
|
let bridges = opts.bridge ? bridgeEndpoints(index) : [];
|
|
@@ -1472,12 +1557,14 @@ function endpoints(index, options = {}) {
|
|
|
1472
1557
|
: 'incomplete-endpoint-inventory',
|
|
1473
1558
|
routes,
|
|
1474
1559
|
requests,
|
|
1560
|
+
uncertainRequests,
|
|
1475
1561
|
bridges,
|
|
1476
1562
|
unmatchedRoutes,
|
|
1477
1563
|
unmatchedRequests,
|
|
1478
1564
|
meta: {
|
|
1479
1565
|
totalRoutes: routes.length,
|
|
1480
1566
|
totalRequests: requests.length,
|
|
1567
|
+
uncertainRequests: uncertainRequests.length,
|
|
1481
1568
|
totalBridges: bridges.length,
|
|
1482
1569
|
unmatchedRoutes: unmatchedRoutes.length,
|
|
1483
1570
|
unmatchedRequests: unmatchedRequests.length,
|
package/core/cache.js
CHANGED
|
@@ -697,7 +697,9 @@ function clearAllCaches() {
|
|
|
697
697
|
// default/type imports and inline type re-exports preserve execution timing.
|
|
698
698
|
// Python TYPE_CHECKING guards require typing ownership and no rebinding.
|
|
699
699
|
// v212 (fix #342): extendsGraph/extendedByGraph no longer persisted (rebuilt on load).
|
|
700
|
-
|
|
700
|
+
// v213 (fix #348): Python from-import records carry per-name `renames` so
|
|
701
|
+
// import bindings pair each alias with its own module.
|
|
702
|
+
const CACHE_FORMAT_VERSION = 214;
|
|
701
703
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
702
704
|
|
|
703
705
|
/**
|
package/core/callers.js
CHANGED
|
@@ -10,7 +10,8 @@ const path = require('path');
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
-
const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath } = require('./shared');
|
|
13
|
+
const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
14
|
+
const { _resolveJavaPackageImport } = require('./graph-build');
|
|
14
15
|
const { scoreEdge, tierForResolution, TIER } = require('./confidence');
|
|
15
16
|
const { findGoModule, resolveRustImport } = require('./imports');
|
|
16
17
|
|
|
@@ -634,6 +635,17 @@ function findCallers(index, name, options = {}) {
|
|
|
634
635
|
langTraits(fileEntry.language)?.typeSystem === 'structural';
|
|
635
636
|
|
|
636
637
|
for (let call of calls) {
|
|
638
|
+
// fix #353: C# `Beta.Helper.Widget()` — the parser records a
|
|
639
|
+
// field hop rooted at `this` (Beta is no local). When the
|
|
640
|
+
// prefix names a project NAMESPACE that declares the last
|
|
641
|
+
// segment as a type, the receiver is that type, namespace-
|
|
642
|
+
// qualified; the hop shape would only ever fail the field
|
|
643
|
+
// walk and route method-ambiguous.
|
|
644
|
+
if (fileEntry.language === 'csharp') {
|
|
645
|
+
const rewritten = _csharpNamespaceQualifiedReceiver(index, call,
|
|
646
|
+
index.findEnclosingFunction(filePath, call.line, true));
|
|
647
|
+
if (rewritten) call = rewritten;
|
|
648
|
+
}
|
|
637
649
|
// Skip if not matching our target name (also check alias resolution)
|
|
638
650
|
let calledAs = null; // surface name when matched via an import/export rename
|
|
639
651
|
const typeQualifierReference = targetIsTypeQuery &&
|
|
@@ -2802,10 +2814,22 @@ function findCallers(index, name, options = {}) {
|
|
|
2802
2814
|
// source name from DIFFERENT modules — the record's local
|
|
2803
2815
|
// alias (call.name) picks its own binding; source-name
|
|
2804
2816
|
// matching alone over-follows into the other module.
|
|
2805
|
-
|
|
2817
|
+
// Python records carry the alias as `calledAs` rather
|
|
2818
|
+
// than resolvedName; both name the paired binding.
|
|
2819
|
+
if ((call.resolvedName || calledAs) && nameBindings.some(b => b.alias)) {
|
|
2806
2820
|
const paired = nameBindings.filter(b => b.alias === call.name);
|
|
2807
2821
|
if (paired.length > 0) nameBindings = paired;
|
|
2808
2822
|
}
|
|
2823
|
+
// Scope-granular import bindings (fix #352): a
|
|
2824
|
+
// function-local `from x import name` binds the name for
|
|
2825
|
+
// ITS function only. Three such imports in one file used
|
|
2826
|
+
// to make three file-level bindings, so a bare call in any
|
|
2827
|
+
// of the functions scope-matched every pin (investment
|
|
2828
|
+
// run_validation: 6 defs, each claiming the others'
|
|
2829
|
+
// sites). The nearest enclosing binder owns the name; a
|
|
2830
|
+
// binding inside a function that does not enclose the
|
|
2831
|
+
// call is out of scope (#215 discipline, line-granular).
|
|
2832
|
+
nameBindings = _scopeImportBindings(fileEntry, nameBindings, call.line);
|
|
2809
2833
|
const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
2810
2834
|
// fix #215 (rich-measured: 225 builtin `print(...)` calls
|
|
2811
2835
|
// confirmed against rich's def via file-level import edges):
|
|
@@ -4073,12 +4097,17 @@ function findCallers(index, name, options = {}) {
|
|
|
4073
4097
|
if (call.isPathCall && receiverName) {
|
|
4074
4098
|
receiverName = String(receiverName).split('::').pop();
|
|
4075
4099
|
}
|
|
4100
|
+
let aliasResolvedFile = null;
|
|
4076
4101
|
if (receiverName && !tTypes.has(receiverName)) {
|
|
4077
4102
|
for (const im of (fileEntry.importBindings || [])) {
|
|
4078
4103
|
if (im.name !== receiverName) continue;
|
|
4079
|
-
|
|
4104
|
+
// fix #353: C# `using BH = Beta.Helper` (and Java
|
|
4105
|
+
// dotted paths) split on `.`; Rust paths on `::`.
|
|
4106
|
+
const orig = String(im.module || '').split(/::|\./).pop();
|
|
4080
4107
|
if (orig && orig !== receiverName && tTypes.has(orig)) {
|
|
4081
4108
|
receiverName = orig;
|
|
4109
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[im.module];
|
|
4110
|
+
if (rel) aliasResolvedFile = path.join(index.root, rel);
|
|
4082
4111
|
break;
|
|
4083
4112
|
}
|
|
4084
4113
|
}
|
|
@@ -4106,7 +4135,14 @@ function findCallers(index, name, options = {}) {
|
|
|
4106
4135
|
// name fallback above handles multi-definition names; this
|
|
4107
4136
|
// covers single-definition targets that skip it.
|
|
4108
4137
|
if (typeQualifiedReceiver) {
|
|
4109
|
-
|
|
4138
|
+
// fix #353: an alias binding that RESOLVED to a file is
|
|
4139
|
+
// the type's identity (`using BH = Beta.Helper`); a
|
|
4140
|
+
// parser-recorded qualifier (`beta.Helper`, `Beta.Helper`)
|
|
4141
|
+
// is a namespace/package hint for the resolver.
|
|
4142
|
+
const identity = aliasResolvedFile
|
|
4143
|
+
? (targetDefs2.some(d => d.file === aliasResolvedFile) ? 'target' : 'other')
|
|
4144
|
+
: _resolveReceiverTypeIdentity(index, filePath, receiverName, targetDefs2, call.line,
|
|
4145
|
+
call.receiverIsTypeQualified ? call.receiverTypeQualifier : undefined);
|
|
4110
4146
|
if (identity === 'other') {
|
|
4111
4147
|
recordExcluded(filePath, call.line, 'path-type-mismatch');
|
|
4112
4148
|
continue;
|
|
@@ -5342,6 +5378,12 @@ function findCallees(index, definition, options = {}) {
|
|
|
5342
5378
|
for (let call of calls) {
|
|
5343
5379
|
siteOrdinal++;
|
|
5344
5380
|
const siteId = siteOrdinal;
|
|
5381
|
+
if (language === 'csharp') {
|
|
5382
|
+
// fix #353: `Beta.Helper.Widget()` — namespace-qualified type
|
|
5383
|
+
// receiver (see the findCallers twin).
|
|
5384
|
+
const rewritten = _csharpNamespaceQualifiedReceiver(index, call, def);
|
|
5385
|
+
if (rewritten) call = rewritten;
|
|
5386
|
+
}
|
|
5345
5387
|
if (language === 'go' && call.isMethod &&
|
|
5346
5388
|
!call.receiverType && call.receiverIndexField) {
|
|
5347
5389
|
const indexedType = _goIndexedReceiverType(index, def.file, call);
|
|
@@ -9851,6 +9893,42 @@ function _projectTopLevelNames(index) {
|
|
|
9851
9893
|
return names;
|
|
9852
9894
|
}
|
|
9853
9895
|
|
|
9896
|
+
/**
|
|
9897
|
+
* Fix #352: restrict import bindings of a name to those in scope at a call
|
|
9898
|
+
* line. A binding whose import line sits inside a function body is local to
|
|
9899
|
+
* that function (Python function-body imports, JS function-scoped require);
|
|
9900
|
+
* the innermost enclosing binder wins, bindings in non-enclosing functions
|
|
9901
|
+
* are dropped, module-level bindings survive only when no enclosing function
|
|
9902
|
+
* binds the name. Bindings without a line (older records) are kept as-is.
|
|
9903
|
+
*/
|
|
9904
|
+
function _scopeImportBindings(fileEntry, bindings, callLine) {
|
|
9905
|
+
if (!bindings || bindings.length < 2 || callLine == null) return bindings;
|
|
9906
|
+
if (!bindings.some(b => b.line != null && b.deferred)) return bindings;
|
|
9907
|
+
const scopes = (fileEntry.symbols || []).filter(s =>
|
|
9908
|
+
s.startLine != null && s.endLine != null && s.endLine > s.startLine &&
|
|
9909
|
+
CALLABLE_SYMBOL_KINDS.has(s.type));
|
|
9910
|
+
const innermost = line => {
|
|
9911
|
+
let best = null;
|
|
9912
|
+
for (const s of scopes) {
|
|
9913
|
+
if (line < s.startLine || line > s.endLine) continue;
|
|
9914
|
+
if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine)) best = s;
|
|
9915
|
+
}
|
|
9916
|
+
return best;
|
|
9917
|
+
};
|
|
9918
|
+
const local = [];
|
|
9919
|
+
const moduleLevel = [];
|
|
9920
|
+
for (const b of bindings) {
|
|
9921
|
+
if (b.line == null) { moduleLevel.push(b); continue; }
|
|
9922
|
+
const scope = innermost(b.line);
|
|
9923
|
+
if (!scope) { moduleLevel.push(b); continue; }
|
|
9924
|
+
if (callLine < scope.startLine || callLine > scope.endLine) continue;
|
|
9925
|
+
local.push({ b, size: scope.endLine - scope.startLine });
|
|
9926
|
+
}
|
|
9927
|
+
if (local.length === 0) return moduleLevel;
|
|
9928
|
+
const nearest = Math.min(...local.map(l => l.size));
|
|
9929
|
+
return local.filter(l => l.size === nearest).map(l => l.b);
|
|
9930
|
+
}
|
|
9931
|
+
|
|
9854
9932
|
/**
|
|
9855
9933
|
* Is an UNRESOLVED module specifier a resolver gap rather than externality
|
|
9856
9934
|
* evidence? (fix #337b) Relative specifiers and first segments naming a
|
|
@@ -9978,6 +10056,32 @@ function _isGenericParamReceiverType(index, filePath, line, typeName) {
|
|
|
9978
10056
|
return _isEnclosingGenericParam(index, filePath, line, typeName);
|
|
9979
10057
|
}
|
|
9980
10058
|
|
|
10059
|
+
/**
|
|
10060
|
+
* fix #353: C# namespace-qualified type receivers. `Beta.Helper.Widget()` is
|
|
10061
|
+
* recorded by the parser as a this-rooted field hop (Beta is not a local);
|
|
10062
|
+
* when the dotted prefix names a project namespace (exactly, or relative to
|
|
10063
|
+
* the call's own namespace) that declares the last segment as a type, the
|
|
10064
|
+
* call is a type-qualified static call on that type. Returns a rewritten
|
|
10065
|
+
* record or null (unknown prefixes keep the parser's shape).
|
|
10066
|
+
*/
|
|
10067
|
+
function _csharpNamespaceQualifiedReceiver(index, call, enclosing) {
|
|
10068
|
+
if (!call.isMethod || call.receiverType || !Array.isArray(call.receiverFields) ||
|
|
10069
|
+
call.receiverFields.length < 2 || call.receiverRoot !== 'this') return null;
|
|
10070
|
+
const typeName = call.receiverFields[call.receiverFields.length - 1];
|
|
10071
|
+
if (!/^[A-Z]/.test(typeName)) return null;
|
|
10072
|
+
const prefix = call.receiverFields.slice(0, -1).join('.');
|
|
10073
|
+
const typeDefs = (index.symbols.get(typeName) || []).filter(d =>
|
|
10074
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.namespace);
|
|
10075
|
+
if (typeDefs.length === 0) return null;
|
|
10076
|
+
const enclosingNs = enclosing?.namespace || null;
|
|
10077
|
+
const candidates = enclosingNs ? [prefix, `${enclosingNs}.${prefix}`] : [prefix];
|
|
10078
|
+
const ns = candidates.find(c => typeDefs.some(d => d.namespace === c));
|
|
10079
|
+
if (!ns) return null;
|
|
10080
|
+
const { receiverRoot, receiverField, receiverFields, receiverRootType, receiverRootNamespace, ...rest } = call;
|
|
10081
|
+
void receiverRoot; void receiverField; void receiverFields; void receiverRootType; void receiverRootNamespace;
|
|
10082
|
+
return { ...rest, receiver: typeName, receiverIsTypeQualified: true, receiverTypeQualifier: ns };
|
|
10083
|
+
}
|
|
10084
|
+
|
|
9981
10085
|
/**
|
|
9982
10086
|
* Java same-package check across Maven/Gradle source roots (fix #246):
|
|
9983
10087
|
* src/main/java/<pkg> and src/test/java/<pkg> hold the SAME package —
|
|
@@ -10017,6 +10121,16 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs, li
|
|
|
10017
10121
|
// cannot resolve it. That is not exclusion evidence.
|
|
10018
10122
|
return 'unknown';
|
|
10019
10123
|
}
|
|
10124
|
+
if (language === 'java' && namespaceHint && /^[a-z_]/.test(namespaceHint)) {
|
|
10125
|
+
// fix #353: a lowercase dotted qualifier is a PACKAGE
|
|
10126
|
+
// (`beta.Helper.widget()`); the package + type name resolve to one
|
|
10127
|
+
// file exactly like an import of `beta.Helper` would. Unresolvable
|
|
10128
|
+
// packages (external, resolver gap) are never exclusion evidence.
|
|
10129
|
+
const resolved = _resolveJavaPackageImport(index, `${namespaceHint}.${knownType}`, null);
|
|
10130
|
+
if (!resolved) return 'unknown';
|
|
10131
|
+
return targetDefs.some(d => d.className === knownType && d.file === resolved)
|
|
10132
|
+
? 'target' : 'other';
|
|
10133
|
+
}
|
|
10020
10134
|
if (language === 'java' && line == null) {
|
|
10021
10135
|
// Return-flow annotations are interpreted in the PRODUCER definition's
|
|
10022
10136
|
// file. That origin is stronger than same-package lookup and is
|
|
@@ -10963,6 +11077,23 @@ function _calleeStructuralBindingRoute(index, fileEntry, call, language, binding
|
|
|
10963
11077
|
let sawProjectish = false;
|
|
10964
11078
|
let sawUnknown = false;
|
|
10965
11079
|
for (const binding of bindings) {
|
|
11080
|
+
// fix #353 (Rust): `use alpha::widget as renamed; renamed()` — the
|
|
11081
|
+
// binding's module is the ITEM path; the item is its last segment
|
|
11082
|
+
// and the owning module file resolves through the Rust resolver.
|
|
11083
|
+
if (language === 'rust') {
|
|
11084
|
+
const segs = String(binding.module || '').split('::').filter(Boolean);
|
|
11085
|
+
const item = segs[segs.length - 1];
|
|
11086
|
+
const owners = _rustBindingResolvedFiles(index, fileEntry, fileEntry.path, binding);
|
|
11087
|
+
if (item && owners.size > 0) {
|
|
11088
|
+
sawProjectish = true;
|
|
11089
|
+
for (const moduleFile of owners) {
|
|
11090
|
+
const routed = _calleeExportDefinitions(index, moduleFile, item, language, call, {});
|
|
11091
|
+
for (const d of routed.matches) matches.set(`${d.file}:${d.startLine}`, d);
|
|
11092
|
+
if (routed.unknown) sawUnknown = true;
|
|
11093
|
+
}
|
|
11094
|
+
continue;
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
10966
11097
|
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
|
|
10967
11098
|
if (!rel) {
|
|
10968
11099
|
if (_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
@@ -11720,6 +11851,42 @@ function _nonCallableFieldMember(index, typeName, name, language) {
|
|
|
11720
11851
|
* (#215): the class defined in this file or a file binding of the name —
|
|
11721
11852
|
* an unbound capitalized receiver may be a parameter or local.
|
|
11722
11853
|
*/
|
|
11854
|
+
/**
|
|
11855
|
+
* fix #353: resolve the files that OWN a type-qualified static receiver from
|
|
11856
|
+
* the qualifier in the call (Java package / C# namespace), the file's alias
|
|
11857
|
+
* or name import binding of the receiver, or the caller's own namespace.
|
|
11858
|
+
* Returns a Set of absolute files, or null when nothing pins the owner.
|
|
11859
|
+
*/
|
|
11860
|
+
function _qualifiedStaticOwnerFiles(index, fileEntry, call, typeName, language, def) {
|
|
11861
|
+
const files = new Set();
|
|
11862
|
+
const typeDefs = (index.symbols.get(typeName) || []).filter(d =>
|
|
11863
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.file);
|
|
11864
|
+
const qual = call.receiverIsTypeQualified ? call.receiverTypeQualifier : null;
|
|
11865
|
+
if (qual) {
|
|
11866
|
+
if (language === 'java' && /^[a-z_]/.test(qual)) {
|
|
11867
|
+
const resolved = _resolveJavaPackageImport(index, `${qual}.${typeName}`, null);
|
|
11868
|
+
if (resolved) files.add(resolved);
|
|
11869
|
+
return files.size > 0 ? files : null;
|
|
11870
|
+
}
|
|
11871
|
+
if (language === 'csharp') {
|
|
11872
|
+
const enclosingNs = def?.namespace || null;
|
|
11873
|
+
const candidates = enclosingNs ? [qual, `${enclosingNs}.${qual}`] : [qual];
|
|
11874
|
+
for (const d of typeDefs) {
|
|
11875
|
+
if (candidates.includes(d.namespace)) files.add(d.file);
|
|
11876
|
+
}
|
|
11877
|
+
return files.size > 0 ? files : null;
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
for (const b of (fileEntry?.importBindings || [])) {
|
|
11881
|
+
const bindsReceiver = b.name === call.receiver || b.alias === call.receiver ||
|
|
11882
|
+
(b.name === typeName && !b.alias);
|
|
11883
|
+
if (!bindsReceiver) continue;
|
|
11884
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[b.module];
|
|
11885
|
+
if (rel) files.add(path.join(index.root, rel));
|
|
11886
|
+
}
|
|
11887
|
+
return files.size > 0 ? files : null;
|
|
11888
|
+
}
|
|
11889
|
+
|
|
11723
11890
|
/**
|
|
11724
11891
|
* Namespace/module-container resolution (fix #254, W8 BUG-4 — verify's
|
|
11725
11892
|
* BUG-BX rule brought into the engine, range-based): `Utils.slug()` where a
|
|
@@ -11833,7 +12000,8 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
11833
12000
|
if (typeDefs.length === 0) {
|
|
11834
12001
|
for (const im of (fileEntry?.importBindings || [])) {
|
|
11835
12002
|
if (im.name !== receiver) continue;
|
|
11836
|
-
|
|
12003
|
+
// fix #353: C# `using BH = Beta.Helper` splits on `.`
|
|
12004
|
+
const orig = String(im.module || '').split(/::|\./).pop();
|
|
11837
12005
|
if (orig && orig !== receiver && typeKindsOf(orig).length > 0) {
|
|
11838
12006
|
receiver = orig;
|
|
11839
12007
|
typeDefs = typeKindsOf(orig);
|
|
@@ -11875,9 +12043,30 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
11875
12043
|
_normalizedAliasBase(index, d)));
|
|
11876
12044
|
if (bases.size === 1) candidateTypes.push(bases.values().next().value);
|
|
11877
12045
|
}
|
|
11878
|
-
const
|
|
12046
|
+
const allSymbols = index.symbols.get(call.name) || [];
|
|
11879
12047
|
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
11880
12048
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
12049
|
+
// fix #353: the qualifier that is right there in the call owns the type —
|
|
12050
|
+
// `beta.Helper.widget()` (package), `Beta.Helper.Widget()` (namespace),
|
|
12051
|
+
// `using BH = Beta.Helper; BH.Widget()` (alias binding), `import
|
|
12052
|
+
// beta.Helper;` (name binding). Same-name types in other packages/
|
|
12053
|
+
// namespaces leave the candidate set BEFORE member-group construction
|
|
12054
|
+
// (the group dedupes identical signatures, so the first same-name type
|
|
12055
|
+
// used to swallow the second). An unresolvable qualifier keeps them all.
|
|
12056
|
+
const ownerFiles = (language === 'java' || language === 'csharp')
|
|
12057
|
+
? _qualifiedStaticOwnerFiles(index, fileEntry, call, receiver, language, def)
|
|
12058
|
+
: null;
|
|
12059
|
+
// A package qualifier the resolver cannot place (`org.external.Helper`)
|
|
12060
|
+
// over a project type of the same name: unpinnable — visible, never
|
|
12061
|
+
// confirmed by first-definition selection (#206 discipline).
|
|
12062
|
+
if (language === 'java' && call.receiverIsTypeQualified &&
|
|
12063
|
+
call.receiverTypeQualifier && /^[a-z_]/.test(call.receiverTypeQualifier) &&
|
|
12064
|
+
!ownerFiles) {
|
|
12065
|
+
return { unverified: 'method-ambiguous' };
|
|
12066
|
+
}
|
|
12067
|
+
const symbols = ownerFiles
|
|
12068
|
+
? allSymbols.filter(s => !candidateTypes.includes(s.className) || ownerFiles.has(s.file))
|
|
12069
|
+
: allSymbols;
|
|
11881
12070
|
if (language === 'java' || language === 'csharp') {
|
|
11882
12071
|
// Class-qualified Java/C# calls see the compiler member group on the
|
|
11883
12072
|
// qualifier. The helper handles inherited slots and C# name hiding.
|
package/core/execute.js
CHANGED
|
@@ -1836,6 +1836,7 @@ const HANDLERS = {
|
|
|
1836
1836
|
result.meta = {
|
|
1837
1837
|
totalRoutes: result.routes.length,
|
|
1838
1838
|
totalRequests: result.requests.length,
|
|
1839
|
+
uncertainRequests: (result.uncertainRequests || []).length,
|
|
1839
1840
|
totalBridges: result.bridges.length,
|
|
1840
1841
|
unmatchedRoutes: result.unmatchedRoutes.length,
|
|
1841
1842
|
unmatchedRequests: result.unmatchedRequests.length,
|
package/core/output/endpoints.js
CHANGED
|
@@ -26,7 +26,7 @@ function formatEndpoints(result, options = {}) {
|
|
|
26
26
|
const showBridge = options.bridge;
|
|
27
27
|
|
|
28
28
|
if (!showBridge) {
|
|
29
|
-
return formatRoutesAndRequests(routes, requests, meta, options, result.advisory);
|
|
29
|
+
return formatRoutesAndRequests(routes, requests, meta, options, result.advisory, result);
|
|
30
30
|
}
|
|
31
31
|
return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options, result.advisory);
|
|
32
32
|
}
|
|
@@ -48,7 +48,20 @@ function uniqueMatchPercent(bridges, totalRequests) {
|
|
|
48
48
|
return Math.min(100, Math.max(0, pct));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
51
|
+
function formatUncertainRequests(result, options) {
|
|
52
|
+
const list = result?.uncertainRequests || [];
|
|
53
|
+
if (list.length === 0) return [];
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push(`Possible client requests (${list.length}) — request-shaped call with a path literal, receiver not recognized as an HTTP client:`);
|
|
56
|
+
const cap = options.all ? Infinity : 10;
|
|
57
|
+
for (const r of list.slice(0, cap)) {
|
|
58
|
+
lines.push(` ${r.file}:${r.line} ${r.receiver}.${r.method}(${JSON.stringify(r.path)}) in ${r.callerName}`);
|
|
59
|
+
}
|
|
60
|
+
if (list.length > cap) lines.push(` (+${list.length - cap} more — use --all)`);
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatRoutesAndRequests(routes, requests, meta, options, advisory = null, result = null) {
|
|
52
65
|
const lines = [];
|
|
53
66
|
const showServer = !options.clientOnly;
|
|
54
67
|
const showClient = !options.serverOnly;
|
|
@@ -109,6 +122,10 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
|
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
125
|
+
if (showClient) {
|
|
126
|
+
const uncertain = formatUncertainRequests(result, options);
|
|
127
|
+
if (uncertain.length > 0) lines.push('', ...uncertain);
|
|
128
|
+
}
|
|
112
129
|
const routeAdvisory = advisoryLine(advisory);
|
|
113
130
|
if (routeAdvisory) lines.push('', routeAdvisory);
|
|
114
131
|
return lines.join('\n').trimEnd();
|
|
@@ -230,6 +247,10 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
230
247
|
data: {
|
|
231
248
|
routes: routes.map(trimRoute),
|
|
232
249
|
requests: requests.map(trimReq),
|
|
250
|
+
uncertainRequests: (result.uncertainRequests || []).map(r => ({
|
|
251
|
+
receiver: r.receiver, method: r.method, path: r.path,
|
|
252
|
+
file: r.file, line: r.line, callerName: r.callerName, reason: r.reason,
|
|
253
|
+
})),
|
|
233
254
|
// In unmatched-only mode, the matched bridges array is suppressed
|
|
234
255
|
// — consumers that want both should not pass --unmatched.
|
|
235
256
|
bridges: unmatchedOnly ? [] : bridges.map(trimBridge),
|
package/core/project.js
CHANGED
|
@@ -17,7 +17,7 @@ const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits, PA
|
|
|
17
17
|
const { validateFileIR } = require('./ir');
|
|
18
18
|
const { createFileEntryFromIR, populateFileEntryFromIR } = require('./index-ir');
|
|
19
19
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
20
|
-
const { escapeRegExp, NON_CALLABLE_TYPES, codeUnitCompare } = require('./shared');
|
|
20
|
+
const { escapeRegExp, NON_CALLABLE_TYPES, codeUnitCompare, literalNameRegex } = require('./shared');
|
|
21
21
|
const stacktrace = require('./stacktrace');
|
|
22
22
|
const indexCache = require('./cache');
|
|
23
23
|
const deadcodeModule = require('./deadcode');
|
|
@@ -1523,7 +1523,7 @@ class ProjectIndex {
|
|
|
1523
1523
|
|
|
1524
1524
|
// Detailed path: full AST-based counting (original algorithm)
|
|
1525
1525
|
// Note: no 'g' flag - we only need to test for presence per line
|
|
1526
|
-
const regex =
|
|
1526
|
+
const regex = literalNameRegex(name);
|
|
1527
1527
|
|
|
1528
1528
|
// Get files that could reference this symbol:
|
|
1529
1529
|
// 1. The file where it's defined
|
|
@@ -1941,6 +1941,30 @@ class ProjectIndex {
|
|
|
1941
1941
|
}
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
|
+
/**
|
|
1945
|
+
* True when the AST node at (line, column) is an identifier token — code
|
|
1946
|
+
* the usage scan saw (and may have deliberately dropped), as opposed to
|
|
1947
|
+
* JSX children, HTML markup, or other non-code text (fix #350).
|
|
1948
|
+
*/
|
|
1949
|
+
isIdentifierAtPosition(content, lineNum, column, filePath, name) {
|
|
1950
|
+
const language = detectLanguage(filePath, this.root);
|
|
1951
|
+
if (!language) return false;
|
|
1952
|
+
try {
|
|
1953
|
+
const tree = this._getParsedTree(filePath, content, language) ||
|
|
1954
|
+
safeParse(getParser(language), content);
|
|
1955
|
+
if (!tree) return false;
|
|
1956
|
+
const node = tree.rootNode.descendantForPosition({ row: lineNum - 1, column });
|
|
1957
|
+
if (!node || !/identifier|^name$|^word$/.test(node.type)) return false;
|
|
1958
|
+
// fix #352: the identifier node must BE the name — a hyphenated JSX
|
|
1959
|
+
// attribute (`data-cell-state`) is one property_identifier whose
|
|
1960
|
+
// text merely contains `cell`; the ACCOUNT counts that line as
|
|
1961
|
+
// other-text, so usages must list it (the #350 equality).
|
|
1962
|
+
return node.text === name;
|
|
1963
|
+
} catch (e) {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1944
1968
|
/**
|
|
1945
1969
|
* Check if a position in code is inside a comment or string using AST
|
|
1946
1970
|
* @param {string} content - File content
|
package/core/search.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
11
|
+
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames, CALLABLE_SYMBOL_KINDS, literalNameRegex } = require('./shared');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
13
|
const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('../languages');
|
|
14
14
|
const { getCachedCalls } = require('./callers');
|
|
@@ -49,11 +49,6 @@ function matchesSubstring(text, pattern, caseSensitive) {
|
|
|
49
49
|
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function literalNameRegex(name) {
|
|
53
|
-
return new RegExp(
|
|
54
|
-
`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Complete the AST-classified usage inventory with literal-name lines that
|
|
@@ -82,14 +77,21 @@ function appendTextComplements(index, {
|
|
|
82
77
|
const lineNum = idx + 1;
|
|
83
78
|
const commentOrString = index.isCommentOrStringAtPosition(
|
|
84
79
|
content, lineNum, match.index, filePath);
|
|
85
|
-
|
|
80
|
+
// A literal match that no AST identifier record claimed and that is
|
|
81
|
+
// not inside a comment or string is non-code text: JSX children,
|
|
82
|
+
// HTML markup and attributes, regex bodies. The ACCOUNT counts these
|
|
83
|
+
// lines as other-text; the escape hatch must list them too. A line
|
|
84
|
+
// the AST scan classified and then deliberately dropped (Rust enum
|
|
85
|
+
// variants against a struct pin, #234) is code, not text: skip it.
|
|
86
|
+
if (!commentOrString &&
|
|
87
|
+
index.isIdentifierAtPosition(content, lineNum, match.index, filePath, name)) continue;
|
|
86
88
|
const usage = {
|
|
87
89
|
file: filePath,
|
|
88
90
|
relativePath: fileEntry.relativePath,
|
|
89
91
|
line: lineNum,
|
|
90
92
|
content: line,
|
|
91
93
|
usageType: 'text',
|
|
92
|
-
textKind: 'comment-or-string',
|
|
94
|
+
textKind: commentOrString ? 'comment-or-string' : 'markup-or-text',
|
|
93
95
|
isDefinition: false,
|
|
94
96
|
};
|
|
95
97
|
if (context > 0) {
|
|
@@ -397,7 +399,7 @@ function usages(index, name, options = {}) {
|
|
|
397
399
|
}
|
|
398
400
|
|
|
399
401
|
// Fallback to regex-based detection
|
|
400
|
-
const regex =
|
|
402
|
+
const regex = literalNameRegex(name);
|
|
401
403
|
lines.forEach((line, idx) => {
|
|
402
404
|
const lineNum = idx + 1;
|
|
403
405
|
|
package/core/shared.js
CHANGED
|
@@ -118,6 +118,18 @@ function addTestExclusions(exclude) {
|
|
|
118
118
|
/**
|
|
119
119
|
* Escape special regex characters
|
|
120
120
|
*/
|
|
121
|
+
/**
|
|
122
|
+
* Whole-identifier match for a symbol name. `\b` is ASCII-only in JS, so
|
|
123
|
+
* `hit` matched inside `hitΔ` while every compiler and ripgrep treat Δ as an
|
|
124
|
+
* identifier character. Letters, digits, `_` and `$` on either side break a
|
|
125
|
+
* match; anything else (punctuation, whitespace, line edges) is a boundary.
|
|
126
|
+
*/
|
|
127
|
+
function literalNameRegex(name, flags = '') {
|
|
128
|
+
return new RegExp(
|
|
129
|
+
`(?<![\\p{L}\\p{N}_])${escapeRegExp(name)}(?![\\p{L}\\p{N}_])`,
|
|
130
|
+
flags.includes('u') ? flags : flags + 'u');
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
function escapeRegExp(text) {
|
|
122
134
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
123
135
|
}
|
|
@@ -463,7 +475,7 @@ function maskBlockComments(content, language) {
|
|
|
463
475
|
return out.join('');
|
|
464
476
|
}
|
|
465
477
|
|
|
466
|
-
module.exports = {
|
|
478
|
+
module.exports = { literalNameRegex,
|
|
467
479
|
pickBestDefinition,
|
|
468
480
|
addTestExclusions,
|
|
469
481
|
escapeRegExp,
|
package/languages/java.js
CHANGED
|
@@ -1502,14 +1502,34 @@ function findCallsInCode(code, parser) {
|
|
|
1502
1502
|
const valueNode = receiverNode.childForFieldName('value');
|
|
1503
1503
|
if (valueNode?.type === 'identifier') castReceiverName = valueNode.text;
|
|
1504
1504
|
}
|
|
1505
|
-
|
|
1505
|
+
let receiver = castReceiverName ||
|
|
1506
1506
|
((receiverNode?.type === 'identifier' || receiverNode?.type === 'this')
|
|
1507
1507
|
? receiverNode.text : undefined);
|
|
1508
|
+
// fix #353: `beta.Helper.widget()` — a lowercase dotted root
|
|
1509
|
+
// under a capitalized member is a PACKAGE-qualified type
|
|
1510
|
+
// (Java packages are lowercase by convention, types
|
|
1511
|
+
// capitalized); the receiver is the type and the package is
|
|
1512
|
+
// its qualifier, so same-name static methods in two packages
|
|
1513
|
+
// resolve by the qualifier that is right there in the call.
|
|
1514
|
+
let packageQualifier;
|
|
1515
|
+
if (!receiver && receiverNode?.type === 'field_access') {
|
|
1516
|
+
const rootNode = receiverNode.childForFieldName('object');
|
|
1517
|
+
const fldNode = receiverNode.childForFieldName('field');
|
|
1518
|
+
const rootText = rootNode?.text || '';
|
|
1519
|
+
const rootHead = rootText.split('.')[0];
|
|
1520
|
+
if (fldNode?.type === 'identifier' && /^[A-Z]/.test(fldNode.text) &&
|
|
1521
|
+
/^[a-z_][\w]*(\.[a-z_][\w]*)*$/.test(rootText) &&
|
|
1522
|
+
!getReceiverType(rootHead) && !isDeclaredLocal(rootHead) &&
|
|
1523
|
+
!hasEnclosingField(node, rootHead)) {
|
|
1524
|
+
receiver = fldNode.text;
|
|
1525
|
+
packageQualifier = rootText;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1508
1528
|
const receiverType = castReceiverType ||
|
|
1509
1529
|
((receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined);
|
|
1510
|
-
const receiverTypeQualifier =
|
|
1511
|
-
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1512
|
-
const receiverIsTypeQualified = !!(receiverNode?.type === 'identifier' &&
|
|
1530
|
+
const receiverTypeQualifier = packageQualifier ||
|
|
1531
|
+
(!castReceiverType && receiver ? getReceiverTypeQualifier(receiver) : undefined);
|
|
1532
|
+
const receiverIsTypeQualified = !!((receiverNode?.type === 'identifier' || packageQualifier) &&
|
|
1513
1533
|
receiver && /^[A-Z]/.test(receiver) && !receiverType &&
|
|
1514
1534
|
!isDeclaredLocal(receiver) && !hasEnclosingField(node, receiver));
|
|
1515
1535
|
// fix #202: one-hop declared-field receivers —
|
package/languages/python.js
CHANGED
|
@@ -3140,6 +3140,7 @@ function findImportsInCode(code, parser) {
|
|
|
3140
3140
|
const deferral = importDeferral(node);
|
|
3141
3141
|
let modulePath = '';
|
|
3142
3142
|
const names = [];
|
|
3143
|
+
const renames = [];
|
|
3143
3144
|
|
|
3144
3145
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3145
3146
|
const child = node.namedChild(i);
|
|
@@ -3158,6 +3159,7 @@ function findImportsInCode(code, parser) {
|
|
|
3158
3159
|
if (nameNode && aliasNode && aliasNode.text !== nameNode.text) {
|
|
3159
3160
|
if (!importAliases) importAliases = [];
|
|
3160
3161
|
importAliases.push({ original: nameNode.text, local: aliasNode.text });
|
|
3162
|
+
renames.push({ original: nameNode.text, local: aliasNode.text });
|
|
3161
3163
|
}
|
|
3162
3164
|
} else if (child.type === 'wildcard_import') {
|
|
3163
3165
|
names.push('*');
|
|
@@ -3171,6 +3173,7 @@ function findImportsInCode(code, parser) {
|
|
|
3171
3173
|
names,
|
|
3172
3174
|
type: isRelative ? 'relative' : 'from',
|
|
3173
3175
|
line,
|
|
3176
|
+
...(renames.length > 0 && { renames }),
|
|
3174
3177
|
...(deferral && { deferred: true, deferredReason: deferral })
|
|
3175
3178
|
});
|
|
3176
3179
|
}
|
package/languages/rust.js
CHANGED
|
@@ -3152,6 +3152,14 @@ function findImportsInCode(code, parser) {
|
|
|
3152
3152
|
if (pathNode && aliasNode) {
|
|
3153
3153
|
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3154
3154
|
'use', false, line);
|
|
3155
|
+
// fix #353: `use alpha::widget as renamed; renamed()` — the
|
|
3156
|
+
// alias pairing feeds findCallers' import-rename surface
|
|
3157
|
+
// (calledAs), exactly like Python/JS `import x as y`.
|
|
3158
|
+
const original = String(pathNode.text).split('::').pop();
|
|
3159
|
+
if (original && original !== aliasNode.text && original !== 'self') {
|
|
3160
|
+
if (!imports.aliases) imports.aliases = [];
|
|
3161
|
+
imports.aliases.push({ original, local: aliasNode.text });
|
|
3162
|
+
}
|
|
3155
3163
|
}
|
|
3156
3164
|
return;
|
|
3157
3165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.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",
|