ucn 5.5.1 → 5.5.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 +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/reflection.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
const { langTraits } = require('../languages');
|
|
28
|
+
const { sameNode } = require('../languages/utils');
|
|
28
29
|
|
|
29
30
|
const WILD = '\u0000*';
|
|
30
31
|
const MODULE_RECEIVER = Object.freeze({ kind: 'module' });
|
|
@@ -501,6 +502,224 @@ function classifyReceiver(node, language) {
|
|
|
501
502
|
return null;
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Names one call away (fix #428)
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
// Callables whose parameters a reflective name can be (grammar node types).
|
|
510
|
+
const PARAMETER_OWNERS = new Set([
|
|
511
|
+
'function_declaration', 'generator_function_declaration', 'function_expression', 'generator_function',
|
|
512
|
+
'arrow_function', 'method_definition', 'function_definition', 'method_declaration',
|
|
513
|
+
'constructor_declaration', 'local_function_statement', 'func_literal',
|
|
514
|
+
]);
|
|
515
|
+
const PARAMETER_LISTS = new Set(['formal_parameters', 'parameters', 'parameter_list']);
|
|
516
|
+
const VARIADIC_PARAMETERS = new Set(['rest_pattern', 'list_splat_pattern', 'dictionary_splat_pattern',
|
|
517
|
+
'spread_parameter', 'variadic_parameter_declaration', 'variadic_parameter']);
|
|
518
|
+
|
|
519
|
+
/** The parameter names of a callable in declaration order (null if unknown). */
|
|
520
|
+
function _parameterNames(fn) {
|
|
521
|
+
const list = fn.childForFieldName('parameters') ||
|
|
522
|
+
(fn.namedChildren || []).find(child => PARAMETER_LISTS.has(child.type));
|
|
523
|
+
if (!list) {
|
|
524
|
+
// `x => ..`: a single bare parameter.
|
|
525
|
+
const single = fn.childForFieldName('parameter');
|
|
526
|
+
return single?.type === 'identifier' ? [single.text] : null;
|
|
527
|
+
}
|
|
528
|
+
const names = [];
|
|
529
|
+
for (const param of list.namedChildren) {
|
|
530
|
+
if (param.type === 'comment') continue;
|
|
531
|
+
if (param.type === 'identifier') { names.push(param.text); continue; }
|
|
532
|
+
// A Go declaration may name several parameters of one type.
|
|
533
|
+
if (param.type === 'parameter_declaration' || param.type === 'variadic_parameter_declaration') {
|
|
534
|
+
const ids = param.namedChildren.filter(child => child.type === 'identifier');
|
|
535
|
+
if (ids.length === 0) names.push(null);
|
|
536
|
+
for (const id of ids) names.push(id.text);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const named = param.childForFieldName('name') || param.childForFieldName('pattern') ||
|
|
540
|
+
param.childForFieldName('left') ||
|
|
541
|
+
(param.type === 'typed_parameter' ? param.namedChild(0) : null);
|
|
542
|
+
names.push(named?.type === 'identifier' ? named.text : null);
|
|
543
|
+
}
|
|
544
|
+
return names;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The name a callable is called by: its own, or the binding it is assigned to. */
|
|
548
|
+
function _callableName(fn) {
|
|
549
|
+
const own = fn.childForFieldName('name');
|
|
550
|
+
if (own) return own.text;
|
|
551
|
+
const parent = fn.parent;
|
|
552
|
+
if (parent?.type === 'variable_declarator') return parent.childForFieldName('name')?.text || null;
|
|
553
|
+
if (parent?.type === 'pair') return parent.childForFieldName('key')?.text?.replace(/^["']|["']$/g, '') || null;
|
|
554
|
+
if (parent?.type === 'assignment_expression') {
|
|
555
|
+
const left = parent.childForFieldName('left');
|
|
556
|
+
if (left?.type === 'identifier') return left.text;
|
|
557
|
+
if (left?.type === 'member_expression') return left.childForFieldName('property')?.text || null;
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Does `body` assign the name anywhere (nested callables included)? */
|
|
563
|
+
function _assignsName(body, name) {
|
|
564
|
+
const stack = [body];
|
|
565
|
+
while (stack.length > 0) {
|
|
566
|
+
const node = stack.pop();
|
|
567
|
+
if (_bindingPairs(node).some(([bound]) => bound === name)) return true;
|
|
568
|
+
for (const child of node.namedChildren || []) stack.push(child);
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A reflective name that is a parameter of an enclosing callable, never
|
|
575
|
+
* rebound: { name, line, index } (the call-site argument position), else
|
|
576
|
+
* null. Its value is the argument the callable's callers pass.
|
|
577
|
+
*/
|
|
578
|
+
function _keyParameter(ident, language) {
|
|
579
|
+
if (ident?.type !== 'identifier') return null;
|
|
580
|
+
for (let fn = ident.parent; fn; fn = fn.parent) {
|
|
581
|
+
if (CLASS_NODES.has(fn.type)) return null;
|
|
582
|
+
if (!PARAMETER_OWNERS.has(fn.type)) continue;
|
|
583
|
+
const names = _parameterNames(fn);
|
|
584
|
+
if (!names) return null;
|
|
585
|
+
let position = names.indexOf(ident.text);
|
|
586
|
+
if (position < 0) continue; // captured from an outer callable
|
|
587
|
+
const name = _callableName(fn);
|
|
588
|
+
if (!name || _assignsName(fn.childForFieldName('body') || fn, ident.text)) return null;
|
|
589
|
+
// A method's receiver parameter (`self`, `cls`) is not a call
|
|
590
|
+
// argument of a bound call.
|
|
591
|
+
const selfNames = langTraits(language)?.selfParam || [];
|
|
592
|
+
let owner = fn.parent?.type === 'decorated_definition' ? fn.parent.parent : fn.parent;
|
|
593
|
+
if (fn.type === 'function_definition' && owner?.type === 'block' &&
|
|
594
|
+
owner.parent?.type === 'class_definition' && selfNames.includes(names[0])) {
|
|
595
|
+
if (position === 0) return null;
|
|
596
|
+
position--;
|
|
597
|
+
}
|
|
598
|
+
// The class a method belongs to: a call naming another class as its
|
|
599
|
+
// qualifier is no call of it. A C# extension's `this` parameter is
|
|
600
|
+
// the receiver of an extension call (`t.M(x)`).
|
|
601
|
+
const ownerClass = _enclosingClass(fn, false);
|
|
602
|
+
const ownerName = ownerClass ? _classInfo(ownerClass)?.name || null : null;
|
|
603
|
+
const params = (fn.childForFieldName('parameters')?.namedChildren || []).filter(child => child.type !== 'comment');
|
|
604
|
+
const firstParam = params.find(child => child.type === 'parameter');
|
|
605
|
+
const extension = !!firstParam?.children?.some(child => child.type === 'modifier' && child.text === 'this');
|
|
606
|
+
// A call passing more arguments than a fixed parameter list takes
|
|
607
|
+
// is not a call of it.
|
|
608
|
+
const variadic = params.some(param => VARIADIC_PARAMETERS.has(param.type) ||
|
|
609
|
+
param.children?.some(child => child.type === 'modifier' && child.text === 'params') ||
|
|
610
|
+
param.namedChildren?.some(child => VARIADIC_PARAMETERS.has(child.type)));
|
|
611
|
+
const maxArgs = variadic ? null : names.length - (names.length > 0 && selfNames.includes(names[0]) &&
|
|
612
|
+
fn.type === 'function_definition' && owner?.type === 'block' && owner.parent?.type === 'class_definition' ? 1 : 0);
|
|
613
|
+
return { name, line: fn.startPosition.row + 1, index: position,
|
|
614
|
+
...(ownerName && { owner: ownerName }), ...(extension && { extension: true }),
|
|
615
|
+
...(Number.isInteger(maxArgs) && { maxArgs }) };
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const MAX_KEY_CALLS = 256;
|
|
621
|
+
// Characters after a call's name its arguments are looked for in, when the
|
|
622
|
+
// record keeps only the name's position.
|
|
623
|
+
const KEY_CALL_WINDOW = 4096;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The names a parameter-keyed site receives (fix #428): the string-shaped
|
|
627
|
+
* arguments at the parameter's position in the calls naming its callable,
|
|
628
|
+
* read from each call's own AST (one hop). Null when no call passes one.
|
|
629
|
+
*/
|
|
630
|
+
function resolveKeyParameter(index, site, language, onlyFiles = null, onlyName = null) {
|
|
631
|
+
const key = site.keyParameter;
|
|
632
|
+
const { getCachedCalls } = require('./callers');
|
|
633
|
+
const { getParser, safeParse } = require('../languages');
|
|
634
|
+
const files = [...(onlyFiles || index.getCalleeFiles?.(key.name) || [])].sort();
|
|
635
|
+
const exact = new Set();
|
|
636
|
+
let seen = 0;
|
|
637
|
+
for (const file of files) {
|
|
638
|
+
const entry = index.files.get(file);
|
|
639
|
+
if (!entry || _family(entry.language) !== _family(language)) continue;
|
|
640
|
+
// A call qualified by another class's name never calls the method;
|
|
641
|
+
// an extension call passes the `this` argument as its receiver.
|
|
642
|
+
const shift = call => key.extension && call.isMethod && call.receiver && call.receiver !== key.owner ? 1 : 0;
|
|
643
|
+
const position = call => key.index - shift(call);
|
|
644
|
+
let calls = (getCachedCalls(index, file) || []).filter(call => call.name === key.name &&
|
|
645
|
+
!call.isFunctionReference && position(call) >= 0 &&
|
|
646
|
+
!(Number.isInteger(key.maxArgs) && Number.isInteger(call.argCount) && call.argCount > key.maxArgs - shift(call)) &&
|
|
647
|
+
!(Number.isInteger(call.argCount) && call.argCount <= position(call)) &&
|
|
648
|
+
!(key.owner && call.receiverIsTypeQualified && call.receiver && call.receiver !== key.owner));
|
|
649
|
+
if (calls.length === 0) continue;
|
|
650
|
+
seen += calls.length;
|
|
651
|
+
if (seen > MAX_KEY_CALLS) return null;
|
|
652
|
+
let content;
|
|
653
|
+
try { content = index._readFile(file); } catch { continue; }
|
|
654
|
+
// A literal argument lies in the call's text: a call whose text
|
|
655
|
+
// spells no quoted `onlyName` cannot pass it, and needs no parse.
|
|
656
|
+
if (onlyName) {
|
|
657
|
+
const quoted = ['"', "'", '`'].map(quote => `${quote}${onlyName}${quote}`);
|
|
658
|
+
calls = calls.filter(call => {
|
|
659
|
+
const start = call.callStart ?? call.callSite?.start;
|
|
660
|
+
if (!Number.isInteger(start)) return true;
|
|
661
|
+
const text = content.slice(start, call.callEnd ?? start + KEY_CALL_WINDOW);
|
|
662
|
+
return quoted.some(spelling => text.includes(spelling));
|
|
663
|
+
});
|
|
664
|
+
if (calls.length === 0) continue;
|
|
665
|
+
}
|
|
666
|
+
const tree = index._getParsedTree?.(file, content, entry.language) || safeParse(getParser(entry.language), content);
|
|
667
|
+
if (!tree) continue;
|
|
668
|
+
const api = langTraits(entry.language)?.reflectionApi || {};
|
|
669
|
+
const ctx = { formatCalls: api.formatCalls || null, resolve: makeResolver() };
|
|
670
|
+
for (const call of calls) {
|
|
671
|
+
const offset = call.callStart ?? call.callSite?.start;
|
|
672
|
+
if (!Number.isInteger(offset)) continue;
|
|
673
|
+
let node = tree.rootNode.descendantForIndex(offset);
|
|
674
|
+
for (let depth = 0; node && depth < 6 && !CALL_NODE_TYPES.includes(node.type); depth++) node = node.parent;
|
|
675
|
+
const args = node ? _callArgs(node) : null;
|
|
676
|
+
// Keyword, spread or missing arguments do not bind by position.
|
|
677
|
+
if (!args || args.slice(0, position(call) + 1).some(arg =>
|
|
678
|
+
/keyword_argument|spread|splat|named_argument/.test(arg.type) ||
|
|
679
|
+
(arg.type === 'argument' && arg.childForFieldName('name')))) continue;
|
|
680
|
+
let arg = args[position(call)];
|
|
681
|
+
if (arg?.type === 'argument') arg = arg.namedChildren.find(child => child.type !== 'comment') || arg;
|
|
682
|
+
if (!arg) continue;
|
|
683
|
+
const cls = classifyNameExpression(arg, ctx);
|
|
684
|
+
if (cls.name) exact.add(cls.name);
|
|
685
|
+
for (const pattern of cls.patterns || []) if (!pattern.includes('*')) exact.add(pattern);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return exact.size > 0 ? [...exact].sort() : null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const QUOTED_IDENTIFIER = /(["'`])([A-Za-z_$][\w$]*)\1/g;
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Could a parameter-keyed site receive `name` (fix #428)? Only when a file
|
|
695
|
+
* holding a call of its callable spells `name` as a string literal: the
|
|
696
|
+
* files' quoted identifiers are read once, and the AST resolution runs only
|
|
697
|
+
* for a name they hold.
|
|
698
|
+
*/
|
|
699
|
+
function _keyParameterPasses(index, pidx, entry, name) {
|
|
700
|
+
const files = index.getCalleeFiles?.(entry.site.keyParameter.name);
|
|
701
|
+
if (!files || files.size === 0) return false;
|
|
702
|
+
const spelled = [];
|
|
703
|
+
for (const file of files) {
|
|
704
|
+
let quoted = pidx.quoted.get(file);
|
|
705
|
+
if (!quoted) {
|
|
706
|
+
quoted = new Set();
|
|
707
|
+
try {
|
|
708
|
+
for (const match of index._readFile(file).matchAll(QUOTED_IDENTIFIER)) quoted.add(match[2]);
|
|
709
|
+
} catch { /* unreadable: no literal */ }
|
|
710
|
+
pidx.quoted.set(file, quoted);
|
|
711
|
+
}
|
|
712
|
+
if (quoted.has(name)) spelled.push(file);
|
|
713
|
+
}
|
|
714
|
+
if (spelled.length === 0) return false;
|
|
715
|
+
let byName = pidx.keyNames.get(entry);
|
|
716
|
+
if (!byName) pidx.keyNames.set(entry, byName = new Map());
|
|
717
|
+
if (!byName.has(name)) {
|
|
718
|
+
byName.set(name, !!resolveKeyParameter(index, entry.site, entry.language, spelled, name)?.includes(name));
|
|
719
|
+
}
|
|
720
|
+
return byName.get(name);
|
|
721
|
+
}
|
|
722
|
+
|
|
504
723
|
// ---------------------------------------------------------------------------
|
|
505
724
|
// Site extraction
|
|
506
725
|
// ---------------------------------------------------------------------------
|
|
@@ -558,7 +777,12 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
558
777
|
const push = (node, kind, nameNode, receiverNode, receiverFact) => {
|
|
559
778
|
const cls = classifyNameExpression(nameNode, ctx);
|
|
560
779
|
const expression = String(node.text || '');
|
|
780
|
+
// A name that is the enclosing callable's parameter is decided one
|
|
781
|
+
// call away, by what its callers pass (fix #428).
|
|
782
|
+
const keyParameter = cls.dynamic && !cls.patterns && _unwrap(nameNode)?.type === 'identifier'
|
|
783
|
+
? _keyParameter(_unwrap(nameNode), language) : null;
|
|
561
784
|
sites.push({
|
|
785
|
+
...(keyParameter && { keyParameter }),
|
|
562
786
|
kind,
|
|
563
787
|
line: node.startPosition.row + 1,
|
|
564
788
|
expression: expression.length > EXPRESSION_CAP
|
|
@@ -595,6 +819,14 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
595
819
|
// counts; opaque keys stay the computed-dispatch disclosure.
|
|
596
820
|
const index = node.childForFieldName('index');
|
|
597
821
|
const inner = _unwrap(index);
|
|
822
|
+
// `registry[type](..)` with `type` a parameter of the enclosing
|
|
823
|
+
// callable dispatches by the name its callers pass (fix #428).
|
|
824
|
+
if (inner?.type === 'identifier' && node.parent?.type === 'call_expression' &&
|
|
825
|
+
sameNode(node.parent.childForFieldName('function'), node) &&
|
|
826
|
+
_keyParameter(inner, language)) {
|
|
827
|
+
push(node, 'computed', index, node.childForFieldName('object'));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
598
830
|
if (!inner || !COMPUTED_PATTERN_KEYS.has(inner.type)) return;
|
|
599
831
|
const cls = classifyNameExpression(index, ctx);
|
|
600
832
|
if (cls.patterns && cls.patterns.length > 0) {
|
|
@@ -696,6 +928,14 @@ function _longestFragment(pattern) {
|
|
|
696
928
|
return best;
|
|
697
929
|
}
|
|
698
930
|
|
|
931
|
+
/** Whether a pattern entry names members by some literal text (fix #433):
|
|
932
|
+
* a fully dynamic name (`eval(code)`, `globals()[key]` with no literal
|
|
933
|
+
* part) reaches every top-level name of its module alike and is attributed
|
|
934
|
+
* to none of them; deadcode still withholds what it may reach. */
|
|
935
|
+
function patternIsAttributable(pattern) {
|
|
936
|
+
return _longestFragment(String(pattern || '').split('\u0000').join('')) !== '';
|
|
937
|
+
}
|
|
938
|
+
|
|
699
939
|
function _family(language) {
|
|
700
940
|
return langTraits(language)?.reflectionApi?.family || language;
|
|
701
941
|
}
|
|
@@ -713,6 +953,7 @@ function reflectionPatternIndex(index) {
|
|
|
713
953
|
const lengths = new Set();
|
|
714
954
|
let entryCount = 0;
|
|
715
955
|
const moduleScoped = new Map();
|
|
956
|
+
const keyParameterSites = [];
|
|
716
957
|
for (const [file, sites] of byFile) {
|
|
717
958
|
const fe = index.files.get(file);
|
|
718
959
|
if (!fe) continue;
|
|
@@ -728,6 +969,11 @@ function reflectionPatternIndex(index) {
|
|
|
728
969
|
}
|
|
729
970
|
continue;
|
|
730
971
|
}
|
|
972
|
+
if (site.keyParameter && !site.patterns) {
|
|
973
|
+
keyParameterSites.push({ file, relativePath: fe.relativePath,
|
|
974
|
+
language: fe.language, family: _family(fe.language), site });
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
731
977
|
if (!site.patterns) continue;
|
|
732
978
|
for (const pattern of site.patterns) {
|
|
733
979
|
const frag = _longestFragment(pattern);
|
|
@@ -740,7 +986,8 @@ function reflectionPatternIndex(index) {
|
|
|
740
986
|
}
|
|
741
987
|
}
|
|
742
988
|
}
|
|
743
|
-
const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped
|
|
989
|
+
const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped,
|
|
990
|
+
keyParameterSites, quoted: new Map(), keyNames: new Map() };
|
|
744
991
|
index._reflectionPatternIndex = result;
|
|
745
992
|
return result;
|
|
746
993
|
}
|
|
@@ -755,8 +1002,12 @@ function patternEntriesFor(index, name, language, file = null) {
|
|
|
755
1002
|
if (globMatches(entry.pattern, name)) out.push(entry);
|
|
756
1003
|
}
|
|
757
1004
|
}
|
|
758
|
-
if (pidx.entryCount === 0) return out;
|
|
759
1005
|
const family = language ? _family(language) : null;
|
|
1006
|
+
for (const entry of pidx.keyParameterSites) {
|
|
1007
|
+
if (family && entry.family !== family) continue;
|
|
1008
|
+
if (_keyParameterPasses(index, pidx, entry, name)) out.push({ ...entry, pattern: name });
|
|
1009
|
+
}
|
|
1010
|
+
if (pidx.entryCount === 0) return out;
|
|
760
1011
|
const seenFrag = new Set();
|
|
761
1012
|
for (const len of pidx.lengths) {
|
|
762
1013
|
if (len > name.length) break;
|
|
@@ -873,5 +1124,7 @@ module.exports = {
|
|
|
873
1124
|
reflectionEntryReaches,
|
|
874
1125
|
reflectionPatternReferences,
|
|
875
1126
|
reflectionPatternIndex,
|
|
1127
|
+
patternIsAttributable,
|
|
876
1128
|
resetReflectionIndex,
|
|
1129
|
+
resolveKeyParameter,
|
|
877
1130
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const cache = require('./cache');
|
|
5
|
+
const { indexedContent, parseViewParts, syntaxMode, SYNTAX_FAMILIES } = require('./source-memo');
|
|
6
|
+
const { langTraits } = require('../languages');
|
|
7
|
+
|
|
8
|
+
/** Cache AST-proved positions only; the caller resolves each binding anew. */
|
|
9
|
+
function callTokenSyntax(index, file, entry, content, range, name, line, derive) {
|
|
10
|
+
if (index.isProvenanceFactIndex || !entry?.hash ||
|
|
11
|
+
!langTraits(entry.language)?.physicalCallTokenSyntax ||
|
|
12
|
+
!Number.isInteger(range.start) || !Number.isInteger(range.end) ||
|
|
13
|
+
range.start < 0 || range.end <= range.start || !Number.isInteger(line) || line <= 0) return derive();
|
|
14
|
+
const tree = index._peekParsedTree?.(file, entry?.language);
|
|
15
|
+
const state = sourceState(index, file, entry, content, tree);
|
|
16
|
+
if (!state?.mode) return derive();
|
|
17
|
+
let facts = state.names.get(name);
|
|
18
|
+
if (!facts) {
|
|
19
|
+
const value = cache.getUsageResult(index, file, name, state.mode);
|
|
20
|
+
const rows = Array.isArray(value) && value.every(row => Array.isArray(row) &&
|
|
21
|
+
row.length === 4 && row.every(Number.isInteger) &&
|
|
22
|
+
row[0] >= 0 && row[1] > row[0] && row[2] > 0 && row[3] >= 0)
|
|
23
|
+
? value.slice() : [];
|
|
24
|
+
const positions = new Map();
|
|
25
|
+
let duplicate = false;
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
const key = `${row[0]}:${row[1]}:${row[2]}`;
|
|
28
|
+
if (positions.has(key)) duplicate = true;
|
|
29
|
+
positions.set(key, row[3]);
|
|
30
|
+
}
|
|
31
|
+
facts = duplicate ? { rows: [], positions: new Map() } : { rows, positions };
|
|
32
|
+
state.names.set(name, facts);
|
|
33
|
+
}
|
|
34
|
+
const key = `${range.start}:${range.end}:${line}`;
|
|
35
|
+
const stored = facts.positions.get(key);
|
|
36
|
+
state.lines ||= content.split('\n');
|
|
37
|
+
const lineText = state.lines[line - 1] || '';
|
|
38
|
+
if (!state.lineStarts) {
|
|
39
|
+
let at = 0;
|
|
40
|
+
state.lineStarts = state.lines.map(text => { const start = at; at += text.length + 1; return start; });
|
|
41
|
+
}
|
|
42
|
+
const lineStart = state.lineStarts[line - 1];
|
|
43
|
+
const inRange = column => Number.isInteger(lineStart) && lineStart + column >= range.start &&
|
|
44
|
+
lineStart + column + name.length <= range.end;
|
|
45
|
+
if (!tree && stored !== undefined && inRange(stored) &&
|
|
46
|
+
lineText.slice(stored, stored + name.length) === name) return stored;
|
|
47
|
+
const column = derive();
|
|
48
|
+
if (column === stored) return column;
|
|
49
|
+
if (Number.isInteger(column) && column >= 0 && inRange(column) && Number.isInteger(range.start) &&
|
|
50
|
+
Number.isInteger(range.end) && range.end > range.start && Number.isInteger(line) &&
|
|
51
|
+
line > 0 && lineText.slice(column, column + name.length) === name) {
|
|
52
|
+
const rows = facts.rows.filter(row =>
|
|
53
|
+
row[0] !== range.start || row[1] !== range.end || row[2] !== line);
|
|
54
|
+
rows.push([range.start, range.end, line, column]);
|
|
55
|
+
facts.rows = rows;
|
|
56
|
+
facts.positions.set(key, column);
|
|
57
|
+
cache.storeUsageResult(index, file, name, state.mode, rows);
|
|
58
|
+
}
|
|
59
|
+
return column;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The selected declaration is a fresh input; only its physical name column is saved. */
|
|
63
|
+
function declarationTokenSyntax(index, file, entry, content, line, name, definition, allIdentifiers = true) {
|
|
64
|
+
if (index.isProvenanceFactIndex || !entry?.hash ||
|
|
65
|
+
!langTraits(entry.language)?.physicalCallTokenSyntax) return null;
|
|
66
|
+
const tree = index._peekParsedTree?.(file, entry?.language);
|
|
67
|
+
const state = sourceState(index, file, entry, content, tree);
|
|
68
|
+
if (!state?.mode) return null;
|
|
69
|
+
let identity;
|
|
70
|
+
try {
|
|
71
|
+
const description = JSON.stringify([allIdentifiers, definition || null], (_key, value) =>
|
|
72
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
73
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value);
|
|
74
|
+
identity = crypto.createHash('sha256').update(description).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
catch { return null; }
|
|
77
|
+
const mode = state.mode.replace(`${SYNTAX_FAMILIES.callToken.prefix}:`, `${SYNTAX_FAMILIES.declarationToken.prefix}:`) +
|
|
78
|
+
`:${line}:${identity}`;
|
|
79
|
+
const saved = cache.getUsageResult(index, file, name, mode);
|
|
80
|
+
state.lines ||= content.split('\n');
|
|
81
|
+
const text = state.lines[line - 1] || '';
|
|
82
|
+
const valid = value => Array.isArray(value) && value.length === 1 &&
|
|
83
|
+
Number.isInteger(value[0]) && value[0] >= 0 &&
|
|
84
|
+
text.slice(value[0], value[0] + name.length) === name;
|
|
85
|
+
return {
|
|
86
|
+
columns: !tree && valid(saved) ? saved.slice() : null,
|
|
87
|
+
save(columns) {
|
|
88
|
+
if (valid(columns) && (!valid(saved) || saved[0] !== columns[0])) {
|
|
89
|
+
cache.storeUsageResult(index, file, name, mode, columns.slice());
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sourceState(index, file, entry, content, tree) {
|
|
96
|
+
if (index.isProvenanceFactIndex || !entry?.hash || !index._usageResultCache ||
|
|
97
|
+
!langTraits(entry.language)?.physicalCallTokenSyntax) return null;
|
|
98
|
+
const memo = index._opMemo?.('callTokenSyntax', () => new Map());
|
|
99
|
+
let state = memo?.get(file);
|
|
100
|
+
// Preserve the existing query-tree LRU and avoid rehashing a hot tree.
|
|
101
|
+
if (!state && tree) return null;
|
|
102
|
+
if (!state || state.content !== content || state.hash !== entry.hash) {
|
|
103
|
+
const eligible = indexedContent(index, file, entry, content);
|
|
104
|
+
const mode = eligible ? syntaxMode('callToken', parseViewParts(entry)) : null;
|
|
105
|
+
state = { content, hash: entry.hash, mode, names: new Map(), lines: null };
|
|
106
|
+
memo?.set(file, state);
|
|
107
|
+
}
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { callTokenSyntax, declarationTokenSyntax };
|
package/core/reporting.js
CHANGED
|
@@ -15,6 +15,11 @@ const { summarizeCommandTrust } = require('./trust-matrix');
|
|
|
15
15
|
const { projectComputedDispatch } = require('./ast-analysis');
|
|
16
16
|
const { langTraits } = require('../languages');
|
|
17
17
|
|
|
18
|
+
// Modules read lazily (load cycles), each resolved once: a require() per
|
|
19
|
+
// call in hot paths was measurable (fix #436).
|
|
20
|
+
let lazySharedModuleCache = null;
|
|
21
|
+
const lazySharedModule = () => lazySharedModuleCache || (lazySharedModuleCache = require('./shared'));
|
|
22
|
+
|
|
18
23
|
function matchesReportingScope(index, relativePath, options = {}) {
|
|
19
24
|
if (!relativePath) return false;
|
|
20
25
|
if (options.file && !relativePath.includes(options.file)) return false;
|
|
@@ -124,6 +129,7 @@ function getStats(index, options = {}) {
|
|
|
124
129
|
// Ensure the calls cache is fully populated before counting.
|
|
125
130
|
// First-time stats --hot may need to parse files to extract calls;
|
|
126
131
|
// subsequent runs use the persisted calls cache.
|
|
132
|
+
require('./cache').ensureCallsCacheLoaded(index);
|
|
127
133
|
if (typeof index.buildCalleeIndex === 'function' && !index.calleeIndex) {
|
|
128
134
|
index.buildCalleeIndex();
|
|
129
135
|
}
|
|
@@ -209,7 +215,7 @@ function getStats(index, options = {}) {
|
|
|
209
215
|
// candidate set, the exact count, AND the upper bound (a bound that
|
|
210
216
|
// still counted them stopped the early exit from firing, so the
|
|
211
217
|
// orientation fell back to its approximate refinement budget).
|
|
212
|
-
const { inlineTestRanges, lineInRanges } =
|
|
218
|
+
const { inlineTestRanges, lineInRanges } = lazySharedModule();
|
|
213
219
|
const inlineTestRangesByFile = new Map();
|
|
214
220
|
const inInlineTest = (file, line) => {
|
|
215
221
|
if (!file || !line) return false;
|
|
@@ -227,7 +233,7 @@ function getStats(index, options = {}) {
|
|
|
227
233
|
// the exact passes never scan them (every per-site verdict is
|
|
228
234
|
// file-local). Orientation on django spent most of its time
|
|
229
235
|
// resolving the test suite's calls, which it then discarded.
|
|
230
|
-
const { isTestPath } =
|
|
236
|
+
const { isTestPath } = lazySharedModule();
|
|
231
237
|
const countableFile = (filePath) => {
|
|
232
238
|
if (!scopedPaths.has(filePath)) return false;
|
|
233
239
|
const fileEntry = index.files.get(filePath);
|
|
@@ -307,7 +313,7 @@ function getStats(index, options = {}) {
|
|
|
307
313
|
if (upper === 0) continue;
|
|
308
314
|
if (index.files.get(symbol.file)?.isBundled) continue;
|
|
309
315
|
if (options.productionCallsOnly &&
|
|
310
|
-
(
|
|
316
|
+
(lazySharedModule().isTestPath(symbol.relativePath) ||
|
|
311
317
|
inInlineTest(symbol.file, symbol.startLine))) continue;
|
|
312
318
|
const identity = `${symbol.file}:${symbol.startLine}:${name}:` +
|
|
313
319
|
`${symbol.className || symbol.receiver || ''}:${symbol.params || ''}`;
|
|
@@ -418,7 +424,7 @@ function getStats(index, options = {}) {
|
|
|
418
424
|
const testPath = (file) => {
|
|
419
425
|
let verdict = testPathMemo.get(file);
|
|
420
426
|
if (verdict === undefined) {
|
|
421
|
-
verdict =
|
|
427
|
+
verdict = lazySharedModule().isTestPath(file);
|
|
422
428
|
testPathMemo.set(file, verdict);
|
|
423
429
|
}
|
|
424
430
|
return verdict;
|
|
@@ -744,7 +750,7 @@ function doctor(index, options = {}) {
|
|
|
744
750
|
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
745
751
|
// (core/shared.js) — the SAME routine detectCompleteness uses for the about
|
|
746
752
|
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
747
|
-
const { hasTextBlindspots, countTextBlindspots } =
|
|
753
|
+
const { hasTextBlindspots, countTextBlindspots } = lazySharedModule();
|
|
748
754
|
|
|
749
755
|
const computedDispatch = projectComputedDispatch(index);
|
|
750
756
|
for (const [filePath, fe] of index.files) {
|
|
@@ -1130,7 +1136,7 @@ function orient(index, options = {}) {
|
|
|
1130
1136
|
in: options.in,
|
|
1131
1137
|
exclude: options.exclude,
|
|
1132
1138
|
};
|
|
1133
|
-
const { isTestPath } =
|
|
1139
|
+
const { isTestPath } = lazySharedModule();
|
|
1134
1140
|
const hasProductionFiles = [...index.files.values()].some(fileEntry => {
|
|
1135
1141
|
const relativePath = fileEntry.relativePath;
|
|
1136
1142
|
return relativePath &&
|
|
@@ -1172,7 +1178,7 @@ function orient(index, options = {}) {
|
|
|
1172
1178
|
let entrypoints = null;
|
|
1173
1179
|
try {
|
|
1174
1180
|
const { detectEntrypoints } = require('./entrypoints');
|
|
1175
|
-
const { addTestExclusions } =
|
|
1181
|
+
const { addTestExclusions } = lazySharedModule();
|
|
1176
1182
|
const orientExclude = Array.isArray(options.exclude)
|
|
1177
1183
|
? options.exclude
|
|
1178
1184
|
: (options.exclude
|