ucn 5.5.3 → 5.6.1
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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/languages/rust.js
CHANGED
|
@@ -287,7 +287,10 @@ const RUST_TRANSPARENT_FN_ATTRIBUTES = new Set([
|
|
|
287
287
|
]);
|
|
288
288
|
|
|
289
289
|
function rustAttributePath(node) {
|
|
290
|
-
|
|
290
|
+
// Raw identifiers change the spelling, not the attribute's identity.
|
|
291
|
+
return node && ['identifier', 'scoped_identifier'].includes(node.type)
|
|
292
|
+
? node.text.replace(/\s+/g, '').split('::').map(part => part.startsWith('r#') ? part.slice(2) : part).join('::')
|
|
293
|
+
: null;
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
/** Outer attribute paths of an item; `cfg_attr(pred, a, b)` yields a, b. */
|
|
@@ -314,7 +317,7 @@ function rustOuterAttributePaths(itemNode) {
|
|
|
314
317
|
continue;
|
|
315
318
|
}
|
|
316
319
|
if (group === 0 || !expectPath) continue;
|
|
317
|
-
if (token.type === 'identifier' || token.type === '::') current += token.text;
|
|
320
|
+
if (token.type === 'identifier' || token.type === '::') current += rustAttributePath(token) || token.text;
|
|
318
321
|
else { if (current) paths.push(current); current = ''; expectPath = false; }
|
|
319
322
|
}
|
|
320
323
|
if (current) paths.push(current);
|
|
@@ -602,93 +605,42 @@ function extractVisibility(text) {
|
|
|
602
605
|
/**
|
|
603
606
|
* Extract attributes from a function node (e.g., #[test], #[tokio::main])
|
|
604
607
|
* @param {Node} node - AST node
|
|
605
|
-
* @param {string} code - Source code
|
|
606
608
|
* @returns {string[]} Array of attribute names
|
|
607
609
|
*/
|
|
608
|
-
function
|
|
610
|
+
function outerAttributeNodes(node) {
|
|
609
611
|
const attributes = [];
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const line = lines[i]?.trim();
|
|
616
|
-
if (!line) break;
|
|
617
|
-
if (line.startsWith('#[')) {
|
|
618
|
-
// Extract attribute name (e.g., #[test] -> test, #[tokio::main] -> tokio::main)
|
|
619
|
-
const match = line.match(/#\[([^\]]+)\]/);
|
|
620
|
-
if (match) {
|
|
621
|
-
const attrContent = match[1];
|
|
622
|
-
// Get just the attribute name (without arguments)
|
|
623
|
-
const attrName = attrContent.split('(')[0].trim();
|
|
624
|
-
// Skip compiler hint attributes that aren't semantically meaningful for display
|
|
625
|
-
const SKIP_ATTRS = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
|
|
626
|
-
if (!SKIP_ATTRS.has(attrName)) {
|
|
627
|
-
attributes.push(attrName);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} else if (!line.startsWith('//')) {
|
|
631
|
-
// Stop at non-comment, non-attribute lines
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
612
|
+
for (let sibling = node.previousNamedSibling; sibling; sibling = sibling.previousNamedSibling) {
|
|
613
|
+
if (sibling.type === 'line_comment' || sibling.type === 'block_comment') continue;
|
|
614
|
+
if (sibling.type !== 'attribute_item') break;
|
|
615
|
+
const attribute = sibling.namedChildren.find(child => child.type === 'attribute');
|
|
616
|
+
if (attribute) attributes.unshift(attribute);
|
|
634
617
|
}
|
|
635
|
-
|
|
636
618
|
return attributes;
|
|
637
619
|
}
|
|
638
620
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (!
|
|
657
|
-
|
|
658
|
-
// Match #[name(...args...)] or #[name]
|
|
659
|
-
// Need to handle nested parens; use a simple bracket-matching approach.
|
|
660
|
-
// (The line is trimmed: only a line ending in ']' can match; a
|
|
661
|
-
// macro expansion's generated line can be very long.)
|
|
662
|
-
const m = line.charCodeAt(line.length - 1) === 93 ? line.match(/^#\[(.+)\]\s*$/) : null;
|
|
663
|
-
if (m) {
|
|
664
|
-
const attrContent = m[1];
|
|
665
|
-
const parenIdx = attrContent.indexOf('(');
|
|
666
|
-
if (parenIdx === -1) {
|
|
667
|
-
result.unshift({ name: attrContent.trim(), args: null });
|
|
668
|
-
} else {
|
|
669
|
-
const name = attrContent.slice(0, parenIdx).trim();
|
|
670
|
-
// Extract content within outer parens (find matching close)
|
|
671
|
-
let depth = 0;
|
|
672
|
-
let endIdx = -1;
|
|
673
|
-
for (let k = parenIdx; k < attrContent.length; k++) {
|
|
674
|
-
const ch = attrContent[k];
|
|
675
|
-
if (ch === '(') depth++;
|
|
676
|
-
else if (ch === ')') {
|
|
677
|
-
depth--;
|
|
678
|
-
if (depth === 0) { endIdx = k; break; }
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const args = endIdx > parenIdx
|
|
682
|
-
? attrContent.slice(parenIdx + 1, endIdx).trim()
|
|
683
|
-
: attrContent.slice(parenIdx + 1).trim();
|
|
684
|
-
result.unshift({ name, args });
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
} else if (!line.startsWith('//')) {
|
|
688
|
-
break;
|
|
621
|
+
function extractAttributes(node) {
|
|
622
|
+
const skip = new Set(['allow', 'deny', 'warn', 'forbid', 'cfg_attr', 'doc']);
|
|
623
|
+
// Preserve the established nearest-first modifier ordering. Ownership
|
|
624
|
+
// comes from adjacent AST siblings, never preceding source lines.
|
|
625
|
+
return extractAttributesWithArgs(node).reverse().map(attribute => attribute.name)
|
|
626
|
+
.filter(name => !skip.has(name));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Written outer attributes attached to this item, in source order. */
|
|
630
|
+
function extractAttributesWithArgs(node) {
|
|
631
|
+
const attributes = [];
|
|
632
|
+
for (const attribute of outerAttributeNodes(node)) {
|
|
633
|
+
let name = rustAttributePath(attribute.namedChild(0));
|
|
634
|
+
if (!name) continue;
|
|
635
|
+
const argumentsNode = attribute.childForFieldName('arguments');
|
|
636
|
+
// The existing attribute record keeps name-value attributes whole
|
|
637
|
+
// (notably path = "module.rs", which module resolution consumes).
|
|
638
|
+
if (!argumentsNode && attribute.children.some(child => child.type === '=')) {
|
|
639
|
+
name += attribute.text.slice(attribute.namedChild(0).endIndex - attribute.startIndex).trimEnd();
|
|
689
640
|
}
|
|
641
|
+
attributes.push({ name, args: argumentsNode ? argumentsNode.text.slice(1, -1).trim() : null });
|
|
690
642
|
}
|
|
691
|
-
return
|
|
643
|
+
return attributes;
|
|
692
644
|
}
|
|
693
645
|
|
|
694
646
|
// --- Module-scope constants for state object detection ---
|
|
@@ -701,30 +653,14 @@ function extractAttributesWithArgs(node, codeOrLines) {
|
|
|
701
653
|
* functions inside a `mod tests` block as test entry points even when
|
|
702
654
|
* they don't carry a direct `#[test]` attribute (BUG-CY).
|
|
703
655
|
*/
|
|
704
|
-
function _isInsideCfgTestModule(node
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (!line) break;
|
|
713
|
-
if (line.startsWith('#[')) {
|
|
714
|
-
// Match #[cfg(...)] forms that include a `test` predicate.
|
|
715
|
-
// Conservatively look for the literal token `test` inside the cfg(...) args.
|
|
716
|
-
const m = line.match(/#\[\s*cfg\s*\(([^\]]*)\)\s*\]/);
|
|
717
|
-
if (m) {
|
|
718
|
-
const args = m[1];
|
|
719
|
-
// Word-boundary match for `test` to avoid matching e.g. `testing_module`.
|
|
720
|
-
if (/\btest\b/.test(args)) return true;
|
|
721
|
-
}
|
|
722
|
-
} else if (!line.startsWith('//')) {
|
|
723
|
-
break;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
parent = parent.parent;
|
|
656
|
+
function _isInsideCfgTestModule(node) {
|
|
657
|
+
const namesTest = part => part.type === 'identifier' && rustAttributePath(part) === 'test' ||
|
|
658
|
+
part.namedChildren.some(namesTest);
|
|
659
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
660
|
+
if (parent.type !== 'mod_item') continue;
|
|
661
|
+
if (outerAttributeNodes(parent).some(attribute =>
|
|
662
|
+
rustAttributePath(attribute.namedChild(0)) === 'cfg' &&
|
|
663
|
+
attribute.childForFieldName('arguments')?.namedChildren.some(namesTest))) return true;
|
|
728
664
|
}
|
|
729
665
|
return false;
|
|
730
666
|
}
|
|
@@ -1978,6 +1914,9 @@ function extractTraitMembers(traitNode, codeOrLines) {
|
|
|
1978
1914
|
sourceSpan,
|
|
1979
1915
|
memberType: 'method',
|
|
1980
1916
|
isMethod: true,
|
|
1917
|
+
// A trait method without a body is one every impl must
|
|
1918
|
+
// supply; one with a body is a provided default.
|
|
1919
|
+
...(child.type === 'function_signature_item' && { requiredMember: true }),
|
|
1981
1920
|
...(qualifiers.async && { isAsync: true }),
|
|
1982
1921
|
modifiers: [...(traitVisibility ? [traitVisibility] : []), ...(qualifiers.async ? ['async'] : [])],
|
|
1983
1922
|
...(futureReturn && { futureReturn }),
|
|
@@ -6389,6 +6328,171 @@ function rustMacroNamespaceName(node, parent, attributeNode) {
|
|
|
6389
6328
|
return !next || next.type !== '::';
|
|
6390
6329
|
}
|
|
6391
6330
|
|
|
6331
|
+
|
|
6332
|
+
// Attribute macros that declare one test per case; the names the harness
|
|
6333
|
+
// lists come from the expansion, not from the function.
|
|
6334
|
+
const RUST_CASE_ATTRIBUTES = new Set(['rstest', 'test_case', 'test_matrix', 'parameterized', 'test_casing',
|
|
6335
|
+
'apply', 'quickcheck', 'proptest', 'test_resources', 'datatest']);
|
|
6336
|
+
|
|
6337
|
+
// Attributes of the language that leave a test as it is. Any other is a
|
|
6338
|
+
// macro that rewrites the function: what then runs is not read.
|
|
6339
|
+
const RUST_INERT_ATTRIBUTES = new Set(['test', 'ignore', 'should_panic', 'cfg', 'cfg_attr', 'allow', 'warn', 'deny',
|
|
6340
|
+
'forbid', 'expect', 'doc', 'inline', 'cold', 'must_use', 'deprecated', 'track_caller', 'no_mangle', 'case',
|
|
6341
|
+
'values', 'timeout']);
|
|
6342
|
+
|
|
6343
|
+
/** The comma-separated syntax groups of an attribute's token tree. */
|
|
6344
|
+
function rustAttributeGroups(argumentsNode) {
|
|
6345
|
+
const groups = [[]];
|
|
6346
|
+
for (const child of argumentsNode?.children || []) {
|
|
6347
|
+
if (['(', ')', '[', ']', '{', '}', 'line_comment', 'block_comment'].includes(child.type)) continue;
|
|
6348
|
+
if (child.type === ',') groups.push([]);
|
|
6349
|
+
else groups[groups.length - 1].push(child);
|
|
6350
|
+
}
|
|
6351
|
+
return groups.filter(group => group.length > 0);
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
/** An attribute written inside cfg_attr is token-tree syntax, not an attribute node. */
|
|
6355
|
+
function rustNestedAttribute(nodes) {
|
|
6356
|
+
let name = '';
|
|
6357
|
+
let args = null;
|
|
6358
|
+
for (const node of nodes) {
|
|
6359
|
+
if (node.type === 'token_tree') { args = node; break; }
|
|
6360
|
+
if (['identifier', 'scoped_identifier', '::'].includes(node.type)) name += rustAttributePath(node) || node.text;
|
|
6361
|
+
else break;
|
|
6362
|
+
}
|
|
6363
|
+
return { name, args };
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6366
|
+
// Only configuration facts intrinsic to a test build are decided here.
|
|
6367
|
+
// Feature, target and user cfg values remain unknown, including their negations.
|
|
6368
|
+
function rustTestCfgValue(nodes) {
|
|
6369
|
+
if (nodes.length === 1 && nodes[0].type === 'identifier' && rustAttributePath(nodes[0]) === 'test') return true;
|
|
6370
|
+
const { name, args } = rustNestedAttribute(nodes);
|
|
6371
|
+
if (!args) return null;
|
|
6372
|
+
const values = rustAttributeGroups(args).map(rustTestCfgValue);
|
|
6373
|
+
if (name === 'all') return values.includes(false) ? false : values.includes(null) ? null : true;
|
|
6374
|
+
if (name === 'any') return values.includes(true) ? true : values.includes(null) ? null : false;
|
|
6375
|
+
if (name === 'not' && values.length === 1) return values[0] === null ? null : !values[0];
|
|
6376
|
+
return null;
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6379
|
+
/** Possible attributes cfg_attr adds to this exact declaration, never a neighbor. */
|
|
6380
|
+
function rustConditionalTestAttributes(node) {
|
|
6381
|
+
const attributes = [];
|
|
6382
|
+
const expand = (name, args, active) => {
|
|
6383
|
+
if (active === false) return;
|
|
6384
|
+
if (name !== 'cfg_attr') {
|
|
6385
|
+
if (name) attributes.push({ name, certain: active === true });
|
|
6386
|
+
return;
|
|
6387
|
+
}
|
|
6388
|
+
const [condition, ...nested] = rustAttributeGroups(args);
|
|
6389
|
+
const value = condition ? rustTestCfgValue(condition) : null;
|
|
6390
|
+
const enabled = value === false ? false : active === null || value === null ? null : true;
|
|
6391
|
+
for (const group of nested) {
|
|
6392
|
+
const attribute = rustNestedAttribute(group);
|
|
6393
|
+
expand(attribute.name, attribute.args, enabled);
|
|
6394
|
+
}
|
|
6395
|
+
};
|
|
6396
|
+
for (const attribute of outerAttributeNodes(node)) {
|
|
6397
|
+
if (rustAttributePath(attribute.namedChild(0)) === 'cfg_attr') {
|
|
6398
|
+
expand('cfg_attr', attribute.childForFieldName('arguments'), true);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
return attributes;
|
|
6402
|
+
}
|
|
6403
|
+
|
|
6404
|
+
/**
|
|
6405
|
+
* The tests a Rust file declares, as the test harness lists them: a
|
|
6406
|
+
* function carrying #[test] or an attribute whose path ends in ::test
|
|
6407
|
+
* (tokio::test and the like), named by the modules it sits in. A function
|
|
6408
|
+
* carrying a case-generating attribute is a test whose listed names are
|
|
6409
|
+
* not its own. A test under a cfg other than cfg(test) is compiled only
|
|
6410
|
+
* sometimes.
|
|
6411
|
+
*/
|
|
6412
|
+
function testUnits(fileEntry, helpers) {
|
|
6413
|
+
const units = [];
|
|
6414
|
+
const holding = new Set();
|
|
6415
|
+
const attributesOf = symbol => symbol.attributesWithArgs || [];
|
|
6416
|
+
let conditionalAttributes;
|
|
6417
|
+
const conditionalAttributesOf = symbol => {
|
|
6418
|
+
if (!attributesOf(symbol).some(attribute => attribute.name === 'cfg_attr')) return [];
|
|
6419
|
+
if (!conditionalAttributes) {
|
|
6420
|
+
conditionalAttributes = new Map();
|
|
6421
|
+
for (const node of helpers.tree()?.rootNode.descendantsOfType('function_item') || []) {
|
|
6422
|
+
conditionalAttributes.set(node.startIndex, rustConditionalTestAttributes(node));
|
|
6423
|
+
}
|
|
6424
|
+
}
|
|
6425
|
+
return conditionalAttributes.get(symbol.sourceSpan?.start) || [];
|
|
6426
|
+
};
|
|
6427
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
6428
|
+
if (symbol.type !== 'function' || symbol.macroExpansion) continue;
|
|
6429
|
+
const attributes = attributesOf(symbol);
|
|
6430
|
+
// The attribute's path: `ignore = "why"` is the attribute `ignore`.
|
|
6431
|
+
const directNames = attributes.map(attribute => String(attribute.name).split(/[\s=(]/)[0]);
|
|
6432
|
+
const conditional = conditionalAttributesOf(symbol);
|
|
6433
|
+
const names = [...directNames, ...conditional.map(attribute => attribute.name)];
|
|
6434
|
+
const ignored = directNames.includes('ignore') || conditional.some(attribute => attribute.name === 'ignore' && attribute.certain);
|
|
6435
|
+
const plain = names.some(name => /(^|::)test$/.test(name));
|
|
6436
|
+
const cases = names.some(name => RUST_CASE_ATTRIBUTES.has(name.split('::').pop()));
|
|
6437
|
+
if (!plain && !cases) continue;
|
|
6438
|
+
const chain = helpers.enclosing(symbol);
|
|
6439
|
+
if (chain.some(outer => outer.type !== 'module')) continue;
|
|
6440
|
+
const gates = chain.filter(outer => (outer.modifiers || []).includes('cfg')).length;
|
|
6441
|
+
const ownGate = attributes.some(attribute => /^cfg(_attr)?$/.test(String(attribute.name)) &&
|
|
6442
|
+
String(attribute.args || '').replace(/[\s()]/g, '') !== 'test');
|
|
6443
|
+
const outerGate = chain.some(outer => attributesOf(outer).some(attribute =>
|
|
6444
|
+
/^cfg(_attr)?$/.test(String(attribute.name)) && String(attribute.args || '').replace(/[\s()]/g, '') !== 'test'));
|
|
6445
|
+
const inTestModule = (symbol.modifiers || []).includes('cfg_test_module');
|
|
6446
|
+
// An attribute that is none of the language's is a macro that rewrites the function.
|
|
6447
|
+
const rewritten = names.some(name => !/(^|::)test$/.test(name) &&
|
|
6448
|
+
!RUST_INERT_ATTRIBUTES.has(name) && !RUST_CASE_ATTRIBUTES.has(name.split('::').pop()) &&
|
|
6449
|
+
!/^(clippy|rustfmt|rustc|diagnostic)(::|_)/.test(name));
|
|
6450
|
+
for (const outer of chain) holding.add(outer);
|
|
6451
|
+
units.push({
|
|
6452
|
+
kind: 'test', name: symbol.name, suite: chain.map(outer => outer.name),
|
|
6453
|
+
line: symbol.nameLine || symbol.startLine, startLine: symbol.startLine,
|
|
6454
|
+
endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
|
|
6455
|
+
// Declared, and left out of a run unless asked for.
|
|
6456
|
+
...(ignored && { ignored: true, skipped: true }),
|
|
6457
|
+
...((ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && { conditional: true, notInEveryBuild: true }),
|
|
6458
|
+
...(!(ownGate || outerGate || gates > (inTestModule ? 1 : 0)) && rewritten && { conditional: true }),
|
|
6459
|
+
// What a macro makes of the function is not read: none, one
|
|
6460
|
+
// or several tests.
|
|
6461
|
+
...(rewritten && { several: true }),
|
|
6462
|
+
...(cases && { dynamic: 'generated-cases' }),
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
6465
|
+
for (const outer of holding) {
|
|
6466
|
+
units.push({
|
|
6467
|
+
kind: 'suite', name: outer.name, suite: helpers.enclosing(outer).map(up => up.name),
|
|
6468
|
+
line: outer.nameLine || outer.startLine, startLine: outer.startLine,
|
|
6469
|
+
endLine: outer.endLine ?? outer.startLine, sourceSpan: outer.sourceSpan,
|
|
6470
|
+
});
|
|
6471
|
+
}
|
|
6472
|
+
// Tests a macro makes are not listed: a function a macro of the
|
|
6473
|
+
// project expands to (its attributes stand in the macro), and an
|
|
6474
|
+
// invocation among the items whose tokens name a test attribute
|
|
6475
|
+
// (proptest! { #[test] fn ... }).
|
|
6476
|
+
let more = (fileEntry.symbols || []).some(symbol => symbol.type === 'function' && symbol.macroExpansion &&
|
|
6477
|
+
!units.some(unit => unit.kind === 'test' && unit.startLine === symbol.startLine && unit.name === symbol.name));
|
|
6478
|
+
const tree = more ? null : helpers.tree();
|
|
6479
|
+
const namesTest = node => (node.type === 'identifier' && /(^|_)test$/.test(node.text)) || node.namedChildren.some(namesTest);
|
|
6480
|
+
const items = node => {
|
|
6481
|
+
for (const child of node.namedChildren) {
|
|
6482
|
+
if (more) return;
|
|
6483
|
+
if (child.type === 'macro_invocation') {
|
|
6484
|
+
if (namesTest(child)) more = true;
|
|
6485
|
+
} else if (child.type === 'expression_statement' && child.namedChildren[0]?.type === 'macro_invocation') {
|
|
6486
|
+
if (namesTest(child)) more = true;
|
|
6487
|
+
} else if (child.type === 'mod_item' || child.type === 'declaration_list') {
|
|
6488
|
+
items(child);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
if (tree) items(tree.rootNode);
|
|
6493
|
+
return { units, inheriting: more };
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6392
6496
|
/**
|
|
6393
6497
|
* Classify a Rust symbol as a runtime entry point of a specific kind.
|
|
6394
6498
|
* Returns 'test' | 'main' | 'framework' | null.
|
|
@@ -6444,5 +6548,6 @@ module.exports = {
|
|
|
6444
6548
|
findUsagesInCode,
|
|
6445
6549
|
isEntryPoint,
|
|
6446
6550
|
getEntryPointKind,
|
|
6551
|
+
testUnits,
|
|
6447
6552
|
parse
|
|
6448
6553
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sameNode } = require('./utils');
|
|
4
|
+
const SCOPES = new Set(['program', 'statement_block', 'switch_body', 'for_statement', 'for_in_statement',
|
|
5
|
+
'function_declaration', 'generator_function_declaration', 'function_expression', 'generator_function',
|
|
6
|
+
'arrow_function', 'method_definition', 'catch_clause', 'class', 'class_declaration']);
|
|
7
|
+
const FUNCTIONS = new Set(['function_declaration', 'generator_function_declaration', 'function_expression',
|
|
8
|
+
'generator_function', 'arrow_function', 'method_definition']);
|
|
9
|
+
|
|
10
|
+
function hasName(pattern, name) {
|
|
11
|
+
return !!pattern && (pattern.text === name ||
|
|
12
|
+
pattern.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern']).some(id => id.text === name));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Decode a JavaScript literal without evaluating project code. */
|
|
16
|
+
function jsString(node) {
|
|
17
|
+
if (!node || !['string', 'template_string'].includes(node.type) ||
|
|
18
|
+
node.namedChildren.some(child => child.type === 'template_substitution')) return null;
|
|
19
|
+
const escapes = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', 0: '\0' };
|
|
20
|
+
return node.text.slice(1, -1).replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0-3][0-7]{0,2}|[4-7][0-7]?|\r?\n|[\s\S])/g, (whole, escape) => {
|
|
21
|
+
if (/^[0-7]+$/.test(escape)) return String.fromCharCode(parseInt(escape, 8));
|
|
22
|
+
if (escape[0] === 'u' || escape[0] === 'x') {
|
|
23
|
+
const code = parseInt(escape.replace(/^[ux]\{?|\}$/g, ''), 16);
|
|
24
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
25
|
+
}
|
|
26
|
+
if (escape === '\n' || escape === '\r\n') return '';
|
|
27
|
+
return Object.hasOwn(escapes, escape) ? escapes[escape] : escape;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One stable const binding, with block and parameter shadowing. */
|
|
32
|
+
function jsConstValue(node, constructedMutable = false) {
|
|
33
|
+
if (node?.type !== 'identifier') return null;
|
|
34
|
+
const name = node.text;
|
|
35
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
36
|
+
if (scope.type === 'with_statement') return null;
|
|
37
|
+
if (!SCOPES.has(scope.type)) continue;
|
|
38
|
+
// Named function/generator/class expressions bind their own name in
|
|
39
|
+
// their body; it never denotes an outer const of the same spelling.
|
|
40
|
+
if (['function_expression', 'generator_function', 'function_declaration',
|
|
41
|
+
'generator_function_declaration', 'class', 'class_declaration'].includes(scope.type) &&
|
|
42
|
+
scope.childForFieldName('name')?.text === name) return null;
|
|
43
|
+
const params = scope.childForFieldName('parameters') || scope.childForFieldName('parameter');
|
|
44
|
+
if (hasName(params, name) || (scope.type === 'for_in_statement' && hasName(scope.childForFieldName('left'), name))) return null;
|
|
45
|
+
if (FUNCTIONS.has(scope.type)) {
|
|
46
|
+
let shadow = false;
|
|
47
|
+
const scanVars = current => {
|
|
48
|
+
for (const child of current.namedChildren) {
|
|
49
|
+
if (FUNCTIONS.has(child.type)) continue;
|
|
50
|
+
if (child.type === 'variable_declarator' && child.parent?.type === 'variable_declaration' &&
|
|
51
|
+
hasName(child.childForFieldName('name'), name)) shadow = true;
|
|
52
|
+
scanVars(child);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
scanVars(scope);
|
|
56
|
+
if (shadow) return null;
|
|
57
|
+
}
|
|
58
|
+
const bindings = [];
|
|
59
|
+
const walk = current => {
|
|
60
|
+
for (const child of current.namedChildren) {
|
|
61
|
+
if (['function_declaration', 'generator_function_declaration', 'class_declaration'].includes(child.type) && child.childForFieldName('name')?.text === name) bindings.push(child);
|
|
62
|
+
if (SCOPES.has(child.type) && !sameNode(child, scope)) continue;
|
|
63
|
+
if (child.type === 'variable_declarator' && hasName(child.childForFieldName('name'), name)) bindings.push(child);
|
|
64
|
+
else if (child.type === 'import_statement' && hasName(child, name)) bindings.push(child);
|
|
65
|
+
walk(child);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(scope);
|
|
69
|
+
if (bindings.length === 0) continue;
|
|
70
|
+
if (bindings.length !== 1) return null;
|
|
71
|
+
const binding = bindings[0];
|
|
72
|
+
if (binding.type !== 'variable_declarator' || binding.childForFieldName('name')?.type !== 'identifier' ||
|
|
73
|
+
binding.endIndex > node.startIndex) return null;
|
|
74
|
+
const value = binding.childForFieldName('value');
|
|
75
|
+
if (binding.parent?.type === 'lexical_declaration' && binding.parent.child(0)?.type === 'const') return value;
|
|
76
|
+
if (!constructedMutable || value?.type !== 'new_expression') return null;
|
|
77
|
+
const writes = node.tree.rootNode.descendantsOfType(['assignment_expression', 'augmented_assignment_expression', 'update_expression']);
|
|
78
|
+
if (writes.some(write => hasName(write.childForFieldName('left') || write.childForFieldName('argument'), name))) return null;
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function jsStaticKey(node) {
|
|
85
|
+
return jsString(node) ?? jsString(jsConstValue(node));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { jsString, jsConstValue, jsStaticKey };
|