ucn 3.1.2 → 3.1.4
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/core/project.js +35 -16
- package/languages/javascript.js +14 -2
- package/languages/rust.js +3 -2
- package/package.json +1 -1
- package/test/parser.test.js +126 -0
package/core/project.js
CHANGED
|
@@ -1010,11 +1010,37 @@ class ProjectIndex {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
1012
|
// Look up each callee in the symbol table
|
|
1013
|
+
// For methods, prefer callees from: 1) same file, 2) same package, 3) same receiver type
|
|
1013
1014
|
const result = [];
|
|
1015
|
+
const defDir = path.dirname(def.file);
|
|
1016
|
+
const defReceiver = def.receiver;
|
|
1017
|
+
|
|
1014
1018
|
for (const [calleeName, count] of callees) {
|
|
1015
1019
|
const symbols = this.symbols.get(calleeName);
|
|
1016
1020
|
if (symbols && symbols.length > 0) {
|
|
1017
|
-
|
|
1021
|
+
let callee = symbols[0];
|
|
1022
|
+
|
|
1023
|
+
// If multiple definitions, try to find the best match
|
|
1024
|
+
if (symbols.length > 1) {
|
|
1025
|
+
// Priority 1: Same file
|
|
1026
|
+
const sameFile = symbols.find(s => s.file === def.file);
|
|
1027
|
+
if (sameFile) {
|
|
1028
|
+
callee = sameFile;
|
|
1029
|
+
} else {
|
|
1030
|
+
// Priority 2: Same directory (package)
|
|
1031
|
+
const sameDir = symbols.find(s => path.dirname(s.file) === defDir);
|
|
1032
|
+
if (sameDir) {
|
|
1033
|
+
callee = sameDir;
|
|
1034
|
+
} else if (defReceiver) {
|
|
1035
|
+
// Priority 3: Same receiver type (for methods)
|
|
1036
|
+
const sameReceiver = symbols.find(s => s.receiver === defReceiver);
|
|
1037
|
+
if (sameReceiver) {
|
|
1038
|
+
callee = sameReceiver;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1018
1044
|
result.push({
|
|
1019
1045
|
...callee,
|
|
1020
1046
|
callCount: count,
|
|
@@ -2137,23 +2163,15 @@ class ProjectIndex {
|
|
|
2137
2163
|
|
|
2138
2164
|
const def = definitions[0];
|
|
2139
2165
|
const visited = new Set();
|
|
2166
|
+
const defDir = path.dirname(def.file);
|
|
2140
2167
|
|
|
2141
|
-
const buildTree = (
|
|
2142
|
-
|
|
2168
|
+
const buildTree = (funcDef, currentDepth, dir) => {
|
|
2169
|
+
const funcName = funcDef.name;
|
|
2170
|
+
if (currentDepth > maxDepth || visited.has(`${funcDef.file}:${funcDef.startLine}`)) {
|
|
2143
2171
|
return null;
|
|
2144
2172
|
}
|
|
2145
|
-
visited.add(
|
|
2146
|
-
|
|
2147
|
-
const funcDefs = this.symbols.get(funcName);
|
|
2148
|
-
if (!funcDefs || funcDefs.length === 0) {
|
|
2149
|
-
return {
|
|
2150
|
-
name: funcName,
|
|
2151
|
-
external: true,
|
|
2152
|
-
children: []
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2173
|
+
visited.add(`${funcDef.file}:${funcDef.startLine}`);
|
|
2155
2174
|
|
|
2156
|
-
const funcDef = funcDefs[0];
|
|
2157
2175
|
const node = {
|
|
2158
2176
|
name: funcName,
|
|
2159
2177
|
file: funcDef.relativePath,
|
|
@@ -2165,7 +2183,8 @@ class ProjectIndex {
|
|
|
2165
2183
|
if (dir === 'down' || dir === 'both') {
|
|
2166
2184
|
const callees = this.findCallees(funcDef);
|
|
2167
2185
|
for (const callee of callees.slice(0, 10)) { // Limit children
|
|
2168
|
-
|
|
2186
|
+
// callee already has the best-matched definition from findCallees
|
|
2187
|
+
const childTree = buildTree(callee, currentDepth + 1, 'down');
|
|
2169
2188
|
if (childTree) {
|
|
2170
2189
|
node.children.push({
|
|
2171
2190
|
...childTree,
|
|
@@ -2179,7 +2198,7 @@ class ProjectIndex {
|
|
|
2179
2198
|
return node;
|
|
2180
2199
|
};
|
|
2181
2200
|
|
|
2182
|
-
const tree = buildTree(
|
|
2201
|
+
const tree = buildTree(def, 0, direction);
|
|
2183
2202
|
|
|
2184
2203
|
// Also get callers if direction is 'up' or 'both'
|
|
2185
2204
|
let callers = [];
|
package/languages/javascript.js
CHANGED
|
@@ -1248,8 +1248,9 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1248
1248
|
const usages = [];
|
|
1249
1249
|
|
|
1250
1250
|
traverseTree(tree.rootNode, (node) => {
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1251
|
+
// Look for both identifier and property_identifier (method names in obj.method() calls)
|
|
1252
|
+
const isIdentifier = node.type === 'identifier' || node.type === 'property_identifier';
|
|
1253
|
+
if (!isIdentifier || node.text !== name) {
|
|
1253
1254
|
return true;
|
|
1254
1255
|
}
|
|
1255
1256
|
|
|
@@ -1318,6 +1319,17 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1318
1319
|
// Property access (method call): a.name() - the name after dot
|
|
1319
1320
|
else if (parent.type === 'member_expression' &&
|
|
1320
1321
|
parent.childForFieldName('property') === node) {
|
|
1322
|
+
// Skip built-in objects and common module names (JSON.parse, path.parse, etc.)
|
|
1323
|
+
const object = parent.childForFieldName('object');
|
|
1324
|
+
const builtins = [
|
|
1325
|
+
// JS built-in objects
|
|
1326
|
+
'JSON', 'Math', 'console', 'Object', 'Array', 'String', 'Number', 'Date', 'RegExp', 'Promise', 'Reflect', 'Proxy', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Symbol', 'Intl', 'WebAssembly', 'Atomics', 'SharedArrayBuffer', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'URL', 'URLSearchParams',
|
|
1327
|
+
// Node.js core modules
|
|
1328
|
+
'path', 'fs', 'os', 'http', 'https', 'net', 'dgram', 'dns', 'tls', 'crypto', 'zlib', 'stream', 'util', 'events', 'buffer', 'child_process', 'cluster', 'readline', 'repl', 'vm', 'assert', 'querystring', 'url', 'punycode', 'string_decoder', 'timers', 'tty', 'v8', 'perf_hooks', 'worker_threads', 'inspector', 'trace_events', 'async_hooks', 'process'
|
|
1329
|
+
];
|
|
1330
|
+
if (object && object.type === 'identifier' && builtins.includes(object.text)) {
|
|
1331
|
+
return true; // Skip built-in method calls
|
|
1332
|
+
}
|
|
1321
1333
|
// Check if this is a method call
|
|
1322
1334
|
const grandparent = parent.parent;
|
|
1323
1335
|
if (grandparent && grandparent.type === 'call_expression') {
|
package/languages/rust.js
CHANGED
|
@@ -910,8 +910,9 @@ function findUsagesInCode(code, name, parser) {
|
|
|
910
910
|
const usages = [];
|
|
911
911
|
|
|
912
912
|
traverseTree(tree.rootNode, (node) => {
|
|
913
|
-
//
|
|
914
|
-
|
|
913
|
+
// Look for both identifier and field_identifier (method names in obj.method() calls)
|
|
914
|
+
const isIdentifier = node.type === 'identifier' || node.type === 'field_identifier';
|
|
915
|
+
if (!isIdentifier || node.text !== name) {
|
|
915
916
|
return true;
|
|
916
917
|
}
|
|
917
918
|
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -4459,6 +4459,132 @@ func (s *ServiceB) Process() error {
|
|
|
4459
4459
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4460
4460
|
}
|
|
4461
4461
|
});
|
|
4462
|
+
|
|
4463
|
+
it('should detect JavaScript method calls but filter built-ins', () => {
|
|
4464
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-js-method-'));
|
|
4465
|
+
try {
|
|
4466
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
4467
|
+
class Service {
|
|
4468
|
+
process() {}
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function main() {
|
|
4472
|
+
const svc = new Service();
|
|
4473
|
+
svc.process(); // user method - SHOULD be counted
|
|
4474
|
+
JSON.parse('{}'); // built-in - should NOT be counted
|
|
4475
|
+
process(); // direct call - SHOULD be counted
|
|
4476
|
+
}
|
|
4477
|
+
`);
|
|
4478
|
+
|
|
4479
|
+
const index = new ProjectIndex(tmpDir);
|
|
4480
|
+
index.build('**/*.js', { quiet: true });
|
|
4481
|
+
|
|
4482
|
+
const usages = index.usages('process', { codeOnly: true });
|
|
4483
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
4484
|
+
|
|
4485
|
+
// Should find 2 calls: svc.process() and process()
|
|
4486
|
+
assert.strictEqual(calls.length, 2, 'Should find 2 calls (user method + direct)');
|
|
4487
|
+
|
|
4488
|
+
// Should NOT include JSON.parse
|
|
4489
|
+
const hasJsonParse = calls.some(c => c.content && c.content.includes('JSON.parse'));
|
|
4490
|
+
assert.strictEqual(hasJsonParse, false, 'JSON.parse should NOT be counted');
|
|
4491
|
+
|
|
4492
|
+
// Should include svc.process()
|
|
4493
|
+
const hasUserMethod = calls.some(c => c.content && c.content.includes('svc.process'));
|
|
4494
|
+
assert.strictEqual(hasUserMethod, true, 'svc.process() SHOULD be counted');
|
|
4495
|
+
} finally {
|
|
4496
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4497
|
+
}
|
|
4498
|
+
});
|
|
4499
|
+
|
|
4500
|
+
it('should prefer same-file callees for Go methods', () => {
|
|
4501
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-callee-disambig-'));
|
|
4502
|
+
try {
|
|
4503
|
+
fs.writeFileSync(path.join(tmpDir, 'go.mod'), `module example.com/test
|
|
4504
|
+
go 1.21
|
|
4505
|
+
`);
|
|
4506
|
+
|
|
4507
|
+
// Two files with same method name 'helper'
|
|
4508
|
+
fs.writeFileSync(path.join(tmpDir, 'service_a.go'), `package main
|
|
4509
|
+
|
|
4510
|
+
type ServiceA struct{}
|
|
4511
|
+
|
|
4512
|
+
func (s *ServiceA) Process() {
|
|
4513
|
+
s.helper()
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
func (s *ServiceA) helper() {}
|
|
4517
|
+
`);
|
|
4518
|
+
|
|
4519
|
+
fs.writeFileSync(path.join(tmpDir, 'service_b.go'), `package main
|
|
4520
|
+
|
|
4521
|
+
type ServiceB struct{}
|
|
4522
|
+
|
|
4523
|
+
func (s *ServiceB) Process() {
|
|
4524
|
+
s.helper()
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
func (s *ServiceB) helper() {}
|
|
4528
|
+
`);
|
|
4529
|
+
|
|
4530
|
+
const index = new ProjectIndex(tmpDir);
|
|
4531
|
+
index.build('**/*.go', { quiet: true });
|
|
4532
|
+
|
|
4533
|
+
// Get callees for ServiceA.Process - should find ServiceA.helper, not ServiceB.helper
|
|
4534
|
+
const defs = index.symbols.get('Process') || [];
|
|
4535
|
+
const serviceAProcess = defs.find(d => d.relativePath.includes('service_a.go'));
|
|
4536
|
+
assert.ok(serviceAProcess, 'Should find ServiceA.Process');
|
|
4537
|
+
|
|
4538
|
+
const callees = index.findCallees(serviceAProcess);
|
|
4539
|
+
assert.ok(callees.length >= 1, 'Should find at least 1 callee');
|
|
4540
|
+
|
|
4541
|
+
const helperCallee = callees.find(c => c.name === 'helper');
|
|
4542
|
+
assert.ok(helperCallee, 'Should find helper callee');
|
|
4543
|
+
assert.ok(helperCallee.relativePath.includes('service_a.go'),
|
|
4544
|
+
'helper callee should be from service_a.go, not service_b.go');
|
|
4545
|
+
} finally {
|
|
4546
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
|
|
4550
|
+
it('should detect Rust method calls in usages', () => {
|
|
4551
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-rust-method-'));
|
|
4552
|
+
try {
|
|
4553
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), `[package]
|
|
4554
|
+
name = "test"
|
|
4555
|
+
version = "0.1.0"
|
|
4556
|
+
`);
|
|
4557
|
+
|
|
4558
|
+
fs.writeFileSync(path.join(tmpDir, 'main.rs'), `
|
|
4559
|
+
struct Client {}
|
|
4560
|
+
|
|
4561
|
+
impl Client {
|
|
4562
|
+
fn process(&self) {}
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
fn main() {
|
|
4566
|
+
let c = Client{};
|
|
4567
|
+
c.process();
|
|
4568
|
+
process();
|
|
4569
|
+
}
|
|
4570
|
+
`);
|
|
4571
|
+
|
|
4572
|
+
const index = new ProjectIndex(tmpDir);
|
|
4573
|
+
index.build('**/*.rs', { quiet: true });
|
|
4574
|
+
|
|
4575
|
+
const usages = index.usages('process', { codeOnly: true });
|
|
4576
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
4577
|
+
|
|
4578
|
+
// Should find 2 calls: c.process() and process()
|
|
4579
|
+
assert.ok(calls.length >= 2, 'Should find at least 2 calls');
|
|
4580
|
+
|
|
4581
|
+
// Should include c.process()
|
|
4582
|
+
const hasMethodCall = calls.some(c => c.content && c.content.includes('c.process'));
|
|
4583
|
+
assert.strictEqual(hasMethodCall, true, 'c.process() SHOULD be counted');
|
|
4584
|
+
} finally {
|
|
4585
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4462
4588
|
});
|
|
4463
4589
|
|
|
4464
4590
|
console.log('UCN v3 Test Suite');
|