ucn 5.3.1 → 5.3.2
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 +11 -3
- package/core/account.js +4 -4
- package/core/bridge.js +89 -2
- package/core/cache.js +3 -1
- package/core/callers.js +3 -1
- package/core/execute.js +1 -0
- package/core/output/endpoints.js +23 -2
- package/core/project.js +21 -2
- package/core/search.js +11 -9
- package/core/shared.js +13 -1
- package/languages/python.js +3 -0
- package/package.json +1 -1
|
@@ -157,9 +157,17 @@ 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: `hit` never matches inside `hitΔ`.
|
|
165
|
+
|
|
166
|
+
`endpoints` recognizes client receivers by evidence (a receiver typed to an
|
|
167
|
+
HTTP client class, or bound to a pytest fixture that constructs one), not only
|
|
168
|
+
by name. Request-shaped calls with a path literal on an unrecognized receiver
|
|
169
|
+
are listed under `Possible client requests` (JSON `uncertainRequests`),
|
|
170
|
+
counted in `meta`, never in the inventory. `search` treats its term literally by default; pass `--regex` only
|
|
163
171
|
when regular-expression semantics are intended. Ordinary regex patterns run
|
|
164
172
|
through an RE2-compatible linear-time engine; unsafe nested repetition is
|
|
165
173
|
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 = 213;
|
|
701
703
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
702
704
|
|
|
703
705
|
/**
|
package/core/callers.js
CHANGED
|
@@ -2802,7 +2802,9 @@ function findCallers(index, name, options = {}) {
|
|
|
2802
2802
|
// source name from DIFFERENT modules — the record's local
|
|
2803
2803
|
// alias (call.name) picks its own binding; source-name
|
|
2804
2804
|
// matching alone over-follows into the other module.
|
|
2805
|
-
|
|
2805
|
+
// Python records carry the alias as `calledAs` rather
|
|
2806
|
+
// than resolvedName; both name the paired binding.
|
|
2807
|
+
if ((call.resolvedName || calledAs) && nameBindings.some(b => b.alias)) {
|
|
2806
2808
|
const paired = nameBindings.filter(b => b.alias === call.name);
|
|
2807
2809
|
if (paired.length > 0) nameBindings = paired;
|
|
2808
2810
|
}
|
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,25 @@ 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) {
|
|
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
|
+
return !!node && /identifier|^name$|^word$/.test(node.type);
|
|
1958
|
+
} catch (e) {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1944
1963
|
/**
|
|
1945
1964
|
* Check if a position in code is inside a comment or string using AST
|
|
1946
1965
|
* @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)) 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
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",
|