ucn 5.4.1 → 5.5.0
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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
package/core/ast-analysis.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* with stable tree-sitter facts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const { getParser, safeParse } = require('../languages');
|
|
11
|
+
const { getParser, safeParse, langTraits } = require('../languages');
|
|
12
12
|
|
|
13
13
|
const CALLABLE_NODES = new Set([
|
|
14
14
|
// JavaScript / TypeScript
|
|
@@ -167,9 +167,13 @@ function isDefaultBranch(node) {
|
|
|
167
167
|
function findCallableForRange(root, startLine, endLine) {
|
|
168
168
|
const candidates = [];
|
|
169
169
|
walkNamed(root, node => {
|
|
170
|
-
if (!CALLABLE_NODES.has(node.type)) return true;
|
|
171
170
|
const start = node.startPosition.row + 1;
|
|
172
171
|
const end = node.endPosition.row + 1;
|
|
172
|
+
// A subtree lies within its root's rows: one disjoint from the
|
|
173
|
+
// range holds no candidate (fix #365 - the walk used to visit every
|
|
174
|
+
// node of the file for each measured callable).
|
|
175
|
+
if (end < startLine || start > endLine) return false;
|
|
176
|
+
if (!CALLABLE_NODES.has(node.type)) return true;
|
|
173
177
|
if (start < startLine || end > endLine) return true;
|
|
174
178
|
candidates.push({
|
|
175
179
|
node,
|
|
@@ -193,10 +197,8 @@ function findCallableForRange(root, startLine, endLine) {
|
|
|
193
197
|
* indexed callable. Formatting, comments, strings, optional chaining, and
|
|
194
198
|
* nullish coalescing cannot affect these values.
|
|
195
199
|
*/
|
|
196
|
-
function computeAstComplexity(content, language, {
|
|
197
|
-
startLine = 1,
|
|
198
|
-
endLine = startLine,
|
|
199
|
-
} = {}) {
|
|
200
|
+
function computeAstComplexity(content, language, options = {}) {
|
|
201
|
+
const { startLine = 1, endLine = startLine } = options;
|
|
200
202
|
const lineCount = Math.max(0, endLine - startLine + 1);
|
|
201
203
|
try {
|
|
202
204
|
const parser = getParser(language);
|
|
@@ -208,7 +210,7 @@ function computeAstComplexity(content, language, {
|
|
|
208
210
|
measuredBy: 'unavailable-no-parser',
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
|
-
const tree = safeParse(parser, content);
|
|
213
|
+
const tree = options.tree || safeParse(parser, content);
|
|
212
214
|
const callable = findCallableForRange(tree.rootNode, startLine, endLine);
|
|
213
215
|
if (!callable) {
|
|
214
216
|
return {
|
|
@@ -276,99 +278,22 @@ function computedReceiver(callee) {
|
|
|
276
278
|
return node?.type === 'identifier' ? node.text : null;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
|
-
const STRING_LITERAL_NODES = new Set([
|
|
280
|
-
'string', 'string_literal', 'raw_string_literal',
|
|
281
|
-
'interpreted_string_literal', 'verbatim_string_literal',
|
|
282
|
-
]);
|
|
283
|
-
|
|
284
|
-
function literalStringValue(node) {
|
|
285
|
-
if (!node || !STRING_LITERAL_NODES.has(node.type)) return null;
|
|
286
|
-
const content = (node.namedChildren || []).find(child =>
|
|
287
|
-
child.type === 'string_content' || child.type === 'interpreted_string_literal_content');
|
|
288
|
-
if (content) return content.text;
|
|
289
|
-
const raw = String(node.text || '');
|
|
290
|
-
const first = raw.search(/["']/);
|
|
291
|
-
if (first < 0) return null;
|
|
292
|
-
const quote = raw[first];
|
|
293
|
-
const triple = raw.slice(first, first + 3) === quote.repeat(3);
|
|
294
|
-
const width = triple ? 3 : 1;
|
|
295
|
-
if (!raw.endsWith(quote.repeat(width))) return null;
|
|
296
|
-
const value = raw.slice(first + width, -width);
|
|
297
|
-
// Escaped/interpolated member names are not a stable static spelling.
|
|
298
|
-
if (value.includes('\\') || value.includes('{') || value.includes('$')) return null;
|
|
299
|
-
return value;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function callShape(node) {
|
|
303
|
-
if (!['call', 'call_expression', 'invocation_expression',
|
|
304
|
-
'method_invocation'].includes(node.type)) return null;
|
|
305
|
-
const callee = node.childForFieldName('function') ||
|
|
306
|
-
node.childForFieldName('name') || node.namedChild(0);
|
|
307
|
-
const args = node.childForFieldName('arguments') ||
|
|
308
|
-
(node.namedChildren || []).find(child =>
|
|
309
|
-
['argument_list', 'arguments', 'bracketed_argument_list'].includes(child.type));
|
|
310
|
-
return callee && args ? { callee, args: args.namedChildren || [] } : null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
281
|
/**
|
|
314
|
-
* Extract recognized reflection operations and classify
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
282
|
+
* Extract recognized reflection operations and classify their member target:
|
|
283
|
+
* a stable literal (`name`), literal fragments around a runtime part
|
|
284
|
+
* (`patterns`, fix #363), or dynamic. Literal targets and specific patterns
|
|
285
|
+
* are positive liveness evidence; dynamic targets cannot identify a member
|
|
286
|
+
* but must still be disclosed by deletion-oriented commands.
|
|
318
287
|
*/
|
|
319
|
-
function reflectionSites(content, language) {
|
|
288
|
+
function reflectionSites(content, language, tree = null) {
|
|
320
289
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
go: ['MethodByName', 'FieldByName'],
|
|
327
|
-
java: ['getMethod', 'getDeclaredMethod', 'getField', 'getDeclaredField'],
|
|
328
|
-
csharp: ['GetMethod', 'GetProperty', 'GetField'],
|
|
329
|
-
}[language];
|
|
330
|
-
if (!syntaxHints || !syntaxHints.some(hint => content.includes(hint))) {
|
|
331
|
-
return [];
|
|
290
|
+
if (!langTraits(language)?.reflectionApi) return [];
|
|
291
|
+
if (!tree) {
|
|
292
|
+
const parser = getParser(language);
|
|
293
|
+
if (!parser) return [];
|
|
294
|
+
tree = safeParse(parser, content);
|
|
332
295
|
}
|
|
333
|
-
|
|
334
|
-
if (!parser) return [];
|
|
335
|
-
const tree = safeParse(parser, content);
|
|
336
|
-
const sites = [];
|
|
337
|
-
walkNamed(tree.rootNode, node => {
|
|
338
|
-
const call = callShape(node);
|
|
339
|
-
if (!call) return true;
|
|
340
|
-
const callee = String(call.callee.text || '');
|
|
341
|
-
let argIndex = null;
|
|
342
|
-
let kind = null;
|
|
343
|
-
if (language === 'python' &&
|
|
344
|
-
['getattr', 'setattr', 'hasattr', 'delattr'].includes(callee)) {
|
|
345
|
-
argIndex = 1;
|
|
346
|
-
kind = callee;
|
|
347
|
-
} else if (['javascript', 'typescript', 'tsx'].includes(language) &&
|
|
348
|
-
/^(?:globalThis\.)?Reflect\.(?:get|set|has|deleteProperty)$/.test(callee)) {
|
|
349
|
-
argIndex = 1;
|
|
350
|
-
kind = callee.split('.').pop();
|
|
351
|
-
} else if (language === 'go' &&
|
|
352
|
-
/\.(?:MethodByName|FieldByName)$/.test(callee)) {
|
|
353
|
-
argIndex = 0;
|
|
354
|
-
kind = callee.split('.').pop();
|
|
355
|
-
} else if (['java', 'csharp'].includes(language) &&
|
|
356
|
-
/(?:^|\.)(?:getMethod|getDeclaredMethod|getField|getDeclaredField|GetMethod|GetProperty|GetField)$/.test(callee)) {
|
|
357
|
-
argIndex = 0;
|
|
358
|
-
kind = callee.split('.').pop();
|
|
359
|
-
}
|
|
360
|
-
if (argIndex == null || !call.args[argIndex]) return true;
|
|
361
|
-
const name = literalStringValue(call.args[argIndex]);
|
|
362
|
-
sites.push({
|
|
363
|
-
...(name && /^[A-Za-z_$][\w$]*$/.test(name) && { name }),
|
|
364
|
-
kind,
|
|
365
|
-
line: node.startPosition.row + 1,
|
|
366
|
-
expression: node.text,
|
|
367
|
-
dynamic: !name || !/^[A-Za-z_$][\w$]*$/.test(name),
|
|
368
|
-
});
|
|
369
|
-
return true;
|
|
370
|
-
});
|
|
371
|
-
return sites;
|
|
296
|
+
return require('./reflection').reflectionSitesInTree(tree, language, content);
|
|
372
297
|
} catch (_) {
|
|
373
298
|
return [];
|
|
374
299
|
}
|
|
@@ -382,11 +307,19 @@ function literalReflectionSites(content, language) {
|
|
|
382
307
|
* Find direct computed dispatch calls such as handlers[name](). Literal keys
|
|
383
308
|
* are excluded because they retain a statically visible member name.
|
|
384
309
|
*/
|
|
385
|
-
|
|
310
|
+
const CALL_NODE_TYPES = new Set(['call_expression', 'call', 'invocation_expression']);
|
|
311
|
+
const BINDING_NODE_TYPES = new Set(['variable_declarator', 'assignment_expression', 'assignment']);
|
|
312
|
+
const COMPUTED_INDEX_NODE_TYPES = [
|
|
313
|
+
'subscript_expression', 'index_expression', 'subscript', 'element_access_expression',
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
function computedDispatchSites(content, language, tree = null) {
|
|
386
317
|
try {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
318
|
+
if (!tree) {
|
|
319
|
+
const parser = getParser(language);
|
|
320
|
+
if (!parser) return [];
|
|
321
|
+
tree = safeParse(parser, content);
|
|
322
|
+
}
|
|
390
323
|
const sites = [];
|
|
391
324
|
const seen = new Set();
|
|
392
325
|
const selectedByLocal = new Map();
|
|
@@ -410,35 +343,45 @@ function computedDispatchSites(content, language) {
|
|
|
410
343
|
expression: expression || node.text,
|
|
411
344
|
});
|
|
412
345
|
};
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
346
|
+
// Every site's callee (or bound value) is an index expression, so
|
|
347
|
+
// the native query starts from those (fix #365: the named-node walk
|
|
348
|
+
// touched every node of every file). A callee is its call's first
|
|
349
|
+
// child, so index-expression order is the calls' document order.
|
|
350
|
+
const sameRange = (a, b) => !!a && !!b &&
|
|
351
|
+
a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type;
|
|
352
|
+
for (const indexExpr of tree.rootNode.descendantsOfType(COMPUTED_INDEX_NODE_TYPES)) {
|
|
353
|
+
if (!indexExpr.isNamed) continue;
|
|
354
|
+
const node = indexExpr.parent;
|
|
355
|
+
if (!node || !node.isNamed) continue;
|
|
356
|
+
if (CALL_NODE_TYPES.has(node.type)) {
|
|
357
|
+
const callee = node.childForFieldName('function');
|
|
358
|
+
if (sameRange(callee, indexExpr)) record(node, callee);
|
|
359
|
+
continue;
|
|
423
360
|
}
|
|
424
|
-
|
|
425
361
|
// Two-step dispatch: `const h = handlers[key]; h()`. Record the
|
|
426
362
|
// dynamic access only when its bound local is actually invoked in
|
|
427
363
|
// the same callable scope. Ordinary indexing (`xs[i]`) is a value
|
|
428
364
|
// read and says nothing about runtime-selected call targets.
|
|
429
|
-
if (node.type
|
|
430
|
-
node.type === 'assignment_expression' ||
|
|
431
|
-
node.type === 'assignment') {
|
|
365
|
+
if (BINDING_NODE_TYPES.has(node.type)) {
|
|
432
366
|
const left = node.childForFieldName('name') ||
|
|
433
367
|
node.childForFieldName('left');
|
|
434
368
|
const right = node.childForFieldName('value') ||
|
|
435
369
|
node.childForFieldName('right');
|
|
436
|
-
if (left?.type === 'identifier' &&
|
|
370
|
+
if (sameRange(right, indexExpr) && left?.type === 'identifier' &&
|
|
371
|
+
indexNodeForComputedCallee(right)) {
|
|
437
372
|
selectedByLocal.set(`${scopeKey(node)}\0${left.text}`, { node, right });
|
|
438
373
|
}
|
|
439
374
|
}
|
|
440
|
-
|
|
441
|
-
|
|
375
|
+
}
|
|
376
|
+
if (selectedByLocal.size > 0) {
|
|
377
|
+
for (const node of tree.rootNode.descendantsOfType([...CALL_NODE_TYPES])) {
|
|
378
|
+
if (!node.isNamed) continue;
|
|
379
|
+
const callee = node.childForFieldName('function');
|
|
380
|
+
if (callee?.type === 'identifier') {
|
|
381
|
+
calledLocals.add(`${scopeKey(node)}\0${callee.text}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
442
385
|
for (const [key, selection] of selectedByLocal) {
|
|
443
386
|
if (calledLocals.has(key)) record(selection.node, selection.right);
|
|
444
387
|
}
|
|
@@ -469,11 +412,27 @@ function projectComputedDispatch(index) {
|
|
|
469
412
|
return byFile;
|
|
470
413
|
}
|
|
471
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Project reflection inventory (fix #363): per-file sites are extracted at
|
|
417
|
+
* index time from the file's own parse (fileEntry.reflectionSites), so this
|
|
418
|
+
* is a view, never a re-parse.
|
|
419
|
+
*/
|
|
420
|
+
function projectReflectionSites(index) {
|
|
421
|
+
const byFile = new Map();
|
|
422
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
423
|
+
if (Array.isArray(fileEntry.reflectionSites) && fileEntry.reflectionSites.length > 0) {
|
|
424
|
+
byFile.set(filePath, fileEntry.reflectionSites);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return byFile;
|
|
428
|
+
}
|
|
429
|
+
|
|
472
430
|
module.exports = {
|
|
473
431
|
declarationSnapshots,
|
|
474
432
|
computeAstComplexity,
|
|
475
433
|
computedDispatchSites,
|
|
476
434
|
projectComputedDispatch,
|
|
435
|
+
projectReflectionSites,
|
|
477
436
|
reflectionSites,
|
|
478
437
|
literalReflectionSites,
|
|
479
438
|
};
|