ucn 3.8.18 → 3.8.19
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 +2 -2
- package/cli/index.js +226 -58
- package/core/analysis.js +6 -2
- package/core/execute.js +17 -0
- package/core/output/analysis.js +1 -1
- package/core/registry.js +37 -4
- package/core/search.js +482 -127
- package/core/tracing.js +251 -39
- package/languages/javascript.js +19 -2
- package/languages/rust.js +40 -0
- package/mcp/server.js +75 -44
- package/package.json +1 -1
package/core/tracing.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { escapeRegExp } = require('./shared');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
+
const { getCachedCalls } = require('./callers');
|
|
14
|
+
const { detectLanguage, getLanguageModule } = require('../languages');
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Trace execution flow — build a tree of callees (down), callers (up), or both.
|
|
@@ -541,19 +543,11 @@ function affectedTests(index, name, options = {}) {
|
|
|
541
543
|
};
|
|
542
544
|
collectNames(blastResult.tree);
|
|
543
545
|
|
|
544
|
-
// Step 3:
|
|
545
|
-
|
|
546
|
-
for (const n of affectedNames) {
|
|
547
|
-
const escaped = escapeRegExp(n);
|
|
548
|
-
namePatterns.set(n, {
|
|
549
|
-
regex: new RegExp('\\b' + escaped + '\\b'),
|
|
550
|
-
callPattern: new RegExp(escaped + '\\s*\\('),
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Step 4: Scan test files once for all affected names
|
|
546
|
+
// Step 3: Scan test files for all affected names using AST
|
|
547
|
+
// Only count call and test-case matches as real coverage — not imports or bare references.
|
|
555
548
|
const exclude = options.exclude;
|
|
556
549
|
const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
|
|
550
|
+
const className = options.className || null;
|
|
557
551
|
const results = [];
|
|
558
552
|
for (const [filePath, fileEntry] of index.files) {
|
|
559
553
|
let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
|
|
@@ -565,40 +559,100 @@ function affectedTests(index, name, options = {}) {
|
|
|
565
559
|
if (excludeArr.length > 0 && !index.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
566
560
|
try {
|
|
567
561
|
const content = index._readFile(filePath);
|
|
568
|
-
const lines = content.split('\n');
|
|
569
562
|
const fileMatches = new Map();
|
|
570
563
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
564
|
+
for (const funcName of affectedNames) {
|
|
565
|
+
// Fast pre-check
|
|
566
|
+
if (!content.includes(funcName)) continue;
|
|
567
|
+
|
|
568
|
+
// AST-based usage detection
|
|
569
|
+
const astUsages = index._getCachedUsages(filePath, funcName);
|
|
570
|
+
if (!astUsages || astUsages.length === 0) continue;
|
|
571
|
+
|
|
572
|
+
// Build instance type map for className scoping (if applicable)
|
|
573
|
+
let instanceTypeMap = null;
|
|
574
|
+
if (className) {
|
|
575
|
+
instanceTypeMap = _buildInstanceTypeMapForTracing(index, filePath, content, className);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const seenLines = new Set();
|
|
579
|
+
for (const usage of astUsages) {
|
|
580
|
+
if (usage.usageType === 'definition') continue;
|
|
581
|
+
const lineKey = `${usage.line}:${usage.usageType}`;
|
|
582
|
+
if (seenLines.has(lineKey)) continue;
|
|
583
|
+
seenLines.add(lineKey);
|
|
584
|
+
|
|
585
|
+
let matchType;
|
|
586
|
+
if (usage.usageType === 'import') {
|
|
587
|
+
matchType = 'import';
|
|
588
|
+
} else if (usage.usageType === 'call') {
|
|
589
|
+
matchType = 'call';
|
|
590
|
+
} else {
|
|
591
|
+
matchType = 'reference';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// className scoping for calls: check receiver
|
|
595
|
+
if (className && matchType === 'call') {
|
|
596
|
+
if (!_receiverMatchesClassTracing(usage, className, instanceTypeMap,
|
|
597
|
+
index.getLineContent(filePath, usage.line), funcName)) continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// className scoping for references: require class-associated receiver
|
|
601
|
+
if (className && matchType === 'reference') {
|
|
602
|
+
if (!usage.receiver) continue;
|
|
603
|
+
if (usage.receiver !== className &&
|
|
604
|
+
!(instanceTypeMap && instanceTypeMap.get(usage.receiver) === className)) {
|
|
605
|
+
continue;
|
|
581
606
|
}
|
|
582
|
-
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
583
|
-
fileMatches.get(funcName).push({
|
|
584
|
-
line: idx + 1, content: line.trim(),
|
|
585
|
-
matchType, functionName: funcName
|
|
586
|
-
});
|
|
587
607
|
}
|
|
608
|
+
|
|
609
|
+
const lineContent = index.getLineContent(filePath, usage.line);
|
|
610
|
+
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
611
|
+
fileMatches.get(funcName).push({
|
|
612
|
+
line: usage.line, content: lineContent.trim(),
|
|
613
|
+
matchType, functionName: funcName
|
|
614
|
+
});
|
|
588
615
|
}
|
|
589
|
-
|
|
616
|
+
|
|
617
|
+
// Language-aware test-case detection
|
|
618
|
+
_addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches);
|
|
619
|
+
}
|
|
590
620
|
|
|
591
621
|
if (fileMatches.size > 0) {
|
|
592
622
|
const coveredFunctions = [...fileMatches.keys()];
|
|
593
623
|
const allMatches = [];
|
|
594
624
|
for (const matches of fileMatches.values()) allMatches.push(...matches);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
625
|
+
// Deduplicate same line+function (test-case line might overlap with call line)
|
|
626
|
+
const dedupMap = new Map();
|
|
627
|
+
for (const m of allMatches) {
|
|
628
|
+
const key = `${m.line}:${m.functionName}`;
|
|
629
|
+
const existing = dedupMap.get(key);
|
|
630
|
+
if (!existing || _matchPriority(m.matchType) > _matchPriority(existing.matchType)) {
|
|
631
|
+
dedupMap.set(key, m);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const deduped = [...dedupMap.values()].sort((a, b) => a.line - b.line);
|
|
635
|
+
|
|
636
|
+
// Only count functions with call or test-case matches as covered.
|
|
637
|
+
// Import-only or reference-only functions are not real coverage.
|
|
638
|
+
const realCoveredFunctions = coveredFunctions.filter(fn => {
|
|
639
|
+
const fnMatches = deduped.filter(m => m.functionName === fn);
|
|
640
|
+
return fnMatches.some(m => m.matchType === 'call' || m.matchType === 'test-case');
|
|
601
641
|
});
|
|
642
|
+
|
|
643
|
+
// Only include file if it has real coverage
|
|
644
|
+
const realMatches = deduped.filter(m =>
|
|
645
|
+
m.matchType === 'call' || m.matchType === 'test-case' ||
|
|
646
|
+
realCoveredFunctions.includes(m.functionName)
|
|
647
|
+
);
|
|
648
|
+
if (realCoveredFunctions.length > 0) {
|
|
649
|
+
results.push({
|
|
650
|
+
file: fileEntry.relativePath,
|
|
651
|
+
coveredFunctions: realCoveredFunctions,
|
|
652
|
+
matchCount: realMatches.length,
|
|
653
|
+
matches: realMatches
|
|
654
|
+
});
|
|
655
|
+
}
|
|
602
656
|
}
|
|
603
657
|
} catch (e) { /* skip unreadable */ }
|
|
604
658
|
}
|
|
@@ -606,18 +660,35 @@ function affectedTests(index, name, options = {}) {
|
|
|
606
660
|
// Sort by coverage breadth then alphabetically
|
|
607
661
|
results.sort((a, b) => b.coveredFunctions.length - a.coveredFunctions.length || a.file.localeCompare(b.file));
|
|
608
662
|
|
|
609
|
-
// Compute coverage stats
|
|
663
|
+
// Compute coverage stats.
|
|
664
|
+
// Filter out test function names from affectedNames — they are callers,
|
|
665
|
+
// not production symbols that need test coverage.
|
|
666
|
+
const productionNames = new Set();
|
|
667
|
+
for (const n of affectedNames) {
|
|
668
|
+
// Check if this name is only found in test files
|
|
669
|
+
let foundInSource = false;
|
|
670
|
+
for (const [fp, fe] of index.files) {
|
|
671
|
+
if (isTestFile(fe.relativePath, fe.language)) continue;
|
|
672
|
+
if (fe.symbols?.some(s => s.name === n)) { foundInSource = true; break; }
|
|
673
|
+
}
|
|
674
|
+
if (foundInSource) productionNames.add(n);
|
|
675
|
+
}
|
|
676
|
+
// Fall back to full set if filtering removed everything (e.g., test-only project)
|
|
677
|
+
const namesForCoverage = productionNames.size > 0 ? productionNames : affectedNames;
|
|
678
|
+
|
|
610
679
|
const coveredSet = new Set();
|
|
611
|
-
for (const r of results) for (const f of r.coveredFunctions)
|
|
612
|
-
|
|
680
|
+
for (const r of results) for (const f of r.coveredFunctions) {
|
|
681
|
+
if (namesForCoverage.has(f)) coveredSet.add(f);
|
|
682
|
+
}
|
|
683
|
+
const uncovered = [...namesForCoverage].filter(n => !coveredSet.has(n));
|
|
613
684
|
|
|
614
685
|
return {
|
|
615
686
|
root: blastResult.root, file: blastResult.file, line: blastResult.line,
|
|
616
687
|
depth: blastResult.maxDepth,
|
|
617
|
-
affectedFunctions: [...
|
|
688
|
+
affectedFunctions: [...namesForCoverage],
|
|
618
689
|
testFiles: results,
|
|
619
690
|
summary: {
|
|
620
|
-
totalAffected:
|
|
691
|
+
totalAffected: namesForCoverage.size,
|
|
621
692
|
totalTestFiles: results.length,
|
|
622
693
|
coveredFunctions: coveredSet.size,
|
|
623
694
|
uncoveredCount: uncovered.length,
|
|
@@ -628,4 +699,145 @@ function affectedTests(index, name, options = {}) {
|
|
|
628
699
|
} finally { index._endOp(); }
|
|
629
700
|
}
|
|
630
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Add test-case matches for a function name in a test file (language-aware).
|
|
704
|
+
* Only adds test-case when the test body has a call match (not just import/reference).
|
|
705
|
+
*/
|
|
706
|
+
function _addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches) {
|
|
707
|
+
const lang = fileEntry.language;
|
|
708
|
+
const existingMatches = fileMatches.get(funcName) || [];
|
|
709
|
+
const existingLines = new Set(existingMatches.map(m => m.line));
|
|
710
|
+
|
|
711
|
+
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') {
|
|
712
|
+
const calls = getCachedCalls(index, filePath);
|
|
713
|
+
if (!calls) return;
|
|
714
|
+
const testFrameworkCalls = new Set(['describe', 'it', 'test', 'spec']);
|
|
715
|
+
for (const call of calls) {
|
|
716
|
+
if (!testFrameworkCalls.has(call.name)) continue;
|
|
717
|
+
const lineContent = index.getLineContent(filePath, call.line);
|
|
718
|
+
if (lineContent.includes(funcName) && !existingLines.has(call.line)) {
|
|
719
|
+
// Only add test-case if a call match exists in the test body
|
|
720
|
+
const endLine = _estimateTestBlockEndTracing(index, filePath, call.line);
|
|
721
|
+
const hasCallMatch = existingMatches.some(m =>
|
|
722
|
+
m.line >= call.line && m.line <= endLine &&
|
|
723
|
+
m.matchType === 'call'
|
|
724
|
+
);
|
|
725
|
+
if (!hasCallMatch) continue;
|
|
726
|
+
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
727
|
+
fileMatches.get(funcName).push({
|
|
728
|
+
line: call.line, content: lineContent.trim(),
|
|
729
|
+
matchType: 'test-case', functionName: funcName
|
|
730
|
+
});
|
|
731
|
+
existingLines.add(call.line);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
if (!fileEntry.symbols) return;
|
|
736
|
+
try {
|
|
737
|
+
const langModule = getLanguageModule(lang);
|
|
738
|
+
if (!langModule || !langModule.isEntryPoint) return;
|
|
739
|
+
for (const symbol of fileEntry.symbols) {
|
|
740
|
+
if (!langModule.isEntryPoint(symbol)) continue;
|
|
741
|
+
// Only add test-case if a call match exists in the test body
|
|
742
|
+
const hasCallInRange = existingMatches.some(m =>
|
|
743
|
+
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|
|
744
|
+
m.matchType === 'call'
|
|
745
|
+
);
|
|
746
|
+
if (hasCallInRange && !existingLines.has(symbol.startLine)) {
|
|
747
|
+
const lineContent = index.getLineContent(filePath, symbol.startLine);
|
|
748
|
+
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
749
|
+
fileMatches.get(funcName).push({
|
|
750
|
+
line: symbol.startLine, content: lineContent.trim(),
|
|
751
|
+
matchType: 'test-case', functionName: funcName
|
|
752
|
+
});
|
|
753
|
+
existingLines.add(symbol.startLine);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch (e) { /* skip */ }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Build instance type map for className scoping in affectedTests.
|
|
762
|
+
* Same logic as _buildInstanceTypeMap in search.js.
|
|
763
|
+
*/
|
|
764
|
+
function _buildInstanceTypeMapForTracing(index, filePath, content, targetClassName) {
|
|
765
|
+
const typeMap = new Map();
|
|
766
|
+
const calls = getCachedCalls(index, filePath);
|
|
767
|
+
if (calls) {
|
|
768
|
+
for (const call of calls) {
|
|
769
|
+
if (call.isMethod && call.receiver && call.receiverType === targetClassName) {
|
|
770
|
+
typeMap.set(call.receiver, targetClassName);
|
|
771
|
+
}
|
|
772
|
+
if (call.name === targetClassName && !call.isMethod) {
|
|
773
|
+
const lineContent = index.getLineContent(filePath, call.line);
|
|
774
|
+
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
775
|
+
if (assignMatch) typeMap.set(assignMatch[1], targetClassName);
|
|
776
|
+
}
|
|
777
|
+
if (call.isMethod && call.receiver === targetClassName) {
|
|
778
|
+
const lineContent = index.getLineContent(filePath, call.line);
|
|
779
|
+
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
780
|
+
if (assignMatch) typeMap.set(assignMatch[1], targetClassName);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const classUsages = index._getCachedUsages(filePath, targetClassName);
|
|
785
|
+
if (classUsages) {
|
|
786
|
+
for (const u of classUsages) {
|
|
787
|
+
if (u.usageType === 'import' || u.usageType === 'definition') continue;
|
|
788
|
+
const lineContent = index.getLineContent(filePath, u.line);
|
|
789
|
+
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
790
|
+
if (assignMatch && assignMatch[1] !== targetClassName) {
|
|
791
|
+
typeMap.set(assignMatch[1], targetClassName);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return typeMap;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Check if a usage's receiver matches the target className (for affectedTests).
|
|
800
|
+
* Same logic as _receiverMatchesClass in search.js.
|
|
801
|
+
*/
|
|
802
|
+
function _receiverMatchesClassTracing(usage, className, instanceTypeMap, lineContent, searchTerm) {
|
|
803
|
+
if (usage.receiver === className) return true;
|
|
804
|
+
if (usage.receiver && instanceTypeMap && instanceTypeMap.get(usage.receiver) === className) return true;
|
|
805
|
+
if (usage.receiver) return false;
|
|
806
|
+
if (lineContent && searchTerm) {
|
|
807
|
+
const pat = new RegExp(
|
|
808
|
+
'\\b' + escapeRegExp(className) + '\\s*(?:(?:\\([^)]*\\)|\\{[^}]*\\})\\s*\\.\\s*' +
|
|
809
|
+
escapeRegExp(searchTerm) + '\\s*\\(|' +
|
|
810
|
+
'new\\s+' + escapeRegExp(className) + '\\s*\\([^)]*\\)\\s*\\.\\s*' +
|
|
811
|
+
escapeRegExp(searchTerm) + '\\s*\\()'
|
|
812
|
+
);
|
|
813
|
+
if (pat.test(lineContent)) return true;
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Estimate the end line of a test block by tracking brace/paren nesting.
|
|
820
|
+
*/
|
|
821
|
+
function _estimateTestBlockEndTracing(index, filePath, startLine) {
|
|
822
|
+
const content = index._readFile(filePath);
|
|
823
|
+
if (!content) return startLine + 5;
|
|
824
|
+
const lines = content.split('\n');
|
|
825
|
+
let depth = 0;
|
|
826
|
+
let started = false;
|
|
827
|
+
for (let i = startLine - 1; i < lines.length; i++) {
|
|
828
|
+
const line = lines[i];
|
|
829
|
+
for (const ch of line) {
|
|
830
|
+
if (ch === '{' || ch === '(') { depth++; started = true; }
|
|
831
|
+
else if (ch === '}' || ch === ')') { depth--; }
|
|
832
|
+
}
|
|
833
|
+
if (started && depth <= 0) return i + 1;
|
|
834
|
+
}
|
|
835
|
+
return Math.min(startLine + 10, lines.length);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function _matchPriority(matchType) {
|
|
839
|
+
const p = { 'test-case': 5, 'call': 4, 'import': 3, 'string-ref': 2, 'reference': 1 };
|
|
840
|
+
return p[matchType] || 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
631
843
|
module.exports = { trace, blast, reverseTrace, affectedTests };
|
package/languages/javascript.js
CHANGED
|
@@ -2033,8 +2033,10 @@ function findUsagesInCode(code, name, parser) {
|
|
|
2033
2033
|
|
|
2034
2034
|
traverseTreeCached(tree.rootNode, (node) => {
|
|
2035
2035
|
// Look for identifier, property_identifier (method names in obj.method() calls),
|
|
2036
|
-
//
|
|
2037
|
-
|
|
2036
|
+
// type_identifier (TypeScript type annotations), and shorthand_property_identifier_pattern
|
|
2037
|
+
// (destructured names in `const { name } = require(...)`)
|
|
2038
|
+
const isIdentifier = node.type === 'identifier' || node.type === 'property_identifier' ||
|
|
2039
|
+
node.type === 'type_identifier' || node.type === 'shorthand_property_identifier_pattern';
|
|
2038
2040
|
if (!isIdentifier || node.text !== name) {
|
|
2039
2041
|
return true;
|
|
2040
2042
|
}
|
|
@@ -2101,6 +2103,21 @@ function findUsagesInCode(code, name, parser) {
|
|
|
2101
2103
|
}
|
|
2102
2104
|
}
|
|
2103
2105
|
}
|
|
2106
|
+
// Destructured require: const { name } = require('...')
|
|
2107
|
+
else if (node.type === 'shorthand_property_identifier_pattern' &&
|
|
2108
|
+
parent.type === 'object_pattern') {
|
|
2109
|
+
// Check if the object_pattern is part of a variable_declarator with require()
|
|
2110
|
+
const declarator = parent.parent;
|
|
2111
|
+
if (declarator && declarator.type === 'variable_declarator') {
|
|
2112
|
+
const value = declarator.childForFieldName('value');
|
|
2113
|
+
if (value && value.type === 'call_expression') {
|
|
2114
|
+
const func = value.childForFieldName('function');
|
|
2115
|
+
if (func && func.text === 'require') {
|
|
2116
|
+
usageType = 'import';
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2104
2121
|
// Property access (method call): a.name() - the name after dot
|
|
2105
2122
|
else if (parent.type === 'member_expression' &&
|
|
2106
2123
|
parent.childForFieldName('property') === node) {
|
package/languages/rust.js
CHANGED
|
@@ -1319,6 +1319,13 @@ function findExportsInCode(code, parser) {
|
|
|
1319
1319
|
* @param {object} parser - Tree-sitter parser instance
|
|
1320
1320
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
1321
1321
|
*/
|
|
1322
|
+
function _indexInParent(node, parent) {
|
|
1323
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
1324
|
+
if (parent.child(i) === node) return i;
|
|
1325
|
+
}
|
|
1326
|
+
return -1;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1322
1329
|
function findUsagesInCode(code, name, parser) {
|
|
1323
1330
|
const tree = parseTree(parser, code);
|
|
1324
1331
|
const usages = [];
|
|
@@ -1433,6 +1440,39 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1433
1440
|
return true;
|
|
1434
1441
|
}
|
|
1435
1442
|
}
|
|
1443
|
+
// Macro body: tree-sitter parses macro arguments as flat token_tree
|
|
1444
|
+
// nodes, so `svc.save()` inside `assert_eq!(svc.save(), 1)` appears
|
|
1445
|
+
// as sibling identifiers: [svc] [.] [save] [()] rather than a
|
|
1446
|
+
// field_expression. Detect the `obj.name(` pattern via siblings.
|
|
1447
|
+
else if (parent.type === 'token_tree') {
|
|
1448
|
+
const idx = _indexInParent(node, parent);
|
|
1449
|
+
// Method call pattern: [obj] [.] [name] [()] inside macro
|
|
1450
|
+
if (idx >= 2) {
|
|
1451
|
+
const dot = parent.child(idx - 1);
|
|
1452
|
+
const obj = parent.child(idx - 2);
|
|
1453
|
+
const next = parent.child(idx + 1);
|
|
1454
|
+
if (dot && dot.text === '.' && obj &&
|
|
1455
|
+
(obj.type === 'identifier' || obj.type === 'self')) {
|
|
1456
|
+
if (next && next.type === 'token_tree' &&
|
|
1457
|
+
next.childCount > 0 && next.child(0).text === '(') {
|
|
1458
|
+
usageType = 'call';
|
|
1459
|
+
}
|
|
1460
|
+
usages.push({ line, column, usageType, receiver: obj.text });
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
// Bare function call pattern: [name] [()] inside macro
|
|
1465
|
+
if (idx >= 0) {
|
|
1466
|
+
const next = parent.child(idx + 1);
|
|
1467
|
+
// Check no preceding dot (would be method call handled above)
|
|
1468
|
+
const prev = idx > 0 ? parent.child(idx - 1) : null;
|
|
1469
|
+
if ((!prev || prev.text !== '.') &&
|
|
1470
|
+
next && next.type === 'token_tree' &&
|
|
1471
|
+
next.childCount > 0 && next.child(0).text === '(') {
|
|
1472
|
+
usageType = 'call';
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1436
1476
|
}
|
|
1437
1477
|
|
|
1438
1478
|
// Filter out enum variant references: Boundary::Grid is NOT a usage of Grid struct
|