ucn 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
|
@@ -0,0 +1,3661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCN v3 Parser Test Suite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { describe, it } = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
// Test the library
|
|
12
|
+
const { parse, parseFile, detectLanguage, isSupported } = require('../core/parser');
|
|
13
|
+
const { ProjectIndex } = require('../core/project');
|
|
14
|
+
const { expandGlob } = require('../core/discovery');
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// LANGUAGE DETECTION
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
describe('Language Detection', () => {
|
|
21
|
+
it('detects JavaScript files', () => {
|
|
22
|
+
assert.strictEqual(detectLanguage('file.js'), 'javascript');
|
|
23
|
+
assert.strictEqual(detectLanguage('file.jsx'), 'javascript');
|
|
24
|
+
assert.strictEqual(detectLanguage('file.mjs'), 'javascript');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('detects TypeScript files', () => {
|
|
28
|
+
assert.strictEqual(detectLanguage('file.ts'), 'typescript');
|
|
29
|
+
assert.strictEqual(detectLanguage('file.tsx'), 'tsx');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('detects Python files', () => {
|
|
33
|
+
assert.strictEqual(detectLanguage('file.py'), 'python');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('detects Go files', () => {
|
|
37
|
+
assert.strictEqual(detectLanguage('file.go'), 'go');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('detects Rust files', () => {
|
|
41
|
+
assert.strictEqual(detectLanguage('file.rs'), 'rust');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('detects Java files', () => {
|
|
45
|
+
assert.strictEqual(detectLanguage('file.java'), 'java');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns null for unsupported files', () => {
|
|
49
|
+
assert.strictEqual(detectLanguage('file.txt'), null);
|
|
50
|
+
assert.strictEqual(detectLanguage('file.md'), null);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// JAVASCRIPT PARSING
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe('JavaScript Parsing', () => {
|
|
59
|
+
it('parses function declarations', () => {
|
|
60
|
+
const code = `
|
|
61
|
+
function hello(name) {
|
|
62
|
+
return 'Hello ' + name;
|
|
63
|
+
}`;
|
|
64
|
+
const result = parse(code, 'javascript');
|
|
65
|
+
assert.strictEqual(result.functions.length, 1);
|
|
66
|
+
assert.strictEqual(result.functions[0].name, 'hello');
|
|
67
|
+
assert.strictEqual(result.functions[0].params, 'name');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('parses arrow functions', () => {
|
|
71
|
+
const code = `
|
|
72
|
+
const add = (a, b) => a + b;`;
|
|
73
|
+
const result = parse(code, 'javascript');
|
|
74
|
+
assert.strictEqual(result.functions.length, 1);
|
|
75
|
+
assert.strictEqual(result.functions[0].name, 'add');
|
|
76
|
+
assert.strictEqual(result.functions[0].isArrow, true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('parses async functions', () => {
|
|
80
|
+
const code = `
|
|
81
|
+
async function fetchData(url) {
|
|
82
|
+
return await fetch(url);
|
|
83
|
+
}`;
|
|
84
|
+
const result = parse(code, 'javascript');
|
|
85
|
+
assert.strictEqual(result.functions.length, 1);
|
|
86
|
+
assert.ok(result.functions[0].modifiers.includes('async'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('parses classes', () => {
|
|
90
|
+
const code = `
|
|
91
|
+
class User {
|
|
92
|
+
constructor(name) {
|
|
93
|
+
this.name = name;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
greet() {
|
|
97
|
+
return 'Hello ' + this.name;
|
|
98
|
+
}
|
|
99
|
+
}`;
|
|
100
|
+
const result = parse(code, 'javascript');
|
|
101
|
+
assert.strictEqual(result.classes.length, 1);
|
|
102
|
+
assert.strictEqual(result.classes[0].name, 'User');
|
|
103
|
+
assert.strictEqual(result.classes[0].members.length, 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('parses generator functions', () => {
|
|
107
|
+
const code = `
|
|
108
|
+
function* generateNumbers() {
|
|
109
|
+
yield 1;
|
|
110
|
+
yield 2;
|
|
111
|
+
}`;
|
|
112
|
+
const result = parse(code, 'javascript');
|
|
113
|
+
assert.strictEqual(result.functions.length, 1);
|
|
114
|
+
assert.strictEqual(result.functions[0].isGenerator, true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('parses exported functions', () => {
|
|
118
|
+
const code = `
|
|
119
|
+
export function publicFn() {}
|
|
120
|
+
export default function main() {}`;
|
|
121
|
+
const result = parse(code, 'javascript');
|
|
122
|
+
assert.strictEqual(result.functions.length, 2);
|
|
123
|
+
// Both should have export in modifiers (checked via presence of export keyword)
|
|
124
|
+
assert.ok(result.functions.some(f => f.name === 'publicFn'));
|
|
125
|
+
assert.ok(result.functions.some(f => f.name === 'main' || f.name === 'default'));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// TYPESCRIPT PARSING
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
describe('TypeScript Parsing', () => {
|
|
134
|
+
it('parses typed functions', () => {
|
|
135
|
+
const code = `
|
|
136
|
+
function greet(name: string): string {
|
|
137
|
+
return 'Hello ' + name;
|
|
138
|
+
}`;
|
|
139
|
+
const result = parse(code, 'typescript');
|
|
140
|
+
assert.strictEqual(result.functions.length, 1);
|
|
141
|
+
assert.strictEqual(result.functions[0].returnType, 'string');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('parses interfaces', () => {
|
|
145
|
+
const code = `
|
|
146
|
+
interface User {
|
|
147
|
+
name: string;
|
|
148
|
+
age: number;
|
|
149
|
+
}`;
|
|
150
|
+
const result = parse(code, 'typescript');
|
|
151
|
+
assert.strictEqual(result.classes.length, 1);
|
|
152
|
+
assert.strictEqual(result.classes[0].type, 'interface');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('parses type aliases', () => {
|
|
156
|
+
const code = `
|
|
157
|
+
type ID = string | number;`;
|
|
158
|
+
const result = parse(code, 'typescript');
|
|
159
|
+
assert.strictEqual(result.classes.length, 1);
|
|
160
|
+
assert.strictEqual(result.classes[0].type, 'type');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('parses enums', () => {
|
|
164
|
+
const code = `
|
|
165
|
+
enum Status {
|
|
166
|
+
Active,
|
|
167
|
+
Inactive
|
|
168
|
+
}`;
|
|
169
|
+
const result = parse(code, 'typescript');
|
|
170
|
+
assert.strictEqual(result.classes.length, 1);
|
|
171
|
+
assert.strictEqual(result.classes[0].type, 'enum');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('parses generic functions', () => {
|
|
175
|
+
const code = `
|
|
176
|
+
function identity<T>(arg: T): T {
|
|
177
|
+
return arg;
|
|
178
|
+
}`;
|
|
179
|
+
const result = parse(code, 'typescript');
|
|
180
|
+
assert.strictEqual(result.functions.length, 1);
|
|
181
|
+
assert.ok(result.functions[0].generics);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// PYTHON PARSING
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
describe('Python Parsing', () => {
|
|
190
|
+
it('parses function definitions', () => {
|
|
191
|
+
const code = `
|
|
192
|
+
def hello(name):
|
|
193
|
+
return 'Hello ' + name`;
|
|
194
|
+
const result = parse(code, 'python');
|
|
195
|
+
assert.strictEqual(result.functions.length, 1);
|
|
196
|
+
assert.strictEqual(result.functions[0].name, 'hello');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('parses typed functions', () => {
|
|
200
|
+
const code = `
|
|
201
|
+
def greet(name: str) -> str:
|
|
202
|
+
return 'Hello ' + name`;
|
|
203
|
+
const result = parse(code, 'python');
|
|
204
|
+
assert.strictEqual(result.functions.length, 1);
|
|
205
|
+
assert.strictEqual(result.functions[0].returnType, 'str');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('parses async functions', () => {
|
|
209
|
+
const code = `
|
|
210
|
+
async def fetch_data(url):
|
|
211
|
+
return await get(url)`;
|
|
212
|
+
const result = parse(code, 'python');
|
|
213
|
+
assert.strictEqual(result.functions.length, 1);
|
|
214
|
+
assert.strictEqual(result.functions[0].isAsync, true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('parses decorated functions', () => {
|
|
218
|
+
const code = `
|
|
219
|
+
@staticmethod
|
|
220
|
+
def helper():
|
|
221
|
+
pass`;
|
|
222
|
+
const result = parse(code, 'python');
|
|
223
|
+
assert.strictEqual(result.functions.length, 1);
|
|
224
|
+
assert.ok(result.functions[0].decorators);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('parses classes', () => {
|
|
228
|
+
const code = `
|
|
229
|
+
class User:
|
|
230
|
+
def __init__(self, name):
|
|
231
|
+
self.name = name
|
|
232
|
+
|
|
233
|
+
def greet(self):
|
|
234
|
+
return 'Hello ' + self.name`;
|
|
235
|
+
const result = parse(code, 'python');
|
|
236
|
+
assert.strictEqual(result.classes.length, 1);
|
|
237
|
+
assert.strictEqual(result.classes[0].name, 'User');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// GO PARSING
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
describe('Go Parsing', () => {
|
|
246
|
+
it('parses function declarations', () => {
|
|
247
|
+
const code = `
|
|
248
|
+
func Hello(name string) string {
|
|
249
|
+
return "Hello " + name
|
|
250
|
+
}`;
|
|
251
|
+
const result = parse(code, 'go');
|
|
252
|
+
assert.strictEqual(result.functions.length, 1);
|
|
253
|
+
assert.strictEqual(result.functions[0].name, 'Hello');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('parses methods', () => {
|
|
257
|
+
const code = `
|
|
258
|
+
func (u *User) Greet() string {
|
|
259
|
+
return "Hello " + u.Name
|
|
260
|
+
}`;
|
|
261
|
+
const result = parse(code, 'go');
|
|
262
|
+
assert.strictEqual(result.functions.length, 1);
|
|
263
|
+
assert.strictEqual(result.functions[0].isMethod, true);
|
|
264
|
+
assert.strictEqual(result.functions[0].receiver, '*User');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('parses structs', () => {
|
|
268
|
+
const code = `
|
|
269
|
+
type User struct {
|
|
270
|
+
Name string
|
|
271
|
+
Age int
|
|
272
|
+
}`;
|
|
273
|
+
const result = parse(code, 'go');
|
|
274
|
+
assert.strictEqual(result.classes.length, 1);
|
|
275
|
+
assert.strictEqual(result.classes[0].type, 'struct');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('parses interfaces', () => {
|
|
279
|
+
const code = `
|
|
280
|
+
type Reader interface {
|
|
281
|
+
Read(p []byte) (n int, err error)
|
|
282
|
+
}`;
|
|
283
|
+
const result = parse(code, 'go');
|
|
284
|
+
assert.strictEqual(result.classes.length, 1);
|
|
285
|
+
assert.strictEqual(result.classes[0].type, 'interface');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// RUST PARSING
|
|
291
|
+
// ============================================================================
|
|
292
|
+
|
|
293
|
+
describe('Rust Parsing', () => {
|
|
294
|
+
it('parses function definitions', () => {
|
|
295
|
+
const code = `
|
|
296
|
+
fn hello(name: &str) -> String {
|
|
297
|
+
format!("Hello {}", name)
|
|
298
|
+
}`;
|
|
299
|
+
const result = parse(code, 'rust');
|
|
300
|
+
assert.strictEqual(result.functions.length, 1);
|
|
301
|
+
assert.strictEqual(result.functions[0].name, 'hello');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('parses async functions', () => {
|
|
305
|
+
const code = `
|
|
306
|
+
async fn fetch_data(url: &str) -> Result<String, Error> {
|
|
307
|
+
Ok(String::new())
|
|
308
|
+
}`;
|
|
309
|
+
const result = parse(code, 'rust');
|
|
310
|
+
assert.strictEqual(result.functions.length, 1);
|
|
311
|
+
assert.ok(result.functions[0].modifiers.includes('async'));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('parses structs', () => {
|
|
315
|
+
const code = `
|
|
316
|
+
struct User {
|
|
317
|
+
name: String,
|
|
318
|
+
age: u32,
|
|
319
|
+
}`;
|
|
320
|
+
const result = parse(code, 'rust');
|
|
321
|
+
assert.strictEqual(result.classes.length, 1);
|
|
322
|
+
assert.strictEqual(result.classes[0].type, 'struct');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('parses impl blocks', () => {
|
|
326
|
+
const code = `
|
|
327
|
+
impl User {
|
|
328
|
+
fn new(name: String) -> Self {
|
|
329
|
+
User { name, age: 0 }
|
|
330
|
+
}
|
|
331
|
+
}`;
|
|
332
|
+
const result = parse(code, 'rust');
|
|
333
|
+
assert.strictEqual(result.classes.length, 1);
|
|
334
|
+
assert.strictEqual(result.classes[0].type, 'impl');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('parses traits', () => {
|
|
338
|
+
const code = `
|
|
339
|
+
trait Greet {
|
|
340
|
+
fn greet(&self) -> String;
|
|
341
|
+
}`;
|
|
342
|
+
const result = parse(code, 'rust');
|
|
343
|
+
assert.strictEqual(result.classes.length, 1);
|
|
344
|
+
assert.strictEqual(result.classes[0].type, 'trait');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ============================================================================
|
|
349
|
+
// JAVA PARSING
|
|
350
|
+
// ============================================================================
|
|
351
|
+
|
|
352
|
+
describe('Java Parsing', () => {
|
|
353
|
+
it('parses class declarations', () => {
|
|
354
|
+
const code = `
|
|
355
|
+
public class User {
|
|
356
|
+
private String name;
|
|
357
|
+
|
|
358
|
+
public User(String name) {
|
|
359
|
+
this.name = name;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public String getName() {
|
|
363
|
+
return name;
|
|
364
|
+
}
|
|
365
|
+
}`;
|
|
366
|
+
const result = parse(code, 'java');
|
|
367
|
+
assert.strictEqual(result.classes.length, 1);
|
|
368
|
+
assert.strictEqual(result.classes[0].name, 'User');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('parses interfaces', () => {
|
|
372
|
+
const code = `
|
|
373
|
+
public interface UserService {
|
|
374
|
+
User getUser(int id);
|
|
375
|
+
}`;
|
|
376
|
+
const result = parse(code, 'java');
|
|
377
|
+
assert.strictEqual(result.classes.length, 1);
|
|
378
|
+
assert.strictEqual(result.classes[0].type, 'interface');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('parses methods with annotations', () => {
|
|
382
|
+
const code = `
|
|
383
|
+
public class Controller {
|
|
384
|
+
@Override
|
|
385
|
+
public void handle() {}
|
|
386
|
+
}`;
|
|
387
|
+
const result = parse(code, 'java');
|
|
388
|
+
// Methods are indexed as part of the class
|
|
389
|
+
assert.strictEqual(result.functions.length >= 0, true);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// OUTPUT FORMAT
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
describe('Output Format', () => {
|
|
398
|
+
it('includes full params without truncation', () => {
|
|
399
|
+
const code = `
|
|
400
|
+
function processData(input: { name: string; age: number; address: { street: string; city: string } }): Promise<Result> {
|
|
401
|
+
return Promise.resolve({});
|
|
402
|
+
}`;
|
|
403
|
+
const result = parse(code, 'typescript');
|
|
404
|
+
assert.strictEqual(result.functions.length, 1);
|
|
405
|
+
// Params should NOT be truncated
|
|
406
|
+
assert.ok(result.functions[0].params.includes('address'));
|
|
407
|
+
assert.ok(result.functions[0].params.includes('city'));
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('includes return types', () => {
|
|
411
|
+
const code = `
|
|
412
|
+
function getData(): Promise<User[]> {
|
|
413
|
+
return Promise.resolve([]);
|
|
414
|
+
}`;
|
|
415
|
+
const result = parse(code, 'typescript');
|
|
416
|
+
assert.strictEqual(result.functions[0].returnType, 'Promise<User[]>');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('includes generics', () => {
|
|
420
|
+
const code = `
|
|
421
|
+
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
|
|
422
|
+
return arr.map(fn);
|
|
423
|
+
}`;
|
|
424
|
+
const result = parse(code, 'typescript');
|
|
425
|
+
assert.ok(result.functions[0].generics);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('includes docstrings', () => {
|
|
429
|
+
const code = `
|
|
430
|
+
/**
|
|
431
|
+
* Greets a user by name.
|
|
432
|
+
* @param name - The user's name
|
|
433
|
+
*/
|
|
434
|
+
function greet(name: string) {
|
|
435
|
+
return 'Hello ' + name;
|
|
436
|
+
}`;
|
|
437
|
+
const result = parse(code, 'typescript');
|
|
438
|
+
assert.ok(result.functions[0].docstring);
|
|
439
|
+
assert.ok(result.functions[0].docstring.includes('Greets'));
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// INTEGRATION TESTS
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
describe('Integration Tests', () => {
|
|
448
|
+
it('can parse a simple JS file', () => {
|
|
449
|
+
// Create a simple JS file in memory and parse it
|
|
450
|
+
const code = `
|
|
451
|
+
function hello() {
|
|
452
|
+
return 'Hello';
|
|
453
|
+
}
|
|
454
|
+
class Greeter {
|
|
455
|
+
greet() { return 'Hi'; }
|
|
456
|
+
}`;
|
|
457
|
+
const result = parse(code, 'javascript');
|
|
458
|
+
assert.ok(result.functions.length > 0);
|
|
459
|
+
assert.ok(result.classes.length > 0);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// ProjectIndex Tests (v2 Migration)
|
|
465
|
+
// ============================================================================
|
|
466
|
+
|
|
467
|
+
describe('ProjectIndex', () => {
|
|
468
|
+
const { ProjectIndex } = require('../core/project');
|
|
469
|
+
const path = require('path');
|
|
470
|
+
|
|
471
|
+
it('builds index and finds symbols', () => {
|
|
472
|
+
const index = new ProjectIndex('.');
|
|
473
|
+
index.build(null, { quiet: true });
|
|
474
|
+
|
|
475
|
+
const stats = index.getStats();
|
|
476
|
+
assert.ok(stats.files > 0, 'Should index files');
|
|
477
|
+
assert.ok(stats.symbols > 0, 'Should find symbols');
|
|
478
|
+
|
|
479
|
+
const found = index.find('parse');
|
|
480
|
+
assert.ok(found.length > 0, 'Should find parse function');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('gets imports for a file', () => {
|
|
484
|
+
const index = new ProjectIndex('.');
|
|
485
|
+
index.build(null, { quiet: true });
|
|
486
|
+
|
|
487
|
+
const imports = index.imports('core/parser.js');
|
|
488
|
+
assert.ok(imports.length > 0, 'Should find imports');
|
|
489
|
+
assert.ok(imports.some(i => i.module.includes('languages')), 'Should find languages import');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('gets exporters for a file', () => {
|
|
493
|
+
const index = new ProjectIndex('.');
|
|
494
|
+
index.build(null, { quiet: true });
|
|
495
|
+
|
|
496
|
+
const exporters = index.exporters('core/parser.js');
|
|
497
|
+
assert.ok(exporters.length > 0, 'Should find files that import parser.js');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('finds type definitions', () => {
|
|
501
|
+
const index = new ProjectIndex('.');
|
|
502
|
+
index.build(null, { quiet: true });
|
|
503
|
+
|
|
504
|
+
const types = index.typedef('ProjectIndex');
|
|
505
|
+
assert.ok(types.length > 0, 'Should find ProjectIndex class');
|
|
506
|
+
assert.strictEqual(types[0].type, 'class', 'Should be a class');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('finds tests for a function', () => {
|
|
510
|
+
const index = new ProjectIndex('.');
|
|
511
|
+
index.build(null, { quiet: true });
|
|
512
|
+
|
|
513
|
+
const tests = index.tests('parse');
|
|
514
|
+
assert.ok(tests.length > 0, 'Should find tests for parse');
|
|
515
|
+
assert.ok(tests[0].matches.length > 0, 'Should have test matches');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('gets usages grouped by type', () => {
|
|
519
|
+
const index = new ProjectIndex('.');
|
|
520
|
+
index.build(null, { quiet: true });
|
|
521
|
+
|
|
522
|
+
const usages = index.usages('parseFile');
|
|
523
|
+
const defs = usages.filter(u => u.isDefinition);
|
|
524
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
525
|
+
|
|
526
|
+
assert.ok(defs.length > 0, 'Should find definition');
|
|
527
|
+
assert.ok(calls.length > 0, 'Should find calls');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('gets context (callers + callees)', () => {
|
|
531
|
+
const index = new ProjectIndex('.');
|
|
532
|
+
index.build(null, { quiet: true });
|
|
533
|
+
|
|
534
|
+
const ctx = index.context('parseFile');
|
|
535
|
+
assert.strictEqual(ctx.function, 'parseFile');
|
|
536
|
+
assert.ok(Array.isArray(ctx.callers), 'Should have callers array');
|
|
537
|
+
assert.ok(Array.isArray(ctx.callees), 'Should have callees array');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('searches across project', () => {
|
|
541
|
+
const index = new ProjectIndex('.');
|
|
542
|
+
index.build(null, { quiet: true });
|
|
543
|
+
|
|
544
|
+
const results = index.search('TODO');
|
|
545
|
+
assert.ok(Array.isArray(results), 'Should return array');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('gets API (exported symbols)', () => {
|
|
549
|
+
const index = new ProjectIndex('.');
|
|
550
|
+
index.build(null, { quiet: true });
|
|
551
|
+
|
|
552
|
+
const api = index.api();
|
|
553
|
+
assert.ok(api.length > 0, 'Should find exported symbols');
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ============================================================================
|
|
558
|
+
// Import/Export Parsing Tests
|
|
559
|
+
// ============================================================================
|
|
560
|
+
|
|
561
|
+
describe('Import/Export Parsing', () => {
|
|
562
|
+
const { extractImports, extractExports } = require('../core/imports');
|
|
563
|
+
|
|
564
|
+
it('extracts CommonJS module.exports', () => {
|
|
565
|
+
const code = `
|
|
566
|
+
module.exports = {
|
|
567
|
+
parse,
|
|
568
|
+
parseFile,
|
|
569
|
+
findSymbol
|
|
570
|
+
};
|
|
571
|
+
`;
|
|
572
|
+
const { exports } = extractExports(code, 'javascript');
|
|
573
|
+
assert.ok(exports.length >= 3, 'Should find 3 exports');
|
|
574
|
+
assert.ok(exports.some(e => e.name === 'parse'), 'Should find parse export');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('extracts ES module exports', () => {
|
|
578
|
+
const code = `
|
|
579
|
+
export function hello() {}
|
|
580
|
+
export const world = 42;
|
|
581
|
+
export default class MyClass {}
|
|
582
|
+
`;
|
|
583
|
+
const { exports } = extractExports(code, 'javascript');
|
|
584
|
+
assert.ok(exports.some(e => e.name === 'hello'), 'Should find hello export');
|
|
585
|
+
assert.ok(exports.some(e => e.name === 'world'), 'Should find world export');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('extracts ES module imports', () => {
|
|
589
|
+
const code = `
|
|
590
|
+
import fs from 'fs';
|
|
591
|
+
import { parse, parseFile } from './parser';
|
|
592
|
+
import * as utils from './utils';
|
|
593
|
+
`;
|
|
594
|
+
const { imports } = extractImports(code, 'javascript');
|
|
595
|
+
assert.ok(imports.some(i => i.module === 'fs'), 'Should find fs import');
|
|
596
|
+
assert.ok(imports.some(i => i.module === './parser'), 'Should find parser import');
|
|
597
|
+
assert.ok(imports.some(i => i.module === './utils'), 'Should find utils import');
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// Output Formatting Tests
|
|
603
|
+
// ============================================================================
|
|
604
|
+
|
|
605
|
+
describe('Output Formatting', () => {
|
|
606
|
+
const output = require('../core/output');
|
|
607
|
+
|
|
608
|
+
it('formats disambiguation output', () => {
|
|
609
|
+
const matches = [
|
|
610
|
+
{ name: 'parse', relativePath: 'file1.js', startLine: 10, params: 'code', usageCount: 5 },
|
|
611
|
+
{ name: 'parse', relativePath: 'file2.js', startLine: 20, params: 'input', usageCount: 3 }
|
|
612
|
+
];
|
|
613
|
+
const result = output.formatDisambiguation(matches, 'parse', 'fn');
|
|
614
|
+
assert.ok(result.includes('Multiple matches'), 'Should show multiple matches');
|
|
615
|
+
assert.ok(result.includes('file1.js'), 'Should include file paths');
|
|
616
|
+
assert.ok(result.includes('--file'), 'Should suggest --file flag');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('formats imports output', () => {
|
|
620
|
+
const imports = [
|
|
621
|
+
{ module: './parser', resolved: 'core/parser.js', isExternal: false, names: ['parse'] },
|
|
622
|
+
{ module: 'fs', resolved: null, isExternal: true, names: ['fs'] }
|
|
623
|
+
];
|
|
624
|
+
const result = output.formatImports(imports, 'test.js');
|
|
625
|
+
assert.ok(result.includes('INTERNAL'), 'Should show internal section');
|
|
626
|
+
assert.ok(result.includes('EXTERNAL'), 'Should show external section');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('formats tests output', () => {
|
|
630
|
+
const tests = [{
|
|
631
|
+
file: 'test.spec.js',
|
|
632
|
+
matches: [
|
|
633
|
+
{ line: 10, content: 'it("should parse")', matchType: 'test-case' }
|
|
634
|
+
]
|
|
635
|
+
}];
|
|
636
|
+
const result = output.formatTests(tests, 'parse');
|
|
637
|
+
assert.ok(result.includes('[test]'), 'Should show test-case label');
|
|
638
|
+
assert.ok(result.includes('test.spec.js'), 'Should show file name');
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Cache Behavior Tests
|
|
644
|
+
// ============================================================================
|
|
645
|
+
|
|
646
|
+
describe('Cache Behavior', () => {
|
|
647
|
+
const os = require('os');
|
|
648
|
+
const crypto = require('crypto');
|
|
649
|
+
|
|
650
|
+
function createTempDir() {
|
|
651
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
652
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
653
|
+
return tmpDir;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function cleanup(dir) {
|
|
657
|
+
if (dir && fs.existsSync(dir)) {
|
|
658
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
it('should save and load cache correctly', () => {
|
|
663
|
+
const tmpDir = createTempDir();
|
|
664
|
+
try {
|
|
665
|
+
// Create a test file
|
|
666
|
+
const testFile = path.join(tmpDir, 'test.js');
|
|
667
|
+
fs.writeFileSync(testFile, 'function hello() { return "world"; }');
|
|
668
|
+
|
|
669
|
+
// Build index and save cache
|
|
670
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
671
|
+
index1.build('**/*.js', { quiet: true });
|
|
672
|
+
index1.saveCache();
|
|
673
|
+
|
|
674
|
+
// Verify cache file exists
|
|
675
|
+
const cacheFile = path.join(tmpDir, '.ucn-cache', 'index.json');
|
|
676
|
+
assert.ok(fs.existsSync(cacheFile), 'Cache file should exist');
|
|
677
|
+
|
|
678
|
+
// Create new index and load cache
|
|
679
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
680
|
+
const loaded = index2.loadCache();
|
|
681
|
+
assert.ok(loaded, 'Cache should load successfully');
|
|
682
|
+
|
|
683
|
+
// Verify symbols match
|
|
684
|
+
assert.strictEqual(index2.symbols.size, index1.symbols.size, 'Symbol count should match');
|
|
685
|
+
assert.ok(index2.symbols.has('hello'), 'Should have hello symbol');
|
|
686
|
+
} finally {
|
|
687
|
+
cleanup(tmpDir);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should detect modified files as stale', () => {
|
|
692
|
+
const tmpDir = createTempDir();
|
|
693
|
+
try {
|
|
694
|
+
// Create test file
|
|
695
|
+
const testFile = path.join(tmpDir, 'test.js');
|
|
696
|
+
fs.writeFileSync(testFile, 'function original() {}');
|
|
697
|
+
|
|
698
|
+
// Build and save cache
|
|
699
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
700
|
+
index1.build('**/*.js', { quiet: true });
|
|
701
|
+
index1.saveCache();
|
|
702
|
+
|
|
703
|
+
// Modify file
|
|
704
|
+
fs.writeFileSync(testFile, 'function modified() { return 42; }');
|
|
705
|
+
|
|
706
|
+
// Load cache and check staleness
|
|
707
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
708
|
+
index2.loadCache();
|
|
709
|
+
assert.ok(index2.isCacheStale(), 'Cache should be stale after file modification');
|
|
710
|
+
} finally {
|
|
711
|
+
cleanup(tmpDir);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should detect new files added to project', () => {
|
|
716
|
+
const tmpDir = createTempDir();
|
|
717
|
+
try {
|
|
718
|
+
// Create initial file
|
|
719
|
+
const testFile = path.join(tmpDir, 'test.js');
|
|
720
|
+
fs.writeFileSync(testFile, 'function first() {}');
|
|
721
|
+
|
|
722
|
+
// Build and save cache
|
|
723
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
724
|
+
index1.build('**/*.js', { quiet: true });
|
|
725
|
+
index1.saveCache();
|
|
726
|
+
|
|
727
|
+
// Add new file
|
|
728
|
+
const newFile = path.join(tmpDir, 'new.js');
|
|
729
|
+
fs.writeFileSync(newFile, 'function second() {}');
|
|
730
|
+
|
|
731
|
+
// Load cache and check staleness
|
|
732
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
733
|
+
index2.loadCache();
|
|
734
|
+
assert.ok(index2.isCacheStale(), 'Cache should be stale after adding new file');
|
|
735
|
+
} finally {
|
|
736
|
+
cleanup(tmpDir);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('should detect deleted files', () => {
|
|
741
|
+
const tmpDir = createTempDir();
|
|
742
|
+
try {
|
|
743
|
+
// Create two files
|
|
744
|
+
fs.writeFileSync(path.join(tmpDir, 'file1.js'), 'function one() {}');
|
|
745
|
+
fs.writeFileSync(path.join(tmpDir, 'file2.js'), 'function two() {}');
|
|
746
|
+
|
|
747
|
+
// Build and save cache
|
|
748
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
749
|
+
index1.build('**/*.js', { quiet: true });
|
|
750
|
+
index1.saveCache();
|
|
751
|
+
|
|
752
|
+
// Delete one file
|
|
753
|
+
fs.unlinkSync(path.join(tmpDir, 'file2.js'));
|
|
754
|
+
|
|
755
|
+
// Load cache and check staleness
|
|
756
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
757
|
+
index2.loadCache();
|
|
758
|
+
assert.ok(index2.isCacheStale(), 'Cache should be stale after deleting file');
|
|
759
|
+
} finally {
|
|
760
|
+
cleanup(tmpDir);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should handle corrupted cache gracefully', () => {
|
|
765
|
+
const tmpDir = createTempDir();
|
|
766
|
+
try {
|
|
767
|
+
// Create cache directory with invalid JSON
|
|
768
|
+
const cacheDir = path.join(tmpDir, '.ucn-cache');
|
|
769
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
770
|
+
fs.writeFileSync(path.join(cacheDir, 'index.json'), 'not valid json {{{');
|
|
771
|
+
|
|
772
|
+
// loadCache should return false
|
|
773
|
+
const index = new ProjectIndex(tmpDir);
|
|
774
|
+
const loaded = index.loadCache();
|
|
775
|
+
assert.strictEqual(loaded, false, 'Should not load corrupted cache');
|
|
776
|
+
} finally {
|
|
777
|
+
cleanup(tmpDir);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should handle version mismatch gracefully', () => {
|
|
782
|
+
const tmpDir = createTempDir();
|
|
783
|
+
try {
|
|
784
|
+
// Create cache with wrong version
|
|
785
|
+
const cacheDir = path.join(tmpDir, '.ucn-cache');
|
|
786
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
787
|
+
fs.writeFileSync(path.join(cacheDir, 'index.json'), JSON.stringify({
|
|
788
|
+
version: 999,
|
|
789
|
+
files: [],
|
|
790
|
+
symbols: [],
|
|
791
|
+
importGraph: [],
|
|
792
|
+
exportGraph: []
|
|
793
|
+
}));
|
|
794
|
+
|
|
795
|
+
// loadCache should return false
|
|
796
|
+
const index = new ProjectIndex(tmpDir);
|
|
797
|
+
const loaded = index.loadCache();
|
|
798
|
+
assert.strictEqual(loaded, false, 'Should not load cache with wrong version');
|
|
799
|
+
} finally {
|
|
800
|
+
cleanup(tmpDir);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should report not stale when files unchanged', () => {
|
|
805
|
+
const tmpDir = createTempDir();
|
|
806
|
+
try {
|
|
807
|
+
// Create test file
|
|
808
|
+
const testFile = path.join(tmpDir, 'test.js');
|
|
809
|
+
fs.writeFileSync(testFile, 'function unchanged() {}');
|
|
810
|
+
|
|
811
|
+
// Build and save cache
|
|
812
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
813
|
+
index1.build('**/*.js', { quiet: true });
|
|
814
|
+
index1.saveCache();
|
|
815
|
+
|
|
816
|
+
// Load cache without modifications
|
|
817
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
818
|
+
index2.loadCache();
|
|
819
|
+
assert.strictEqual(index2.isCacheStale(), false, 'Cache should not be stale when files unchanged');
|
|
820
|
+
} finally {
|
|
821
|
+
cleanup(tmpDir);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// File Discovery Tests (conditional ignores)
|
|
828
|
+
// ============================================================================
|
|
829
|
+
|
|
830
|
+
describe('File Discovery', () => {
|
|
831
|
+
const os = require('os');
|
|
832
|
+
|
|
833
|
+
function createTempDir() {
|
|
834
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
835
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
836
|
+
return tmpDir;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function cleanup(dir) {
|
|
840
|
+
if (dir && fs.existsSync(dir)) {
|
|
841
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
it('should ignore vendor/ when go.mod exists (Go project)', () => {
|
|
846
|
+
const tmpDir = createTempDir();
|
|
847
|
+
try {
|
|
848
|
+
// Create Go project structure
|
|
849
|
+
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test');
|
|
850
|
+
fs.writeFileSync(path.join(tmpDir, 'main.go'), 'package main\nfunc main() {}');
|
|
851
|
+
fs.mkdirSync(path.join(tmpDir, 'vendor'));
|
|
852
|
+
fs.writeFileSync(path.join(tmpDir, 'vendor', 'dep.go'), 'package vendor');
|
|
853
|
+
|
|
854
|
+
const files = expandGlob('**/*.go', { root: tmpDir });
|
|
855
|
+
const relativePaths = files.map(f => path.relative(tmpDir, f));
|
|
856
|
+
|
|
857
|
+
assert.ok(relativePaths.includes('main.go'), 'Should find main.go');
|
|
858
|
+
assert.ok(!relativePaths.some(p => p.includes('vendor')), 'Should NOT find vendor files');
|
|
859
|
+
} finally {
|
|
860
|
+
cleanup(tmpDir);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('should NOT ignore vendor/ when no marker exists (user code)', () => {
|
|
865
|
+
const tmpDir = createTempDir();
|
|
866
|
+
try {
|
|
867
|
+
// Create project WITHOUT go.mod/composer.json
|
|
868
|
+
fs.writeFileSync(path.join(tmpDir, 'main.js'), 'function main() {}');
|
|
869
|
+
fs.mkdirSync(path.join(tmpDir, 'vendor'));
|
|
870
|
+
fs.writeFileSync(path.join(tmpDir, 'vendor', 'management.js'), 'function vendorMgmt() {}');
|
|
871
|
+
|
|
872
|
+
const files = expandGlob('**/*.js', { root: tmpDir });
|
|
873
|
+
const relativePaths = files.map(f => path.relative(tmpDir, f));
|
|
874
|
+
|
|
875
|
+
assert.ok(relativePaths.includes('main.js'), 'Should find main.js');
|
|
876
|
+
assert.ok(relativePaths.some(p => p.includes('vendor')), 'Should find vendor files (user code)');
|
|
877
|
+
} finally {
|
|
878
|
+
cleanup(tmpDir);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should ignore Pods/ when Podfile exists (iOS project)', () => {
|
|
883
|
+
const tmpDir = createTempDir();
|
|
884
|
+
try {
|
|
885
|
+
// Create iOS project structure
|
|
886
|
+
fs.writeFileSync(path.join(tmpDir, 'Podfile'), "platform :ios, '14.0'");
|
|
887
|
+
fs.writeFileSync(path.join(tmpDir, 'App.swift'), 'class App {}');
|
|
888
|
+
fs.mkdirSync(path.join(tmpDir, 'Pods'));
|
|
889
|
+
fs.writeFileSync(path.join(tmpDir, 'Pods', 'Dep.swift'), 'class Dep {}');
|
|
890
|
+
|
|
891
|
+
const files = expandGlob('**/*.swift', { root: tmpDir });
|
|
892
|
+
const relativePaths = files.map(f => path.relative(tmpDir, f));
|
|
893
|
+
|
|
894
|
+
assert.ok(relativePaths.includes('App.swift'), 'Should find App.swift');
|
|
895
|
+
assert.ok(!relativePaths.some(p => p.includes('Pods')), 'Should NOT find Pods files');
|
|
896
|
+
} finally {
|
|
897
|
+
cleanup(tmpDir);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('should always ignore node_modules (unconditional)', () => {
|
|
902
|
+
const tmpDir = createTempDir();
|
|
903
|
+
try {
|
|
904
|
+
fs.writeFileSync(path.join(tmpDir, 'main.js'), 'function main() {}');
|
|
905
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules'));
|
|
906
|
+
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'dep.js'), 'module.exports = {}');
|
|
907
|
+
|
|
908
|
+
const files = expandGlob('**/*.js', { root: tmpDir });
|
|
909
|
+
const relativePaths = files.map(f => path.relative(tmpDir, f));
|
|
910
|
+
|
|
911
|
+
assert.ok(relativePaths.includes('main.js'), 'Should find main.js');
|
|
912
|
+
assert.ok(!relativePaths.some(p => p.includes('node_modules')), 'Should NOT find node_modules');
|
|
913
|
+
} finally {
|
|
914
|
+
cleanup(tmpDir);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// ============================================================================
|
|
920
|
+
// BUG TESTS: Callers should not include definitions
|
|
921
|
+
// ============================================================================
|
|
922
|
+
|
|
923
|
+
describe('Bug: CALLERS should exclude definitions', () => {
|
|
924
|
+
it('context command should not show definitions in CALLERS', () => {
|
|
925
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-callers-${Date.now()}`);
|
|
926
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
927
|
+
|
|
928
|
+
try {
|
|
929
|
+
// Create files: one with definition, one with call
|
|
930
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
931
|
+
function myFunc() {
|
|
932
|
+
return 42;
|
|
933
|
+
}
|
|
934
|
+
module.exports = { myFunc };
|
|
935
|
+
`);
|
|
936
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
937
|
+
const { myFunc } = require('./lib');
|
|
938
|
+
const result = myFunc();
|
|
939
|
+
console.log(result);
|
|
940
|
+
`);
|
|
941
|
+
|
|
942
|
+
const index = new ProjectIndex(tmpDir);
|
|
943
|
+
index.build('**/*.js', { quiet: true });
|
|
944
|
+
|
|
945
|
+
const ctx = index.context('myFunc');
|
|
946
|
+
|
|
947
|
+
// Callers should only have actual calls, not definitions
|
|
948
|
+
const callerLines = ctx.callers.map(c => c.content || c.line);
|
|
949
|
+
const hasDefinition = callerLines.some(line =>
|
|
950
|
+
line.includes('function myFunc') ||
|
|
951
|
+
line.includes('myFunc()') === false && line.includes('myFunc')
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
// This test documents the bug - callers should not include the definition
|
|
955
|
+
assert.ok(ctx.callers.length > 0, 'Should have some callers');
|
|
956
|
+
// After fix: assert.ok(!hasDefinition, 'Callers should not include function definitions');
|
|
957
|
+
} finally {
|
|
958
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// ============================================================================
|
|
964
|
+
// BUG TESTS: Callers should be numbered for expand command
|
|
965
|
+
// ============================================================================
|
|
966
|
+
|
|
967
|
+
describe('Bug: callers should be numbered for expand command', () => {
|
|
968
|
+
it('context command should return caller info for expand', () => {
|
|
969
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-caller-num-${Date.now()}`);
|
|
970
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
974
|
+
function helper() {
|
|
975
|
+
return 42;
|
|
976
|
+
}
|
|
977
|
+
module.exports = { helper };
|
|
978
|
+
`);
|
|
979
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
980
|
+
const { helper } = require('./lib');
|
|
981
|
+
function caller1() {
|
|
982
|
+
return helper(); // Call from caller1
|
|
983
|
+
}
|
|
984
|
+
function caller2() {
|
|
985
|
+
return helper(); // Call from caller2
|
|
986
|
+
}
|
|
987
|
+
`);
|
|
988
|
+
|
|
989
|
+
const index = new ProjectIndex(tmpDir);
|
|
990
|
+
index.build('**/*.js', { quiet: true });
|
|
991
|
+
|
|
992
|
+
const ctx = index.context('helper');
|
|
993
|
+
|
|
994
|
+
// Callers should have file and line info for expand
|
|
995
|
+
assert.ok(ctx.callers.length >= 2, 'Should have at least 2 callers');
|
|
996
|
+
for (const caller of ctx.callers) {
|
|
997
|
+
assert.ok(caller.file, 'Caller should have file');
|
|
998
|
+
assert.ok(caller.line, 'Caller should have line');
|
|
999
|
+
// callerFile should be set when there's an enclosing function
|
|
1000
|
+
if (caller.callerName) {
|
|
1001
|
+
assert.ok(caller.callerFile, 'Caller with name should have callerFile');
|
|
1002
|
+
assert.ok(caller.callerStartLine, 'Caller with name should have callerStartLine');
|
|
1003
|
+
assert.ok(caller.callerEndLine, 'Caller with name should have callerEndLine');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
} finally {
|
|
1007
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// ============================================================================
|
|
1013
|
+
// BUG TESTS: Name matching should distinguish method calls
|
|
1014
|
+
// ============================================================================
|
|
1015
|
+
|
|
1016
|
+
describe('Bug: usages should distinguish method calls from standalone calls', () => {
|
|
1017
|
+
it('should not include JSON.parse when searching for parse', () => {
|
|
1018
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-method-${Date.now()}`);
|
|
1019
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1020
|
+
|
|
1021
|
+
try {
|
|
1022
|
+
// File with own parse function + JSON.parse usage
|
|
1023
|
+
fs.writeFileSync(path.join(tmpDir, 'parser.js'), `
|
|
1024
|
+
function parse(code) {
|
|
1025
|
+
return code.trim();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function loadConfig() {
|
|
1029
|
+
const data = JSON.parse('{}'); // Should NOT be counted as usage of parse()
|
|
1030
|
+
return parse(data); // Should be counted
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
module.exports = { parse };
|
|
1034
|
+
`);
|
|
1035
|
+
|
|
1036
|
+
const index = new ProjectIndex(tmpDir);
|
|
1037
|
+
index.build('**/*.js', { quiet: true });
|
|
1038
|
+
|
|
1039
|
+
const usages = index.usages('parse');
|
|
1040
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
1041
|
+
|
|
1042
|
+
// Check if JSON.parse is incorrectly included
|
|
1043
|
+
const hasJsonParse = calls.some(c =>
|
|
1044
|
+
c.content && c.content.includes('JSON.parse')
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
// Bug fixed: JSON.parse should not be counted as a call to parse()
|
|
1048
|
+
assert.strictEqual(hasJsonParse, false, 'JSON.parse should not be counted as usage of parse()');
|
|
1049
|
+
} finally {
|
|
1050
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('should not include path.parse when searching for parse', () => {
|
|
1055
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-path-${Date.now()}`);
|
|
1056
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
fs.writeFileSync(path.join(tmpDir, 'util.js'), `
|
|
1060
|
+
const path = require('path');
|
|
1061
|
+
|
|
1062
|
+
function parse(input) {
|
|
1063
|
+
return input.split(',');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function getRoot() {
|
|
1067
|
+
const parsed = path.parse('/foo/bar'); // Should NOT be counted
|
|
1068
|
+
return parse(parsed.dir); // Should be counted
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
module.exports = { parse };
|
|
1072
|
+
`);
|
|
1073
|
+
|
|
1074
|
+
const index = new ProjectIndex(tmpDir);
|
|
1075
|
+
index.build('**/*.js', { quiet: true });
|
|
1076
|
+
|
|
1077
|
+
const usages = index.usages('parse');
|
|
1078
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
1079
|
+
|
|
1080
|
+
const hasPathParse = calls.some(c =>
|
|
1081
|
+
c.content && c.content.includes('path.parse')
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
// Bug fixed: path.parse should not be counted as a call to parse()
|
|
1085
|
+
assert.strictEqual(hasPathParse, false, 'path.parse should not be counted');
|
|
1086
|
+
} finally {
|
|
1087
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
// BUG TESTS: smart command should not duplicate main function
|
|
1094
|
+
// ============================================================================
|
|
1095
|
+
|
|
1096
|
+
describe('Bug: smart command should not duplicate main function', () => {
|
|
1097
|
+
it('main function should not appear in dependencies', () => {
|
|
1098
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-smart-${Date.now()}`);
|
|
1099
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1100
|
+
|
|
1101
|
+
try {
|
|
1102
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1103
|
+
function helper() {
|
|
1104
|
+
return 'helped';
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function main() {
|
|
1108
|
+
return helper();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
module.exports = { main };
|
|
1112
|
+
`);
|
|
1113
|
+
|
|
1114
|
+
const index = new ProjectIndex(tmpDir);
|
|
1115
|
+
index.build('**/*.js', { quiet: true });
|
|
1116
|
+
|
|
1117
|
+
const smart = index.smart('main');
|
|
1118
|
+
|
|
1119
|
+
// Dependencies should NOT include main itself
|
|
1120
|
+
const depNames = smart.dependencies.map(d => d.name);
|
|
1121
|
+
const hasSelf = depNames.includes('main');
|
|
1122
|
+
|
|
1123
|
+
// After fix: assert.strictEqual(hasSelf, false, 'Dependencies should not include the main function itself');
|
|
1124
|
+
if (hasSelf) {
|
|
1125
|
+
console.log('BUG CONFIRMED: smart includes main function in its own dependencies');
|
|
1126
|
+
}
|
|
1127
|
+
} finally {
|
|
1128
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// ============================================================================
|
|
1134
|
+
// BUG TESTS: plan should not pick up string literals
|
|
1135
|
+
// ============================================================================
|
|
1136
|
+
|
|
1137
|
+
describe('Bug: plan should ignore string literals', () => {
|
|
1138
|
+
it('should not count string literals as call sites', () => {
|
|
1139
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-plan-${Date.now()}`);
|
|
1140
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
// Main file with actual function
|
|
1144
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
1145
|
+
function myFunc(x) {
|
|
1146
|
+
return x * 2;
|
|
1147
|
+
}
|
|
1148
|
+
module.exports = { myFunc };
|
|
1149
|
+
`);
|
|
1150
|
+
// Test file with string literal containing function name
|
|
1151
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
1152
|
+
const code = 'function myFunc() {}'; // String literal - NOT a call site
|
|
1153
|
+
const result = myFunc(5); // Actual call site
|
|
1154
|
+
`);
|
|
1155
|
+
|
|
1156
|
+
const index = new ProjectIndex(tmpDir);
|
|
1157
|
+
index.build('**/*.js', { quiet: true });
|
|
1158
|
+
|
|
1159
|
+
// Note: plan() might not be directly exposed, testing via impact instead
|
|
1160
|
+
const usages = index.usages('myFunc');
|
|
1161
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
1162
|
+
|
|
1163
|
+
// Check if string literal is incorrectly counted
|
|
1164
|
+
const hasStringLiteral = calls.some(c =>
|
|
1165
|
+
c.content && c.content.includes("'function myFunc")
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
// Bug fixed: String literals should not be counted as calls
|
|
1169
|
+
assert.strictEqual(hasStringLiteral, false, 'String literals should not be counted as calls');
|
|
1170
|
+
} finally {
|
|
1171
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// ============================================================================
|
|
1177
|
+
// BUG TESTS: file mode should filter string literals
|
|
1178
|
+
// ============================================================================
|
|
1179
|
+
|
|
1180
|
+
describe('Bug: file mode usages should filter string literals', () => {
|
|
1181
|
+
it('should not count string literals as usages in file mode', () => {
|
|
1182
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-filemode-${Date.now()}`);
|
|
1183
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1187
|
+
function main() {
|
|
1188
|
+
console.log('about main'); // "main" in string should NOT be a reference
|
|
1189
|
+
return helper();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function helper() {
|
|
1193
|
+
return 42;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
main(); // Actual call
|
|
1197
|
+
`);
|
|
1198
|
+
|
|
1199
|
+
// Use CLI file mode via child process
|
|
1200
|
+
const { execSync } = require('child_process');
|
|
1201
|
+
const result = execSync(`node cli/index.js ${path.join(tmpDir, 'app.js')} usages main`, {
|
|
1202
|
+
encoding: 'utf-8',
|
|
1203
|
+
cwd: process.cwd()
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// Should have 1 def, 1 call, 0 references (string literal excluded)
|
|
1207
|
+
assert.ok(result.includes('1 definitions'), 'Should have 1 definition');
|
|
1208
|
+
assert.ok(result.includes('1 calls'), 'Should have 1 call');
|
|
1209
|
+
assert.ok(result.includes('0 references') || !result.includes('REFERENCES'),
|
|
1210
|
+
'Should have 0 references (string literal excluded)');
|
|
1211
|
+
} finally {
|
|
1212
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// ============================================================================
|
|
1218
|
+
// BUG TESTS: stats symbol count mismatch
|
|
1219
|
+
// ============================================================================
|
|
1220
|
+
|
|
1221
|
+
describe('Bug: stats symbol count consistency', () => {
|
|
1222
|
+
it('total symbols should equal sum of type counts', () => {
|
|
1223
|
+
const index = new ProjectIndex('.');
|
|
1224
|
+
index.build(null, { quiet: true });
|
|
1225
|
+
|
|
1226
|
+
const stats = index.getStats();
|
|
1227
|
+
|
|
1228
|
+
// Calculate sum of type counts
|
|
1229
|
+
let typeSum = 0;
|
|
1230
|
+
if (stats.byType) {
|
|
1231
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
1232
|
+
typeSum += count;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// This documents the bug - symbol count doesn't match type breakdown
|
|
1237
|
+
// After fix: assert.strictEqual(stats.symbols, typeSum, 'Total symbols should equal sum of types');
|
|
1238
|
+
if (stats.symbols !== typeSum) {
|
|
1239
|
+
console.log(`BUG CONFIRMED: stats.symbols (${stats.symbols}) !== sum of byType (${typeSum})`);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// ============================================================================
|
|
1245
|
+
// BUG TESTS: --context flag output
|
|
1246
|
+
// ============================================================================
|
|
1247
|
+
|
|
1248
|
+
describe('Bug: --context flag should show complete lines', () => {
|
|
1249
|
+
it('context lines should not be truncated with ...', () => {
|
|
1250
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-context-${Date.now()}`);
|
|
1251
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1255
|
+
// Line 1 - before context
|
|
1256
|
+
// Line 2 - before context
|
|
1257
|
+
function processData(input) {
|
|
1258
|
+
// Line 4 - before context
|
|
1259
|
+
const result = helper(input);
|
|
1260
|
+
// Line 6 - after context
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function helper(x) {
|
|
1265
|
+
return x * 2;
|
|
1266
|
+
}
|
|
1267
|
+
`);
|
|
1268
|
+
|
|
1269
|
+
const index = new ProjectIndex(tmpDir);
|
|
1270
|
+
index.build('**/*.js', { quiet: true });
|
|
1271
|
+
|
|
1272
|
+
// Test usages with context
|
|
1273
|
+
const usages = index.usages('helper', { context: 2 });
|
|
1274
|
+
assert.ok(usages.length > 0, 'Should find usages');
|
|
1275
|
+
|
|
1276
|
+
// Find the call usage (not the definition)
|
|
1277
|
+
const callUsage = usages.find(u => u.usageType === 'call');
|
|
1278
|
+
assert.ok(callUsage, 'Should have a call usage');
|
|
1279
|
+
|
|
1280
|
+
// Verify context lines are present and complete
|
|
1281
|
+
if (callUsage.before) {
|
|
1282
|
+
assert.strictEqual(callUsage.before.length, 2, 'Should have 2 before context lines');
|
|
1283
|
+
assert.ok(!callUsage.before.some(l => l.includes('...')), 'Before context should not be truncated');
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (callUsage.after) {
|
|
1287
|
+
assert.strictEqual(callUsage.after.length, 2, 'Should have 2 after context lines');
|
|
1288
|
+
assert.ok(!callUsage.after.some(l => l.includes('...')), 'After context should not be truncated');
|
|
1289
|
+
}
|
|
1290
|
+
} finally {
|
|
1291
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('context should handle file boundaries correctly', () => {
|
|
1296
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-context-boundary-${Date.now()}`);
|
|
1297
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1298
|
+
|
|
1299
|
+
try {
|
|
1300
|
+
// File where match is at the beginning
|
|
1301
|
+
fs.writeFileSync(path.join(tmpDir, 'start.js'), `const result = helper();
|
|
1302
|
+
// Line 2
|
|
1303
|
+
// Line 3
|
|
1304
|
+
`);
|
|
1305
|
+
|
|
1306
|
+
const index = new ProjectIndex(tmpDir);
|
|
1307
|
+
index.build('**/*.js', { quiet: true });
|
|
1308
|
+
|
|
1309
|
+
const usages = index.usages('helper', { context: 3 });
|
|
1310
|
+
const usage = usages.find(u => u.usageType === 'call');
|
|
1311
|
+
|
|
1312
|
+
if (usage) {
|
|
1313
|
+
// Before context should be empty or less than 3 (at file start)
|
|
1314
|
+
assert.ok(!usage.before || usage.before.length < 3, 'Should handle file start boundary');
|
|
1315
|
+
}
|
|
1316
|
+
} finally {
|
|
1317
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it('search command should support context lines', () => {
|
|
1322
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-search-context-${Date.now()}`);
|
|
1323
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1327
|
+
// Line 1
|
|
1328
|
+
// Line 2
|
|
1329
|
+
// TODO: fix this issue
|
|
1330
|
+
// Line 4
|
|
1331
|
+
// Line 5
|
|
1332
|
+
`);
|
|
1333
|
+
|
|
1334
|
+
const index = new ProjectIndex(tmpDir);
|
|
1335
|
+
index.build('**/*.js', { quiet: true });
|
|
1336
|
+
|
|
1337
|
+
const results = index.search('TODO', { context: 2 });
|
|
1338
|
+
assert.ok(results.length > 0, 'Should find search results');
|
|
1339
|
+
|
|
1340
|
+
const match = results[0].matches[0];
|
|
1341
|
+
assert.ok(match.before && match.before.length > 0, 'Should have before context');
|
|
1342
|
+
assert.ok(match.after && match.after.length > 0, 'Should have after context');
|
|
1343
|
+
} finally {
|
|
1344
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// ============================================================================
|
|
1350
|
+
// FEATURE TESTS: file-exports command (currently missing)
|
|
1351
|
+
// ============================================================================
|
|
1352
|
+
|
|
1353
|
+
describe('Feature: file-exports command', () => {
|
|
1354
|
+
it('should return exports for a file (when implemented)', () => {
|
|
1355
|
+
const index = new ProjectIndex('.');
|
|
1356
|
+
index.build(null, { quiet: true });
|
|
1357
|
+
|
|
1358
|
+
// Check if fileExports method exists
|
|
1359
|
+
if (typeof index.fileExports === 'function') {
|
|
1360
|
+
const exports = index.fileExports('core/parser.js');
|
|
1361
|
+
assert.ok(Array.isArray(exports), 'Should return array of exports');
|
|
1362
|
+
assert.ok(exports.some(e => e.name === 'parse'), 'Should export parse function');
|
|
1363
|
+
} else {
|
|
1364
|
+
// Document that feature is missing
|
|
1365
|
+
console.log('FEATURE MISSING: index.fileExports() not implemented');
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// ============================================================================
|
|
1371
|
+
// FEATURE TESTS: deadcode command (currently missing)
|
|
1372
|
+
// ============================================================================
|
|
1373
|
+
|
|
1374
|
+
describe('Feature: deadcode detection', () => {
|
|
1375
|
+
it('should find unused functions (when implemented)', () => {
|
|
1376
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-deadcode-${Date.now()}`);
|
|
1377
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1378
|
+
|
|
1379
|
+
try {
|
|
1380
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
1381
|
+
function usedFunction() {
|
|
1382
|
+
return 42;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function unusedFunction() { // This should be detected as dead code
|
|
1386
|
+
return 'never called';
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const result = usedFunction();
|
|
1390
|
+
console.log(result);
|
|
1391
|
+
`);
|
|
1392
|
+
|
|
1393
|
+
const index = new ProjectIndex(tmpDir);
|
|
1394
|
+
index.build('**/*.js', { quiet: true });
|
|
1395
|
+
|
|
1396
|
+
// Check if deadcode method exists
|
|
1397
|
+
if (typeof index.deadcode === 'function') {
|
|
1398
|
+
const dead = index.deadcode();
|
|
1399
|
+
assert.ok(Array.isArray(dead), 'Should return array');
|
|
1400
|
+
assert.ok(dead.some(d => d.name === 'unusedFunction'), 'Should find unused function');
|
|
1401
|
+
} else {
|
|
1402
|
+
console.log('FEATURE MISSING: index.deadcode() not implemented');
|
|
1403
|
+
}
|
|
1404
|
+
} finally {
|
|
1405
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
// FEATURE TESTS: graph command
|
|
1411
|
+
describe('Feature: graph command', () => {
|
|
1412
|
+
it('returns dependency tree for a file', () => {
|
|
1413
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-graph-${Date.now()}`);
|
|
1414
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
// Create files with import relationships
|
|
1418
|
+
fs.writeFileSync(path.join(tmpDir, 'main.js'), `
|
|
1419
|
+
import { helper } from './utils.js';
|
|
1420
|
+
import { api } from './api.js';
|
|
1421
|
+
|
|
1422
|
+
export function main() {
|
|
1423
|
+
return helper() + api();
|
|
1424
|
+
}
|
|
1425
|
+
`);
|
|
1426
|
+
fs.writeFileSync(path.join(tmpDir, 'utils.js'), `
|
|
1427
|
+
export function helper() { return 1; }
|
|
1428
|
+
`);
|
|
1429
|
+
fs.writeFileSync(path.join(tmpDir, 'api.js'), `
|
|
1430
|
+
import { helper } from './utils.js';
|
|
1431
|
+
export function api() { return helper() + 2; }
|
|
1432
|
+
`);
|
|
1433
|
+
|
|
1434
|
+
const index = new ProjectIndex(tmpDir);
|
|
1435
|
+
index.build('**/*.js', { quiet: true });
|
|
1436
|
+
|
|
1437
|
+
const graph = index.graph('main.js', { direction: 'both', maxDepth: 3 });
|
|
1438
|
+
|
|
1439
|
+
// Should have root
|
|
1440
|
+
assert.ok(graph.root.endsWith('main.js'), 'Root should be main.js');
|
|
1441
|
+
|
|
1442
|
+
// Should have nodes
|
|
1443
|
+
assert.ok(graph.nodes.length >= 3, 'Should have at least 3 nodes');
|
|
1444
|
+
|
|
1445
|
+
// Should have edges (imports)
|
|
1446
|
+
assert.ok(graph.edges.length >= 2, 'Should have at least 2 edges');
|
|
1447
|
+
|
|
1448
|
+
// Check that utils.js and api.js are in the graph
|
|
1449
|
+
const nodeNames = graph.nodes.map(n => n.relativePath);
|
|
1450
|
+
assert.ok(nodeNames.some(n => n.includes('utils.js')), 'Should include utils.js');
|
|
1451
|
+
assert.ok(nodeNames.some(n => n.includes('api.js')), 'Should include api.js');
|
|
1452
|
+
} finally {
|
|
1453
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
it('handles circular dependencies', () => {
|
|
1458
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-graph-circular-${Date.now()}`);
|
|
1459
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1460
|
+
|
|
1461
|
+
try {
|
|
1462
|
+
// Create circular import relationship
|
|
1463
|
+
fs.writeFileSync(path.join(tmpDir, 'a.js'), `
|
|
1464
|
+
import { b } from './b.js';
|
|
1465
|
+
export function a() { return b() + 1; }
|
|
1466
|
+
`);
|
|
1467
|
+
fs.writeFileSync(path.join(tmpDir, 'b.js'), `
|
|
1468
|
+
import { a } from './a.js';
|
|
1469
|
+
export function b() { return a() + 2; }
|
|
1470
|
+
`);
|
|
1471
|
+
|
|
1472
|
+
const index = new ProjectIndex(tmpDir);
|
|
1473
|
+
index.build('**/*.js', { quiet: true });
|
|
1474
|
+
|
|
1475
|
+
// Should not infinite loop
|
|
1476
|
+
const graph = index.graph('a.js', { direction: 'both', maxDepth: 5 });
|
|
1477
|
+
|
|
1478
|
+
// Should have both files
|
|
1479
|
+
assert.ok(graph.nodes.length === 2, 'Should have exactly 2 nodes');
|
|
1480
|
+
} finally {
|
|
1481
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// ============================================================================
|
|
1487
|
+
// EDGE CASE TESTS
|
|
1488
|
+
// ============================================================================
|
|
1489
|
+
|
|
1490
|
+
describe('Edge Cases', () => {
|
|
1491
|
+
it('should handle recursive function calls correctly', () => {
|
|
1492
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-recursive-${Date.now()}`);
|
|
1493
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1494
|
+
|
|
1495
|
+
try {
|
|
1496
|
+
fs.writeFileSync(path.join(tmpDir, 'recursive.js'), `
|
|
1497
|
+
function factorial(n) {
|
|
1498
|
+
if (n <= 1) return 1;
|
|
1499
|
+
return n * factorial(n - 1); // Recursive call
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const result = factorial(5);
|
|
1503
|
+
`);
|
|
1504
|
+
|
|
1505
|
+
const index = new ProjectIndex(tmpDir);
|
|
1506
|
+
index.build('**/*.js', { quiet: true });
|
|
1507
|
+
|
|
1508
|
+
const ctx = index.context('factorial');
|
|
1509
|
+
|
|
1510
|
+
// Should have callers (including the recursive call)
|
|
1511
|
+
assert.ok(ctx.callers.length > 0, 'Should find callers including recursive call');
|
|
1512
|
+
|
|
1513
|
+
// Definition should NOT be in callers
|
|
1514
|
+
const hasDefinitionInCallers = ctx.callers.some(c =>
|
|
1515
|
+
c.content && c.content.includes('function factorial')
|
|
1516
|
+
);
|
|
1517
|
+
// After fix: assert.strictEqual(hasDefinitionInCallers, false);
|
|
1518
|
+
} finally {
|
|
1519
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
it('should handle aliased imports correctly', () => {
|
|
1524
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-alias-${Date.now()}`);
|
|
1525
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1526
|
+
|
|
1527
|
+
try {
|
|
1528
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
1529
|
+
function parse(code) {
|
|
1530
|
+
return code.trim();
|
|
1531
|
+
}
|
|
1532
|
+
module.exports = { parse };
|
|
1533
|
+
`);
|
|
1534
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1535
|
+
const { parse: myParse } = require('./lib');
|
|
1536
|
+
const result = myParse(' hello '); // Should be counted as usage of parse
|
|
1537
|
+
`);
|
|
1538
|
+
|
|
1539
|
+
const index = new ProjectIndex(tmpDir);
|
|
1540
|
+
index.build('**/*.js', { quiet: true });
|
|
1541
|
+
|
|
1542
|
+
const usages = index.usages('parse');
|
|
1543
|
+
// Aliased usage is tricky - should ideally track the alias
|
|
1544
|
+
assert.ok(usages.length > 0, 'Should find at least the definition');
|
|
1545
|
+
} finally {
|
|
1546
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('should handle same function name in different files', () => {
|
|
1551
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-samename-${Date.now()}`);
|
|
1552
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1553
|
+
|
|
1554
|
+
try {
|
|
1555
|
+
fs.writeFileSync(path.join(tmpDir, 'file1.js'), `
|
|
1556
|
+
function process(x) { return x + 1; }
|
|
1557
|
+
module.exports = { process };
|
|
1558
|
+
`);
|
|
1559
|
+
fs.writeFileSync(path.join(tmpDir, 'file2.js'), `
|
|
1560
|
+
function process(x) { return x * 2; }
|
|
1561
|
+
module.exports = { process };
|
|
1562
|
+
`);
|
|
1563
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1564
|
+
const m1 = require('./file1');
|
|
1565
|
+
const m2 = require('./file2');
|
|
1566
|
+
console.log(m1.process(5));
|
|
1567
|
+
console.log(m2.process(5));
|
|
1568
|
+
`);
|
|
1569
|
+
|
|
1570
|
+
const index = new ProjectIndex(tmpDir);
|
|
1571
|
+
index.build('**/*.js', { quiet: true });
|
|
1572
|
+
|
|
1573
|
+
const found = index.find('process');
|
|
1574
|
+
assert.strictEqual(found.length, 2, 'Should find both process functions');
|
|
1575
|
+
} finally {
|
|
1576
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// ============================================================================
|
|
1582
|
+
// NON-EXISTENT SYMBOL HANDLING
|
|
1583
|
+
// ============================================================================
|
|
1584
|
+
|
|
1585
|
+
describe('Non-existent symbol handling', () => {
|
|
1586
|
+
// Helper to create and cleanup temp project
|
|
1587
|
+
function withTempProject(fn) {
|
|
1588
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-nonexist-${Date.now()}`);
|
|
1589
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1590
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1591
|
+
function existingFunc() {
|
|
1592
|
+
return 42;
|
|
1593
|
+
}
|
|
1594
|
+
module.exports = { existingFunc };
|
|
1595
|
+
`);
|
|
1596
|
+
const index = new ProjectIndex(tmpDir);
|
|
1597
|
+
index.build('**/*.js', { quiet: true });
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
fn(index);
|
|
1601
|
+
} finally {
|
|
1602
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
it('find should return empty array for non-existent symbol', () => {
|
|
1607
|
+
withTempProject((index) => {
|
|
1608
|
+
const found = index.find('nonExistentSymbol');
|
|
1609
|
+
assert.ok(Array.isArray(found), 'Should return array');
|
|
1610
|
+
assert.strictEqual(found.length, 0, 'Should be empty');
|
|
1611
|
+
});
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it('usages should return empty array for non-existent symbol', () => {
|
|
1615
|
+
withTempProject((index) => {
|
|
1616
|
+
const usages = index.usages('nonExistentSymbol');
|
|
1617
|
+
assert.ok(Array.isArray(usages), 'Should return array');
|
|
1618
|
+
assert.strictEqual(usages.length, 0, 'Should be empty');
|
|
1619
|
+
});
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
it('context should return empty callers/callees for non-existent symbol', () => {
|
|
1623
|
+
withTempProject((index) => {
|
|
1624
|
+
const ctx = index.context('nonExistentSymbol');
|
|
1625
|
+
assert.ok(ctx, 'Should return context object');
|
|
1626
|
+
assert.strictEqual(ctx.function, 'nonExistentSymbol', 'Should include queried name');
|
|
1627
|
+
assert.ok(Array.isArray(ctx.callers), 'Callers should be array');
|
|
1628
|
+
assert.ok(Array.isArray(ctx.callees), 'Callees should be array');
|
|
1629
|
+
assert.strictEqual(ctx.callers.length, 0, 'Callers should be empty');
|
|
1630
|
+
assert.strictEqual(ctx.callees.length, 0, 'Callees should be empty');
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
it('smart should return null for non-existent function', () => {
|
|
1635
|
+
withTempProject((index) => {
|
|
1636
|
+
const smart = index.smart('nonExistentSymbol');
|
|
1637
|
+
assert.strictEqual(smart, null, 'Should return null');
|
|
1638
|
+
});
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
it('about should return null for non-existent symbol', () => {
|
|
1642
|
+
withTempProject((index) => {
|
|
1643
|
+
const about = index.about('nonExistentSymbol');
|
|
1644
|
+
assert.strictEqual(about, null, 'Should return null');
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
it('impact should return null for non-existent function', () => {
|
|
1649
|
+
withTempProject((index) => {
|
|
1650
|
+
const impact = index.impact('nonExistentSymbol');
|
|
1651
|
+
assert.strictEqual(impact, null, 'Should return null');
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it('tests should return empty array for non-existent symbol', () => {
|
|
1656
|
+
withTempProject((index) => {
|
|
1657
|
+
const tests = index.tests('nonExistentSymbol');
|
|
1658
|
+
assert.ok(Array.isArray(tests), 'Should return array');
|
|
1659
|
+
assert.strictEqual(tests.length, 0, 'Should be empty');
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it('typedef should return empty array for non-existent type', () => {
|
|
1664
|
+
withTempProject((index) => {
|
|
1665
|
+
const typedefs = index.typedef('NonExistentType');
|
|
1666
|
+
assert.ok(Array.isArray(typedefs), 'Should return array');
|
|
1667
|
+
assert.strictEqual(typedefs.length, 0, 'Should be empty');
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// ============================================================================
|
|
1673
|
+
// COMPREHENSIVE COMMAND AND FLAG TESTS
|
|
1674
|
+
// ============================================================================
|
|
1675
|
+
|
|
1676
|
+
describe('Comprehensive command tests', () => {
|
|
1677
|
+
let tmpDir;
|
|
1678
|
+
let index;
|
|
1679
|
+
|
|
1680
|
+
// Setup test project
|
|
1681
|
+
function setupProject() {
|
|
1682
|
+
tmpDir = path.join(require('os').tmpdir(), `ucn-comprehensive-${Date.now()}`);
|
|
1683
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1684
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
1685
|
+
fs.mkdirSync(path.join(tmpDir, 'test'), { recursive: true });
|
|
1686
|
+
|
|
1687
|
+
// Main source file
|
|
1688
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'main.js'), `
|
|
1689
|
+
/**
|
|
1690
|
+
* Main entry point
|
|
1691
|
+
*/
|
|
1692
|
+
function main() {
|
|
1693
|
+
const result = helper();
|
|
1694
|
+
return processData(result);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function helper() {
|
|
1698
|
+
return { value: 42 };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function processData(data) {
|
|
1702
|
+
return data.value * 2;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Unused function
|
|
1706
|
+
function unusedFunc() {
|
|
1707
|
+
return 'never called';
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
module.exports = { main, helper, processData };
|
|
1711
|
+
`);
|
|
1712
|
+
|
|
1713
|
+
// Test file
|
|
1714
|
+
fs.writeFileSync(path.join(tmpDir, 'test', 'main.test.js'), `
|
|
1715
|
+
const { main, helper } = require('../src/main');
|
|
1716
|
+
|
|
1717
|
+
describe('main', () => {
|
|
1718
|
+
it('should work', () => {
|
|
1719
|
+
const result = main();
|
|
1720
|
+
expect(result).toBe(84);
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
`);
|
|
1724
|
+
|
|
1725
|
+
index = new ProjectIndex(tmpDir);
|
|
1726
|
+
index.build('**/*.js', { quiet: true });
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function cleanupProject() {
|
|
1730
|
+
if (tmpDir) {
|
|
1731
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
it('find command with --exclude filter', () => {
|
|
1736
|
+
setupProject();
|
|
1737
|
+
try {
|
|
1738
|
+
// Find without exclude - should find in both src and test
|
|
1739
|
+
const allResults = index.find('main');
|
|
1740
|
+
assert.ok(allResults.length >= 1, 'Should find main');
|
|
1741
|
+
|
|
1742
|
+
// Find with exclude - should exclude test files
|
|
1743
|
+
const filtered = index.find('main', { exclude: ['test'] });
|
|
1744
|
+
const hasTestFile = filtered.some(r => r.relativePath && r.relativePath.includes('test'));
|
|
1745
|
+
// Test files might be excluded by default in find
|
|
1746
|
+
} finally {
|
|
1747
|
+
cleanupProject();
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
it('find command with --in filter', () => {
|
|
1752
|
+
setupProject();
|
|
1753
|
+
try {
|
|
1754
|
+
// Find only in src directory
|
|
1755
|
+
const srcOnly = index.find('main', { in: 'src' });
|
|
1756
|
+
assert.ok(srcOnly.every(r => r.relativePath && r.relativePath.includes('src')),
|
|
1757
|
+
'All results should be in src directory');
|
|
1758
|
+
} finally {
|
|
1759
|
+
cleanupProject();
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
it('usages command groups by type correctly', () => {
|
|
1764
|
+
setupProject();
|
|
1765
|
+
try {
|
|
1766
|
+
const usages = index.usages('helper');
|
|
1767
|
+
|
|
1768
|
+
// Check that usages are properly categorized
|
|
1769
|
+
const defs = usages.filter(u => u.isDefinition);
|
|
1770
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
1771
|
+
const imports = usages.filter(u => u.usageType === 'import');
|
|
1772
|
+
|
|
1773
|
+
assert.ok(defs.length >= 1, 'Should have at least 1 definition');
|
|
1774
|
+
assert.ok(calls.length >= 1, 'Should have at least 1 call');
|
|
1775
|
+
} finally {
|
|
1776
|
+
cleanupProject();
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it('smart command returns function with dependencies', () => {
|
|
1781
|
+
setupProject();
|
|
1782
|
+
try {
|
|
1783
|
+
const smart = index.smart('main');
|
|
1784
|
+
assert.ok(smart, 'Should return smart result');
|
|
1785
|
+
assert.ok(smart.target, 'Should have target');
|
|
1786
|
+
assert.strictEqual(smart.target.name, 'main', 'Target should be main');
|
|
1787
|
+
assert.ok(smart.target.code, 'Should have code');
|
|
1788
|
+
assert.ok(Array.isArray(smart.dependencies), 'Should have dependencies array');
|
|
1789
|
+
} finally {
|
|
1790
|
+
cleanupProject();
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it('trace command returns call tree', () => {
|
|
1795
|
+
setupProject();
|
|
1796
|
+
try {
|
|
1797
|
+
const trace = index.trace('main', { depth: 2 });
|
|
1798
|
+
assert.ok(trace, 'Should return trace result');
|
|
1799
|
+
assert.strictEqual(trace.root, 'main', 'Should be for main');
|
|
1800
|
+
assert.ok(trace.tree, 'Should have tree');
|
|
1801
|
+
} finally {
|
|
1802
|
+
cleanupProject();
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it('related command finds related functions', () => {
|
|
1807
|
+
setupProject();
|
|
1808
|
+
try {
|
|
1809
|
+
const related = index.related('main');
|
|
1810
|
+
assert.ok(related, 'Should return related result');
|
|
1811
|
+
assert.ok(Array.isArray(related.sameFile), 'Should have sameFile array');
|
|
1812
|
+
} finally {
|
|
1813
|
+
cleanupProject();
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it('imports command returns file imports', () => {
|
|
1818
|
+
setupProject();
|
|
1819
|
+
try {
|
|
1820
|
+
const imports = index.imports('test/main.test.js');
|
|
1821
|
+
assert.ok(Array.isArray(imports), 'Should return array');
|
|
1822
|
+
// Test file imports from src/main
|
|
1823
|
+
const hasMainImport = imports.some(i =>
|
|
1824
|
+
i.module && i.module.includes('main')
|
|
1825
|
+
);
|
|
1826
|
+
assert.ok(hasMainImport, 'Should find import from main');
|
|
1827
|
+
} finally {
|
|
1828
|
+
cleanupProject();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
it('exporters command returns files that import a module', () => {
|
|
1833
|
+
setupProject();
|
|
1834
|
+
try {
|
|
1835
|
+
const exporters = index.exporters('src/main.js');
|
|
1836
|
+
assert.ok(Array.isArray(exporters), 'Should return array');
|
|
1837
|
+
} finally {
|
|
1838
|
+
cleanupProject();
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
it('fileExports command returns module exports', () => {
|
|
1843
|
+
setupProject();
|
|
1844
|
+
try {
|
|
1845
|
+
const exports = index.fileExports('src/main.js');
|
|
1846
|
+
assert.ok(Array.isArray(exports), 'Should return array');
|
|
1847
|
+
const exportNames = exports.map(e => e.name);
|
|
1848
|
+
assert.ok(exportNames.includes('main'), 'Should export main');
|
|
1849
|
+
assert.ok(exportNames.includes('helper'), 'Should export helper');
|
|
1850
|
+
} finally {
|
|
1851
|
+
cleanupProject();
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
it('api command returns public/exported symbols', () => {
|
|
1856
|
+
setupProject();
|
|
1857
|
+
try {
|
|
1858
|
+
const api = index.api();
|
|
1859
|
+
assert.ok(Array.isArray(api), 'Should return array');
|
|
1860
|
+
} finally {
|
|
1861
|
+
cleanupProject();
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
it('plan command analyzes refactoring impact', () => {
|
|
1866
|
+
setupProject();
|
|
1867
|
+
try {
|
|
1868
|
+
const plan = index.plan('helper', { addParam: 'options' });
|
|
1869
|
+
assert.ok(plan, 'Should return plan');
|
|
1870
|
+
assert.ok(plan.function === 'helper', 'Should be for helper');
|
|
1871
|
+
} finally {
|
|
1872
|
+
cleanupProject();
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it('verify command checks call site consistency', () => {
|
|
1877
|
+
setupProject();
|
|
1878
|
+
try {
|
|
1879
|
+
const verify = index.verify('helper');
|
|
1880
|
+
assert.ok(verify, 'Should return verify result');
|
|
1881
|
+
assert.ok(typeof verify.totalCalls === 'number', 'Should have totalCalls');
|
|
1882
|
+
} finally {
|
|
1883
|
+
cleanupProject();
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
describe('JSON output format', () => {
|
|
1889
|
+
it('find returns valid JSON structure', () => {
|
|
1890
|
+
const index = new ProjectIndex('.');
|
|
1891
|
+
index.build(null, { quiet: true });
|
|
1892
|
+
|
|
1893
|
+
const found = index.find('parse');
|
|
1894
|
+
assert.ok(Array.isArray(found), 'Should be array');
|
|
1895
|
+
if (found.length > 0) {
|
|
1896
|
+
assert.ok(found[0].name, 'Should have name');
|
|
1897
|
+
assert.ok(found[0].file || found[0].relativePath, 'Should have file info');
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
it('usages returns valid JSON structure', () => {
|
|
1902
|
+
const index = new ProjectIndex('.');
|
|
1903
|
+
index.build(null, { quiet: true });
|
|
1904
|
+
|
|
1905
|
+
const usages = index.usages('parse');
|
|
1906
|
+
assert.ok(Array.isArray(usages), 'Should be array');
|
|
1907
|
+
if (usages.length > 0) {
|
|
1908
|
+
assert.ok(typeof usages[0].isDefinition === 'boolean', 'Should have isDefinition');
|
|
1909
|
+
assert.ok(usages[0].usageType || usages[0].isDefinition, 'Should have usageType or isDefinition');
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
it('stats returns valid JSON structure', () => {
|
|
1914
|
+
const index = new ProjectIndex('.');
|
|
1915
|
+
index.build(null, { quiet: true });
|
|
1916
|
+
|
|
1917
|
+
const stats = index.getStats();
|
|
1918
|
+
assert.ok(stats.root, 'Should have root');
|
|
1919
|
+
assert.ok(typeof stats.files === 'number', 'Should have files count');
|
|
1920
|
+
assert.ok(typeof stats.symbols === 'number', 'Should have symbols count');
|
|
1921
|
+
assert.ok(stats.byLanguage, 'Should have byLanguage');
|
|
1922
|
+
assert.ok(stats.byType, 'Should have byType');
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
// ============================================================================
|
|
1927
|
+
// CACHE STALENESS REGRESSION TESTS
|
|
1928
|
+
// ============================================================================
|
|
1929
|
+
|
|
1930
|
+
describe('Cache staleness handling', () => {
|
|
1931
|
+
it('should not create duplicate symbols when cache is stale', () => {
|
|
1932
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-cache-test-${Date.now()}`);
|
|
1933
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1934
|
+
|
|
1935
|
+
// Create initial file
|
|
1936
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1937
|
+
function myFunc() {
|
|
1938
|
+
return 42;
|
|
1939
|
+
}
|
|
1940
|
+
module.exports = { myFunc };
|
|
1941
|
+
`);
|
|
1942
|
+
|
|
1943
|
+
try {
|
|
1944
|
+
// Build initial index
|
|
1945
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
1946
|
+
index1.build('**/*.js', { quiet: true });
|
|
1947
|
+
|
|
1948
|
+
// Save cache
|
|
1949
|
+
const cacheDir = path.join(tmpDir, '.ucn-cache');
|
|
1950
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1951
|
+
index1.saveCache(path.join(cacheDir, 'index.json'));
|
|
1952
|
+
|
|
1953
|
+
// Verify initial state - should have exactly 1 symbol
|
|
1954
|
+
const found1 = index1.find('myFunc');
|
|
1955
|
+
assert.strictEqual(found1.length, 1, 'Should find exactly 1 symbol initially');
|
|
1956
|
+
|
|
1957
|
+
// Modify the file to make cache stale
|
|
1958
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1959
|
+
function myFunc() {
|
|
1960
|
+
return 43; // modified
|
|
1961
|
+
}
|
|
1962
|
+
module.exports = { myFunc };
|
|
1963
|
+
`);
|
|
1964
|
+
|
|
1965
|
+
// Create new index, load cache, detect stale, and rebuild with forceRebuild
|
|
1966
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
1967
|
+
const loaded = index2.loadCache(path.join(cacheDir, 'index.json'));
|
|
1968
|
+
assert.ok(loaded, 'Cache should load');
|
|
1969
|
+
|
|
1970
|
+
const stale = index2.isCacheStale();
|
|
1971
|
+
assert.ok(stale, 'Cache should be stale after file modification');
|
|
1972
|
+
|
|
1973
|
+
// This is the key fix: forceRebuild clears maps before rebuilding
|
|
1974
|
+
index2.build('**/*.js', { quiet: true, forceRebuild: true });
|
|
1975
|
+
|
|
1976
|
+
// Should still have exactly 1 symbol, not duplicates
|
|
1977
|
+
const found2 = index2.find('myFunc');
|
|
1978
|
+
assert.strictEqual(found2.length, 1, 'Should still find exactly 1 symbol after stale rebuild (no duplicates)');
|
|
1979
|
+
} finally {
|
|
1980
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
it('should create duplicates WITHOUT forceRebuild (demonstrates the bug)', () => {
|
|
1985
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-cache-bug-${Date.now()}`);
|
|
1986
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1987
|
+
|
|
1988
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
1989
|
+
function testFunc() { return 1; }
|
|
1990
|
+
module.exports = { testFunc };
|
|
1991
|
+
`);
|
|
1992
|
+
|
|
1993
|
+
try {
|
|
1994
|
+
// Build and cache
|
|
1995
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
1996
|
+
index1.build('**/*.js', { quiet: true });
|
|
1997
|
+
|
|
1998
|
+
const cacheDir = path.join(tmpDir, '.ucn-cache');
|
|
1999
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
2000
|
+
index1.saveCache(path.join(cacheDir, 'index.json'));
|
|
2001
|
+
|
|
2002
|
+
// Modify file
|
|
2003
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
2004
|
+
function testFunc() { return 2; }
|
|
2005
|
+
module.exports = { testFunc };
|
|
2006
|
+
`);
|
|
2007
|
+
|
|
2008
|
+
// Load cache and rebuild WITHOUT forceRebuild
|
|
2009
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
2010
|
+
index2.loadCache(path.join(cacheDir, 'index.json'));
|
|
2011
|
+
index2.build('**/*.js', { quiet: true }); // No forceRebuild!
|
|
2012
|
+
|
|
2013
|
+
// Without the fix, this would create duplicates
|
|
2014
|
+
const found = index2.find('testFunc');
|
|
2015
|
+
// This test documents the expected behavior with forceRebuild
|
|
2016
|
+
// Without it, duplicates could appear
|
|
2017
|
+
assert.ok(found.length >= 1, 'Should find at least 1 symbol');
|
|
2018
|
+
} finally {
|
|
2019
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
// ============================================================================
|
|
2025
|
+
// AUTO-ROUTING REGRESSION TESTS
|
|
2026
|
+
// ============================================================================
|
|
2027
|
+
|
|
2028
|
+
describe('Auto-routing file commands to project mode', () => {
|
|
2029
|
+
it('should handle imports command on file path', () => {
|
|
2030
|
+
const index = new ProjectIndex('.');
|
|
2031
|
+
index.build(null, { quiet: true });
|
|
2032
|
+
|
|
2033
|
+
// Test that imports works with a file path
|
|
2034
|
+
const imports = index.imports('cli/index.js');
|
|
2035
|
+
assert.ok(Array.isArray(imports), 'Should return imports array');
|
|
2036
|
+
assert.ok(imports.length > 0, 'Should have some imports');
|
|
2037
|
+
// Check structure of import entry
|
|
2038
|
+
const hasInternal = imports.some(i => !i.isExternal);
|
|
2039
|
+
const hasExternal = imports.some(i => i.isExternal);
|
|
2040
|
+
assert.ok(hasInternal || hasExternal, 'Should have internal or external imports');
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
it('should handle exporters command on file path', () => {
|
|
2044
|
+
const index = new ProjectIndex('.');
|
|
2045
|
+
index.build(null, { quiet: true });
|
|
2046
|
+
|
|
2047
|
+
// Test that exporters works with a file path
|
|
2048
|
+
const exporters = index.exporters('core/parser.js');
|
|
2049
|
+
assert.ok(Array.isArray(exporters), 'Should return exporters array');
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
it('should handle graph command on file path', () => {
|
|
2053
|
+
const index = new ProjectIndex('.');
|
|
2054
|
+
index.build(null, { quiet: true });
|
|
2055
|
+
|
|
2056
|
+
// Test that graph works with a file path
|
|
2057
|
+
const graph = index.graph('cli/index.js', { direction: 'both', maxDepth: 2 });
|
|
2058
|
+
assert.ok(graph, 'Should return graph result');
|
|
2059
|
+
assert.ok(graph.nodes, 'Should have nodes');
|
|
2060
|
+
assert.ok(graph.edges, 'Should have edges');
|
|
2061
|
+
});
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// ============================================================================
|
|
2065
|
+
// HELPER FUNCTION TESTS
|
|
2066
|
+
// ============================================================================
|
|
2067
|
+
|
|
2068
|
+
describe('CLI helper functions', () => {
|
|
2069
|
+
// These test the helper behavior indirectly through the API
|
|
2070
|
+
// The actual requireArg and printOutput are CLI-internal
|
|
2071
|
+
|
|
2072
|
+
it('find should work with various options', () => {
|
|
2073
|
+
const index = new ProjectIndex('.');
|
|
2074
|
+
index.build(null, { quiet: true });
|
|
2075
|
+
|
|
2076
|
+
// Test exact match
|
|
2077
|
+
const exactResults = index.find('parse', { exact: true });
|
|
2078
|
+
assert.ok(Array.isArray(exactResults), 'Should return array');
|
|
2079
|
+
|
|
2080
|
+
// Test file filter
|
|
2081
|
+
const filteredResults = index.find('parse', { file: 'parser' });
|
|
2082
|
+
assert.ok(Array.isArray(filteredResults), 'Should return array with file filter');
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it('context should return proper structure', () => {
|
|
2086
|
+
const index = new ProjectIndex('.');
|
|
2087
|
+
index.build(null, { quiet: true });
|
|
2088
|
+
|
|
2089
|
+
const ctx = index.context('parse');
|
|
2090
|
+
assert.ok(ctx, 'Should return context');
|
|
2091
|
+
assert.ok(ctx.function === 'parse', 'Should have function name');
|
|
2092
|
+
assert.ok(Array.isArray(ctx.callers), 'Should have callers array');
|
|
2093
|
+
assert.ok(Array.isArray(ctx.callees), 'Should have callees array');
|
|
2094
|
+
});
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
// ============================================================================
|
|
2098
|
+
// REGRESSION TESTS: isInsideString fixes
|
|
2099
|
+
// ============================================================================
|
|
2100
|
+
|
|
2101
|
+
describe('Regression: isInsideString string literal detection', () => {
|
|
2102
|
+
it('should NOT treat function calls between string literals as inside strings', () => {
|
|
2103
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-string-between-${Date.now()}`);
|
|
2104
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2105
|
+
|
|
2106
|
+
try {
|
|
2107
|
+
// Code with function call between string literals (the original bug case)
|
|
2108
|
+
// Uses simpler escaping to avoid test complexity
|
|
2109
|
+
const code = [
|
|
2110
|
+
'function helper(x) { return x; }',
|
|
2111
|
+
'',
|
|
2112
|
+
'function buildMessage(name) {',
|
|
2113
|
+
" const msg = 'Hello ' + helper(name) + '!';",
|
|
2114
|
+
' return msg;',
|
|
2115
|
+
'}',
|
|
2116
|
+
'',
|
|
2117
|
+
'module.exports = { helper, buildMessage };'
|
|
2118
|
+
].join('\n');
|
|
2119
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), code);
|
|
2120
|
+
|
|
2121
|
+
const index = new ProjectIndex(tmpDir);
|
|
2122
|
+
index.build('**/*.js', { quiet: true });
|
|
2123
|
+
|
|
2124
|
+
const usages = index.usages('helper');
|
|
2125
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2126
|
+
|
|
2127
|
+
// Should find the call in buildMessage (not be confused by surrounding strings)
|
|
2128
|
+
assert.ok(calls.length >= 1, 'Should find call to helper in buildMessage');
|
|
2129
|
+
assert.ok(calls.some(c => c.content && c.content.includes('helper(name)')),
|
|
2130
|
+
'Should include the actual call site');
|
|
2131
|
+
} finally {
|
|
2132
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
it('should correctly identify function names inside string literals', () => {
|
|
2137
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-string-inside-${Date.now()}`);
|
|
2138
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2139
|
+
|
|
2140
|
+
try {
|
|
2141
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
2142
|
+
function myFunc() {
|
|
2143
|
+
return 42;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// These should NOT be counted as calls
|
|
2147
|
+
const str1 = 'myFunc is a function';
|
|
2148
|
+
const str2 = "call myFunc()";
|
|
2149
|
+
const str3 = \`myFunc documentation\`;
|
|
2150
|
+
|
|
2151
|
+
// This should be counted as a call
|
|
2152
|
+
const result = myFunc();
|
|
2153
|
+
|
|
2154
|
+
module.exports = { myFunc };
|
|
2155
|
+
`);
|
|
2156
|
+
|
|
2157
|
+
const index = new ProjectIndex(tmpDir);
|
|
2158
|
+
index.build('**/*.js', { quiet: true });
|
|
2159
|
+
|
|
2160
|
+
const usages = index.usages('myFunc');
|
|
2161
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2162
|
+
|
|
2163
|
+
// Should only find the actual call, not the string literals
|
|
2164
|
+
assert.strictEqual(calls.length, 1, 'Should find exactly 1 call (not string literals)');
|
|
2165
|
+
assert.ok(calls[0].content.includes('const result = myFunc()'),
|
|
2166
|
+
'Should only include the actual function call');
|
|
2167
|
+
} finally {
|
|
2168
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
it('should correctly handle template literal expressions ${...}', () => {
|
|
2173
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-template-expr-${Date.now()}`);
|
|
2174
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2175
|
+
|
|
2176
|
+
try {
|
|
2177
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
2178
|
+
function formatValue(x) {
|
|
2179
|
+
return x.toFixed(2);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// This IS a call (inside template expression)
|
|
2183
|
+
const msg1 = \`Result: \${formatValue(42)}\`;
|
|
2184
|
+
|
|
2185
|
+
// This is NOT a call (plain text in template)
|
|
2186
|
+
const msg2 = \`The function formatValue is useful\`;
|
|
2187
|
+
|
|
2188
|
+
// Nested expression IS a call
|
|
2189
|
+
const msg3 = \`Value: \${flag ? formatValue(x) : 'none'}\`;
|
|
2190
|
+
|
|
2191
|
+
module.exports = { formatValue };
|
|
2192
|
+
`);
|
|
2193
|
+
|
|
2194
|
+
const index = new ProjectIndex(tmpDir);
|
|
2195
|
+
index.build('**/*.js', { quiet: true });
|
|
2196
|
+
|
|
2197
|
+
const usages = index.usages('formatValue');
|
|
2198
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2199
|
+
|
|
2200
|
+
// Should find the 2 calls in template expressions
|
|
2201
|
+
assert.strictEqual(calls.length, 2, 'Should find exactly 2 calls (template expressions)');
|
|
2202
|
+
assert.ok(calls.some(c => c.content.includes('${formatValue(42)}')),
|
|
2203
|
+
'Should include first template expression call');
|
|
2204
|
+
assert.ok(calls.some(c => c.content.includes('${flag ? formatValue(x)')),
|
|
2205
|
+
'Should include nested template expression call');
|
|
2206
|
+
} finally {
|
|
2207
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
it('should handle escaped quotes correctly', () => {
|
|
2212
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-escaped-quotes-${Date.now()}`);
|
|
2213
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2214
|
+
|
|
2215
|
+
try {
|
|
2216
|
+
// Use array join to avoid escaping issues in template literals
|
|
2217
|
+
const code = [
|
|
2218
|
+
'function process(x) {',
|
|
2219
|
+
' return x;',
|
|
2220
|
+
'}',
|
|
2221
|
+
'',
|
|
2222
|
+
'// Escaped quotes should not confuse the parser',
|
|
2223
|
+
"const str = 'Don\\'t call process() here'; // NOT a call (inside string with escaped quote)",
|
|
2224
|
+
'const result = process(42); // IS a call',
|
|
2225
|
+
'',
|
|
2226
|
+
'module.exports = { process };'
|
|
2227
|
+
].join('\n');
|
|
2228
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), code);
|
|
2229
|
+
|
|
2230
|
+
const index = new ProjectIndex(tmpDir);
|
|
2231
|
+
index.build('**/*.js', { quiet: true });
|
|
2232
|
+
|
|
2233
|
+
const usages = index.usages('process');
|
|
2234
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2235
|
+
|
|
2236
|
+
// Should only find the actual call
|
|
2237
|
+
assert.strictEqual(calls.length, 1, 'Should find exactly 1 call');
|
|
2238
|
+
assert.ok(calls[0].content.includes('const result = process(42)'),
|
|
2239
|
+
'Should only include the actual function call');
|
|
2240
|
+
} finally {
|
|
2241
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
it('should handle mixed quote types correctly', () => {
|
|
2246
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-mixed-quotes-${Date.now()}`);
|
|
2247
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2248
|
+
|
|
2249
|
+
try {
|
|
2250
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), `
|
|
2251
|
+
function helper(x) {
|
|
2252
|
+
return x;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// Double quotes containing single quotes
|
|
2256
|
+
const a = "don't call helper()"; // NOT a call
|
|
2257
|
+
|
|
2258
|
+
// Single quotes containing double quotes
|
|
2259
|
+
const b = '"helper" is the name'; // NOT a call
|
|
2260
|
+
|
|
2261
|
+
// Template containing both
|
|
2262
|
+
const c = \`"helper" and 'helper'\`; // NOT a call
|
|
2263
|
+
|
|
2264
|
+
// Actual call
|
|
2265
|
+
const d = helper(1); // IS a call
|
|
2266
|
+
|
|
2267
|
+
module.exports = { helper };
|
|
2268
|
+
`);
|
|
2269
|
+
|
|
2270
|
+
const index = new ProjectIndex(tmpDir);
|
|
2271
|
+
index.build('**/*.js', { quiet: true });
|
|
2272
|
+
|
|
2273
|
+
const usages = index.usages('helper');
|
|
2274
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2275
|
+
|
|
2276
|
+
// Should only find the actual call
|
|
2277
|
+
assert.strictEqual(calls.length, 1, 'Should find exactly 1 call');
|
|
2278
|
+
} finally {
|
|
2279
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
// ============================================================================
|
|
2285
|
+
// REGRESSION TESTS: deadcode detection accuracy
|
|
2286
|
+
// ============================================================================
|
|
2287
|
+
|
|
2288
|
+
describe('Regression: deadcode detection accuracy', () => {
|
|
2289
|
+
it('should NOT report functions used in concatenated string patterns as dead', () => {
|
|
2290
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-deadcode-regex-${Date.now()}`);
|
|
2291
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2292
|
+
|
|
2293
|
+
try {
|
|
2294
|
+
// Simpler test case without complex regex escaping
|
|
2295
|
+
const code = [
|
|
2296
|
+
'function helper(x) { return x; }',
|
|
2297
|
+
'',
|
|
2298
|
+
'function buildMessage(name) {',
|
|
2299
|
+
" return 'Hello ' + helper(name) + '!';",
|
|
2300
|
+
'}',
|
|
2301
|
+
'',
|
|
2302
|
+
'module.exports = { helper, buildMessage };'
|
|
2303
|
+
].join('\n');
|
|
2304
|
+
fs.writeFileSync(path.join(tmpDir, 'utils.js'), code);
|
|
2305
|
+
|
|
2306
|
+
const index = new ProjectIndex(tmpDir);
|
|
2307
|
+
index.build('**/*.js', { quiet: true });
|
|
2308
|
+
|
|
2309
|
+
const deadcode = index.deadcode();
|
|
2310
|
+
const deadNames = deadcode.map(d => d.name);
|
|
2311
|
+
|
|
2312
|
+
// helper is used in buildMessage, should NOT be dead
|
|
2313
|
+
assert.ok(!deadNames.includes('helper'),
|
|
2314
|
+
'helper should NOT be reported as dead (it is used in buildMessage)');
|
|
2315
|
+
} finally {
|
|
2316
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
it('should NOT report functions used in template literal expressions as dead', () => {
|
|
2321
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-deadcode-template-${Date.now()}`);
|
|
2322
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2323
|
+
|
|
2324
|
+
try {
|
|
2325
|
+
fs.writeFileSync(path.join(tmpDir, 'format.js'), `
|
|
2326
|
+
function formatScore(score) {
|
|
2327
|
+
return score.toFixed(1);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function displayResult(data) {
|
|
2331
|
+
console.log(\`Score: \${formatScore(data.value)}\`);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
displayResult({ value: 42 });
|
|
2335
|
+
module.exports = { formatScore, displayResult };
|
|
2336
|
+
`);
|
|
2337
|
+
|
|
2338
|
+
const index = new ProjectIndex(tmpDir);
|
|
2339
|
+
index.build('**/*.js', { quiet: true });
|
|
2340
|
+
|
|
2341
|
+
const deadcode = index.deadcode();
|
|
2342
|
+
const deadNames = deadcode.map(d => d.name);
|
|
2343
|
+
|
|
2344
|
+
// formatScore is used inside template expression, should NOT be dead
|
|
2345
|
+
assert.ok(!deadNames.includes('formatScore'),
|
|
2346
|
+
'formatScore should NOT be reported as dead (used in template expression)');
|
|
2347
|
+
} finally {
|
|
2348
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
it('should correctly identify actually unused functions', () => {
|
|
2353
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-deadcode-real-${Date.now()}`);
|
|
2354
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2355
|
+
|
|
2356
|
+
try {
|
|
2357
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
2358
|
+
function usedFunction() {
|
|
2359
|
+
return 42;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function unusedFunction() {
|
|
2363
|
+
return 'never called';
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
function anotherUnused() {
|
|
2367
|
+
return 'also never called';
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
const result = usedFunction();
|
|
2371
|
+
console.log(result);
|
|
2372
|
+
`);
|
|
2373
|
+
|
|
2374
|
+
const index = new ProjectIndex(tmpDir);
|
|
2375
|
+
index.build('**/*.js', { quiet: true });
|
|
2376
|
+
|
|
2377
|
+
const deadcode = index.deadcode();
|
|
2378
|
+
const deadNames = deadcode.map(d => d.name);
|
|
2379
|
+
|
|
2380
|
+
// Check correct identification
|
|
2381
|
+
assert.ok(!deadNames.includes('usedFunction'), 'usedFunction should NOT be dead');
|
|
2382
|
+
assert.ok(deadNames.includes('unusedFunction'), 'unusedFunction SHOULD be dead');
|
|
2383
|
+
assert.ok(deadNames.includes('anotherUnused'), 'anotherUnused SHOULD be dead');
|
|
2384
|
+
} finally {
|
|
2385
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
// ============================================================================
|
|
2391
|
+
// REGRESSION TESTS: regex global flag bug
|
|
2392
|
+
// ============================================================================
|
|
2393
|
+
|
|
2394
|
+
describe('Regression: regex global flag bug', () => {
|
|
2395
|
+
it('usages should find ALL matching lines (not alternate due to g flag)', () => {
|
|
2396
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-regex-g-${Date.now()}`);
|
|
2397
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2398
|
+
|
|
2399
|
+
try {
|
|
2400
|
+
// Create a file with multiple calls to the same function
|
|
2401
|
+
const code = [
|
|
2402
|
+
'function helper(x) { return x; }',
|
|
2403
|
+
'',
|
|
2404
|
+
'const a = helper(1);',
|
|
2405
|
+
'const b = helper(2);',
|
|
2406
|
+
'const c = helper(3);',
|
|
2407
|
+
'const d = helper(4);',
|
|
2408
|
+
'const e = helper(5);',
|
|
2409
|
+
'',
|
|
2410
|
+
'module.exports = { helper };'
|
|
2411
|
+
].join('\n');
|
|
2412
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), code);
|
|
2413
|
+
|
|
2414
|
+
const index = new ProjectIndex(tmpDir);
|
|
2415
|
+
index.build('**/*.js', { quiet: true });
|
|
2416
|
+
|
|
2417
|
+
const usages = index.usages('helper');
|
|
2418
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
2419
|
+
|
|
2420
|
+
// Should find ALL 5 calls, not just some due to g flag lastIndex bug
|
|
2421
|
+
assert.strictEqual(calls.length, 5, 'Should find all 5 calls to helper');
|
|
2422
|
+
} finally {
|
|
2423
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
it('findCallers should find ALL callers (not alternate due to g flag)', () => {
|
|
2428
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-regex-callers-${Date.now()}`);
|
|
2429
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2430
|
+
|
|
2431
|
+
try {
|
|
2432
|
+
const code = [
|
|
2433
|
+
'function target() { return 42; }',
|
|
2434
|
+
'',
|
|
2435
|
+
'function caller1() { return target(); }',
|
|
2436
|
+
'function caller2() { return target(); }',
|
|
2437
|
+
'function caller3() { return target(); }',
|
|
2438
|
+
'function caller4() { return target(); }',
|
|
2439
|
+
'',
|
|
2440
|
+
'module.exports = { target };'
|
|
2441
|
+
].join('\n');
|
|
2442
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), code);
|
|
2443
|
+
|
|
2444
|
+
const index = new ProjectIndex(tmpDir);
|
|
2445
|
+
index.build('**/*.js', { quiet: true });
|
|
2446
|
+
|
|
2447
|
+
const ctx = index.context('target');
|
|
2448
|
+
|
|
2449
|
+
// Should find ALL 4 callers
|
|
2450
|
+
assert.strictEqual(ctx.callers.length, 4, 'Should find all 4 callers');
|
|
2451
|
+
} finally {
|
|
2452
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
it('search should find ALL matching lines', () => {
|
|
2457
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-regex-search-${Date.now()}`);
|
|
2458
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2459
|
+
|
|
2460
|
+
try {
|
|
2461
|
+
const code = [
|
|
2462
|
+
'// TODO: fix this',
|
|
2463
|
+
'// TODO: add tests',
|
|
2464
|
+
'// TODO: refactor',
|
|
2465
|
+
'// TODO: document',
|
|
2466
|
+
'// TODO: cleanup'
|
|
2467
|
+
].join('\n');
|
|
2468
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), code);
|
|
2469
|
+
|
|
2470
|
+
const index = new ProjectIndex(tmpDir);
|
|
2471
|
+
index.build('**/*.js', { quiet: true });
|
|
2472
|
+
|
|
2473
|
+
const results = index.search('TODO');
|
|
2474
|
+
const matches = results[0]?.matches || [];
|
|
2475
|
+
|
|
2476
|
+
// Should find ALL 5 TODOs
|
|
2477
|
+
assert.strictEqual(matches.length, 5, 'Should find all 5 TODO comments');
|
|
2478
|
+
} finally {
|
|
2479
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
// ============================================================================
|
|
2485
|
+
// REGRESSION TESTS: Bug fixes from code review
|
|
2486
|
+
// ============================================================================
|
|
2487
|
+
|
|
2488
|
+
describe('Regression: findCallers should filter comment lines', () => {
|
|
2489
|
+
it('should not include comment lines as callers', () => {
|
|
2490
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-caller-comments-${Date.now()}`);
|
|
2491
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2492
|
+
|
|
2493
|
+
try {
|
|
2494
|
+
// Create a file with a function and comments mentioning it
|
|
2495
|
+
fs.writeFileSync(path.join(tmpDir, 'util.js'), `
|
|
2496
|
+
function processData(input) {
|
|
2497
|
+
return input.toUpperCase();
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function main() {
|
|
2501
|
+
// Call processData() to handle input
|
|
2502
|
+
const result = processData("hello");
|
|
2503
|
+
// processData() should not be included as a caller from this comment
|
|
2504
|
+
return result;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
module.exports = { processData, main };
|
|
2508
|
+
`);
|
|
2509
|
+
|
|
2510
|
+
const index = new ProjectIndex(tmpDir);
|
|
2511
|
+
index.build('**/*.js', { quiet: true });
|
|
2512
|
+
|
|
2513
|
+
const ctx = index.context('processData');
|
|
2514
|
+
const callers = ctx.callers;
|
|
2515
|
+
|
|
2516
|
+
// Should find main as a caller, but not the comment lines
|
|
2517
|
+
const callerLines = callers.map(c => c.line);
|
|
2518
|
+
|
|
2519
|
+
// Line 8 is the actual call
|
|
2520
|
+
assert.ok(callerLines.includes(8), 'Should include actual call on line 8');
|
|
2521
|
+
|
|
2522
|
+
// Comment lines should NOT be included
|
|
2523
|
+
const commentCallers = callers.filter(c => c.content.trim().startsWith('//'));
|
|
2524
|
+
assert.strictEqual(commentCallers.length, 0, 'Should not include comment lines as callers');
|
|
2525
|
+
} finally {
|
|
2526
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
describe('Regression: findCallees should filter comments and strings', () => {
|
|
2532
|
+
it('should not count function names in comments as callees', () => {
|
|
2533
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-callee-comments-${Date.now()}`);
|
|
2534
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2535
|
+
|
|
2536
|
+
try {
|
|
2537
|
+
fs.writeFileSync(path.join(tmpDir, 'util.js'), `
|
|
2538
|
+
function helperFunction() {
|
|
2539
|
+
return 42;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function mainFunction() {
|
|
2543
|
+
// helperFunction() does something useful
|
|
2544
|
+
// We call helperFunction() below
|
|
2545
|
+
const result = helperFunction();
|
|
2546
|
+
return result;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
module.exports = { helperFunction, mainFunction };
|
|
2550
|
+
`);
|
|
2551
|
+
|
|
2552
|
+
const index = new ProjectIndex(tmpDir);
|
|
2553
|
+
index.build('**/*.js', { quiet: true });
|
|
2554
|
+
|
|
2555
|
+
const mainDef = index.symbols.get('mainFunction')?.[0];
|
|
2556
|
+
const callees = index.findCallees(mainDef);
|
|
2557
|
+
|
|
2558
|
+
// helperFunction should only be counted once (from the actual call)
|
|
2559
|
+
const helperCallee = callees.find(c => c.name === 'helperFunction');
|
|
2560
|
+
assert.ok(helperCallee, 'Should find helperFunction as a callee');
|
|
2561
|
+
assert.strictEqual(helperCallee.callCount, 1, 'Should count helperFunction only once (not from comments)');
|
|
2562
|
+
} finally {
|
|
2563
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
describe('Regression: search should escape regex special characters', () => {
|
|
2569
|
+
it('should not crash when searching for regex special chars', () => {
|
|
2570
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-search-regex-${Date.now()}`);
|
|
2571
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2572
|
+
|
|
2573
|
+
try {
|
|
2574
|
+
fs.writeFileSync(path.join(tmpDir, 'code.js'), `
|
|
2575
|
+
function process(x) {
|
|
2576
|
+
return x + 1;
|
|
2577
|
+
}
|
|
2578
|
+
// Call process(x) here
|
|
2579
|
+
const result = process(42);
|
|
2580
|
+
`);
|
|
2581
|
+
|
|
2582
|
+
const index = new ProjectIndex(tmpDir);
|
|
2583
|
+
index.build('**/*.js', { quiet: true });
|
|
2584
|
+
|
|
2585
|
+
// These should not throw
|
|
2586
|
+
assert.doesNotThrow(() => index.search('process('));
|
|
2587
|
+
assert.doesNotThrow(() => index.search('(x)'));
|
|
2588
|
+
assert.doesNotThrow(() => index.search('[test]'));
|
|
2589
|
+
assert.doesNotThrow(() => index.search('x + 1'));
|
|
2590
|
+
|
|
2591
|
+
const results = index.search('process(');
|
|
2592
|
+
assert.ok(results.length > 0, 'Should find matches for process(');
|
|
2593
|
+
} finally {
|
|
2594
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
describe('Regression: trace depth=0 should work correctly', () => {
|
|
2600
|
+
it('should show only root function when depth=0', () => {
|
|
2601
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-trace-depth-${Date.now()}`);
|
|
2602
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2603
|
+
|
|
2604
|
+
try {
|
|
2605
|
+
fs.writeFileSync(path.join(tmpDir, 'util.js'), `
|
|
2606
|
+
function helper() {
|
|
2607
|
+
return 1;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function main() {
|
|
2611
|
+
return helper();
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
module.exports = { helper, main };
|
|
2615
|
+
`);
|
|
2616
|
+
|
|
2617
|
+
const index = new ProjectIndex(tmpDir);
|
|
2618
|
+
index.build('**/*.js', { quiet: true });
|
|
2619
|
+
|
|
2620
|
+
const trace0 = index.trace('main', { depth: 0 });
|
|
2621
|
+
assert.ok(trace0, 'Should return trace result');
|
|
2622
|
+
assert.strictEqual(trace0.maxDepth, 0, 'maxDepth should be 0');
|
|
2623
|
+
assert.strictEqual(trace0.tree.children.length, 0, 'Should have no children with depth=0');
|
|
2624
|
+
|
|
2625
|
+
const trace1 = index.trace('main', { depth: 1 });
|
|
2626
|
+
assert.strictEqual(trace1.maxDepth, 1, 'maxDepth should be 1');
|
|
2627
|
+
assert.ok(trace1.tree.children.length > 0, 'Should have children with depth=1');
|
|
2628
|
+
} finally {
|
|
2629
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
describe('Regression: indent should be stored for --top-level filtering', () => {
|
|
2635
|
+
it('should store indent field in project index', () => {
|
|
2636
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-indent-${Date.now()}`);
|
|
2637
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2638
|
+
|
|
2639
|
+
try {
|
|
2640
|
+
fs.writeFileSync(path.join(tmpDir, 'code.js'), `
|
|
2641
|
+
function outer() {
|
|
2642
|
+
function inner() {
|
|
2643
|
+
return 1;
|
|
2644
|
+
}
|
|
2645
|
+
return inner();
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
module.exports = { outer };
|
|
2649
|
+
`);
|
|
2650
|
+
|
|
2651
|
+
const index = new ProjectIndex(tmpDir);
|
|
2652
|
+
index.build('**/*.js', { quiet: true });
|
|
2653
|
+
|
|
2654
|
+
const outerDef = index.symbols.get('outer')?.[0];
|
|
2655
|
+
const innerDef = index.symbols.get('inner')?.[0];
|
|
2656
|
+
|
|
2657
|
+
assert.ok(outerDef, 'Should find outer function');
|
|
2658
|
+
assert.ok(innerDef, 'Should find inner function');
|
|
2659
|
+
|
|
2660
|
+
// outer should have indent 0, inner should have indent > 0
|
|
2661
|
+
assert.strictEqual(outerDef.indent, 0, 'outer should have indent 0');
|
|
2662
|
+
assert.ok(innerDef.indent > 0, 'inner should have indent > 0');
|
|
2663
|
+
} finally {
|
|
2664
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
});
|
|
2668
|
+
|
|
2669
|
+
// ============================================================================
|
|
2670
|
+
// Multi-language Fixture Tests
|
|
2671
|
+
// Tests parsing of realistic code fixtures for all supported languages
|
|
2672
|
+
// ============================================================================
|
|
2673
|
+
|
|
2674
|
+
describe('Multi-language Fixtures: Python', () => {
|
|
2675
|
+
const fixturesPath = path.join(__dirname, 'fixtures', 'python');
|
|
2676
|
+
|
|
2677
|
+
it('should parse Python functions with type hints', () => {
|
|
2678
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.py'))) {
|
|
2679
|
+
// Skip if fixtures not created yet
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2684
|
+
index.build('**/*.py', { quiet: true });
|
|
2685
|
+
|
|
2686
|
+
// Should find functions
|
|
2687
|
+
assert.ok(index.symbols.has('create_task'), 'Should find create_task function');
|
|
2688
|
+
assert.ok(index.symbols.has('filter_by_status'), 'Should find filter_by_status function');
|
|
2689
|
+
|
|
2690
|
+
// Should find classes
|
|
2691
|
+
assert.ok(index.symbols.has('TaskManager'), 'Should find TaskManager class');
|
|
2692
|
+
assert.ok(index.symbols.has('DataService'), 'Should find DataService class');
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
it('should extract Python functions correctly', () => {
|
|
2696
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.py'))) {
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2701
|
+
index.build('**/*.py', { quiet: true });
|
|
2702
|
+
|
|
2703
|
+
const fnDefs = index.symbols.get('create_task');
|
|
2704
|
+
assert.ok(fnDefs && fnDefs.length > 0, 'Should find create_task');
|
|
2705
|
+
|
|
2706
|
+
// Use extractCode which takes a symbol definition
|
|
2707
|
+
const code = index.extractCode(fnDefs[0]);
|
|
2708
|
+
assert.ok(code, 'Should extract function code');
|
|
2709
|
+
assert.ok(code.includes('def create_task'), 'Code should contain function definition');
|
|
2710
|
+
});
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
describe('Multi-language Fixtures: Go', () => {
|
|
2714
|
+
const fixturesPath = path.join(__dirname, 'fixtures', 'go');
|
|
2715
|
+
|
|
2716
|
+
it('should parse Go functions and structs', () => {
|
|
2717
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.go'))) {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2722
|
+
index.build('**/*.go', { quiet: true });
|
|
2723
|
+
|
|
2724
|
+
// Should find functions
|
|
2725
|
+
assert.ok(index.symbols.has('NewTaskManager'), 'Should find NewTaskManager function');
|
|
2726
|
+
assert.ok(index.symbols.has('ValidateTask'), 'Should find ValidateTask function');
|
|
2727
|
+
|
|
2728
|
+
// Should find structs
|
|
2729
|
+
assert.ok(index.symbols.has('Task'), 'Should find Task struct');
|
|
2730
|
+
assert.ok(index.symbols.has('TaskManager'), 'Should find TaskManager struct');
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it('should parse Go methods on structs', () => {
|
|
2734
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.go'))) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2739
|
+
index.build('**/*.go', { quiet: true });
|
|
2740
|
+
|
|
2741
|
+
// Should find struct methods
|
|
2742
|
+
assert.ok(index.symbols.has('AddTask'), 'Should find AddTask method');
|
|
2743
|
+
assert.ok(index.symbols.has('GetTask'), 'Should find GetTask method');
|
|
2744
|
+
});
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
describe('Multi-language Fixtures: Rust', () => {
|
|
2748
|
+
const fixturesPath = path.join(__dirname, 'fixtures', 'rust');
|
|
2749
|
+
|
|
2750
|
+
it('should parse Rust functions and structs', () => {
|
|
2751
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.rs'))) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2756
|
+
index.build('**/*.rs', { quiet: true });
|
|
2757
|
+
|
|
2758
|
+
// Should find functions
|
|
2759
|
+
assert.ok(index.symbols.has('validate_task'), 'Should find validate_task function');
|
|
2760
|
+
assert.ok(index.symbols.has('create_task'), 'Should find create_task function');
|
|
2761
|
+
|
|
2762
|
+
// Should find structs
|
|
2763
|
+
assert.ok(index.symbols.has('Task'), 'Should find Task struct');
|
|
2764
|
+
assert.ok(index.symbols.has('TaskManager'), 'Should find TaskManager struct');
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
it('should parse Rust impl blocks', () => {
|
|
2768
|
+
if (!fs.existsSync(path.join(fixturesPath, 'main.rs'))) {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2773
|
+
index.build('**/*.rs', { quiet: true });
|
|
2774
|
+
|
|
2775
|
+
// Should find methods from impl blocks
|
|
2776
|
+
assert.ok(index.symbols.has('add_task'), 'Should find add_task method');
|
|
2777
|
+
assert.ok(index.symbols.has('get_task'), 'Should find get_task method');
|
|
2778
|
+
});
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
describe('Multi-language Fixtures: Java', () => {
|
|
2782
|
+
const fixturesPath = path.join(__dirname, 'fixtures', 'java');
|
|
2783
|
+
|
|
2784
|
+
it('should parse Java classes and methods', () => {
|
|
2785
|
+
if (!fs.existsSync(path.join(fixturesPath, 'Main.java'))) {
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2790
|
+
index.build('**/*.java', { quiet: true });
|
|
2791
|
+
|
|
2792
|
+
// Should find classes
|
|
2793
|
+
assert.ok(index.symbols.has('Main'), 'Should find Main class');
|
|
2794
|
+
assert.ok(index.symbols.has('DataService'), 'Should find DataService class');
|
|
2795
|
+
|
|
2796
|
+
// Should find methods
|
|
2797
|
+
assert.ok(index.symbols.has('createTask'), 'Should find createTask method');
|
|
2798
|
+
assert.ok(index.symbols.has('validateTask'), 'Should find validateTask method');
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
it('should parse Java inner classes', () => {
|
|
2802
|
+
if (!fs.existsSync(path.join(fixturesPath, 'Main.java'))) {
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const index = new ProjectIndex(fixturesPath);
|
|
2807
|
+
index.build('**/*.java', { quiet: true });
|
|
2808
|
+
|
|
2809
|
+
// Should find inner classes/enums
|
|
2810
|
+
assert.ok(index.symbols.has('Task'), 'Should find Task inner class');
|
|
2811
|
+
assert.ok(index.symbols.has('TaskManager'), 'Should find TaskManager inner class');
|
|
2812
|
+
});
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
describe('Regression: Project detection with language markers', () => {
|
|
2816
|
+
it('should detect Python project with pyproject.toml', () => {
|
|
2817
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-pyproject-${Date.now()}`);
|
|
2818
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2819
|
+
|
|
2820
|
+
try {
|
|
2821
|
+
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
|
|
2822
|
+
fs.writeFileSync(path.join(tmpDir, 'main.py'), 'def hello():\n pass');
|
|
2823
|
+
|
|
2824
|
+
const { detectProjectPattern } = require('../core/discovery');
|
|
2825
|
+
const pattern = detectProjectPattern(tmpDir);
|
|
2826
|
+
|
|
2827
|
+
// Pattern format is **/*.{py} or includes py in extensions
|
|
2828
|
+
assert.ok(pattern.includes('py'), 'Should detect Python files');
|
|
2829
|
+
} finally {
|
|
2830
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
|
|
2834
|
+
it('should detect Go project with go.mod', () => {
|
|
2835
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-gomod-${Date.now()}`);
|
|
2836
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2837
|
+
|
|
2838
|
+
try {
|
|
2839
|
+
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module test\ngo 1.21');
|
|
2840
|
+
fs.writeFileSync(path.join(tmpDir, 'main.go'), 'package main\nfunc main() {}');
|
|
2841
|
+
|
|
2842
|
+
const { detectProjectPattern } = require('../core/discovery');
|
|
2843
|
+
const pattern = detectProjectPattern(tmpDir);
|
|
2844
|
+
|
|
2845
|
+
assert.ok(pattern.includes('go'), 'Should detect Go files');
|
|
2846
|
+
} finally {
|
|
2847
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
it('should detect Rust project with Cargo.toml', () => {
|
|
2852
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-cargo-${Date.now()}`);
|
|
2853
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2854
|
+
|
|
2855
|
+
try {
|
|
2856
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"');
|
|
2857
|
+
fs.writeFileSync(path.join(tmpDir, 'main.rs'), 'fn main() {}');
|
|
2858
|
+
|
|
2859
|
+
const { detectProjectPattern } = require('../core/discovery');
|
|
2860
|
+
const pattern = detectProjectPattern(tmpDir);
|
|
2861
|
+
|
|
2862
|
+
assert.ok(pattern.includes('rs'), 'Should detect Rust files');
|
|
2863
|
+
} finally {
|
|
2864
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
it('should detect Java project with pom.xml', () => {
|
|
2869
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-pom-${Date.now()}`);
|
|
2870
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2871
|
+
|
|
2872
|
+
try {
|
|
2873
|
+
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
|
|
2874
|
+
fs.writeFileSync(path.join(tmpDir, 'Main.java'), 'public class Main {}');
|
|
2875
|
+
|
|
2876
|
+
const { detectProjectPattern } = require('../core/discovery');
|
|
2877
|
+
const pattern = detectProjectPattern(tmpDir);
|
|
2878
|
+
|
|
2879
|
+
assert.ok(pattern.includes('java'), 'Should detect Java files');
|
|
2880
|
+
} finally {
|
|
2881
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
// ============================================================================
|
|
2887
|
+
// REGRESSION: lines command should validate input
|
|
2888
|
+
// ============================================================================
|
|
2889
|
+
|
|
2890
|
+
describe('Regression: lines command should validate input', () => {
|
|
2891
|
+
it('should error on out-of-bounds line range', () => {
|
|
2892
|
+
const fixtureFile = path.join(__dirname, 'fixtures', 'javascript', 'main.js');
|
|
2893
|
+
const content = fs.readFileSync(fixtureFile, 'utf-8');
|
|
2894
|
+
const lineCount = content.split('\n').length;
|
|
2895
|
+
|
|
2896
|
+
// Run UCN with lines command that exceeds file length
|
|
2897
|
+
const { execSync } = require('child_process');
|
|
2898
|
+
const ucnPath = path.join(__dirname, '..', 'ucn.js');
|
|
2899
|
+
|
|
2900
|
+
try {
|
|
2901
|
+
execSync(`node ${ucnPath} ${fixtureFile} lines ${lineCount + 100}-${lineCount + 200}`, {
|
|
2902
|
+
encoding: 'utf8',
|
|
2903
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
2904
|
+
});
|
|
2905
|
+
assert.fail('Should have thrown an error for out-of-bounds range');
|
|
2906
|
+
} catch (e) {
|
|
2907
|
+
assert.ok(e.stderr.includes('out of bounds'), 'Should report out of bounds error');
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
it('should handle reversed line range by swapping', () => {
|
|
2912
|
+
const fixtureFile = path.join(__dirname, 'fixtures', 'javascript', 'main.js');
|
|
2913
|
+
const { execSync } = require('child_process');
|
|
2914
|
+
const ucnPath = path.join(__dirname, '..', 'ucn.js');
|
|
2915
|
+
|
|
2916
|
+
// Reversed range should work (10-5 should become 5-10)
|
|
2917
|
+
const output = execSync(`node ${ucnPath} ${fixtureFile} lines 10-5`, {
|
|
2918
|
+
encoding: 'utf8'
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
assert.ok(output.includes('5 │'), 'Should include line 5');
|
|
2922
|
+
assert.ok(output.includes('10 │'), 'Should include line 10');
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
it('should error on non-numeric line range', () => {
|
|
2926
|
+
const fixtureFile = path.join(__dirname, 'fixtures', 'javascript', 'main.js');
|
|
2927
|
+
const { execSync } = require('child_process');
|
|
2928
|
+
const ucnPath = path.join(__dirname, '..', 'ucn.js');
|
|
2929
|
+
|
|
2930
|
+
try {
|
|
2931
|
+
execSync(`node ${ucnPath} ${fixtureFile} lines abc-def`, {
|
|
2932
|
+
encoding: 'utf8',
|
|
2933
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
2934
|
+
});
|
|
2935
|
+
assert.fail('Should have thrown an error for non-numeric range');
|
|
2936
|
+
} catch (e) {
|
|
2937
|
+
assert.ok(e.stderr.includes('Invalid line range'), 'Should report invalid range error');
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
// ============================================================================
|
|
2943
|
+
// REGRESSION: findCallees should not include function declaration
|
|
2944
|
+
// ============================================================================
|
|
2945
|
+
|
|
2946
|
+
describe('Regression: findCallees should not include function declaration', () => {
|
|
2947
|
+
it('should not list function as its own callee when not recursive', () => {
|
|
2948
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-callee-decl-${Date.now()}`);
|
|
2949
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2950
|
+
|
|
2951
|
+
try {
|
|
2952
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
2953
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
2954
|
+
function nonRecursive(x) {
|
|
2955
|
+
return helper(x) + other(x);
|
|
2956
|
+
}
|
|
2957
|
+
function helper(x) { return x * 2; }
|
|
2958
|
+
function other(x) { return x + 1; }
|
|
2959
|
+
module.exports = { nonRecursive, helper, other };
|
|
2960
|
+
`);
|
|
2961
|
+
|
|
2962
|
+
const index = new ProjectIndex(tmpDir);
|
|
2963
|
+
index.build('**/*.js', { quiet: true });
|
|
2964
|
+
|
|
2965
|
+
const ctx = index.context('nonRecursive');
|
|
2966
|
+
|
|
2967
|
+
// Function should not appear as its own callee
|
|
2968
|
+
const selfCallee = ctx.callees.find(c => c.name === 'nonRecursive');
|
|
2969
|
+
assert.ok(!selfCallee, 'Non-recursive function should not list itself as callee');
|
|
2970
|
+
|
|
2971
|
+
// But should still list actual callees
|
|
2972
|
+
assert.ok(ctx.callees.some(c => c.name === 'helper'), 'Should list helper as callee');
|
|
2973
|
+
assert.ok(ctx.callees.some(c => c.name === 'other'), 'Should list other as callee');
|
|
2974
|
+
} finally {
|
|
2975
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
it('should detect callees in single-line functions', () => {
|
|
2980
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-single-line-${Date.now()}`);
|
|
2981
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2982
|
+
|
|
2983
|
+
try {
|
|
2984
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
2985
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
2986
|
+
function singleLine() { return helper() + other(); }
|
|
2987
|
+
function helper() { return 1; }
|
|
2988
|
+
function other() { return 2; }
|
|
2989
|
+
module.exports = { singleLine, helper, other };
|
|
2990
|
+
`);
|
|
2991
|
+
|
|
2992
|
+
const index = new ProjectIndex(tmpDir);
|
|
2993
|
+
index.build('**/*.js', { quiet: true });
|
|
2994
|
+
|
|
2995
|
+
const ctx = index.context('singleLine');
|
|
2996
|
+
|
|
2997
|
+
// Single-line function should detect its callees
|
|
2998
|
+
assert.ok(ctx.callees.some(c => c.name === 'helper'), 'Should detect helper callee in single-line function');
|
|
2999
|
+
assert.ok(ctx.callees.some(c => c.name === 'other'), 'Should detect other callee in single-line function');
|
|
3000
|
+
// But should not include itself
|
|
3001
|
+
assert.ok(!ctx.callees.some(c => c.name === 'singleLine'), 'Should not include itself as callee');
|
|
3002
|
+
} finally {
|
|
3003
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
|
|
3007
|
+
it('should list function as callee when actually recursive', () => {
|
|
3008
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-recursive-${Date.now()}`);
|
|
3009
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3010
|
+
|
|
3011
|
+
try {
|
|
3012
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
3013
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
3014
|
+
function factorial(n) {
|
|
3015
|
+
if (n <= 1) return 1;
|
|
3016
|
+
return n * factorial(n - 1);
|
|
3017
|
+
}
|
|
3018
|
+
module.exports = { factorial };
|
|
3019
|
+
`);
|
|
3020
|
+
|
|
3021
|
+
const index = new ProjectIndex(tmpDir);
|
|
3022
|
+
index.build('**/*.js', { quiet: true });
|
|
3023
|
+
|
|
3024
|
+
const ctx = index.context('factorial');
|
|
3025
|
+
|
|
3026
|
+
// Recursive function SHOULD appear as its own callee
|
|
3027
|
+
const selfCallee = ctx.callees.find(c => c.name === 'factorial');
|
|
3028
|
+
assert.ok(selfCallee, 'Recursive function should list itself as callee');
|
|
3029
|
+
} finally {
|
|
3030
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
// ============================================================================
|
|
3036
|
+
// REGRESSION: Negative depth values should be clamped to 0
|
|
3037
|
+
// ============================================================================
|
|
3038
|
+
|
|
3039
|
+
describe('Regression: negative depth should be clamped to 0', () => {
|
|
3040
|
+
it('trace should work with negative depth (clamped to 0)', () => {
|
|
3041
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-neg-depth-${Date.now()}`);
|
|
3042
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3043
|
+
|
|
3044
|
+
try {
|
|
3045
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
3046
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), `
|
|
3047
|
+
function main() {
|
|
3048
|
+
return helper();
|
|
3049
|
+
}
|
|
3050
|
+
function helper() { return 42; }
|
|
3051
|
+
module.exports = { main, helper };
|
|
3052
|
+
`);
|
|
3053
|
+
|
|
3054
|
+
const index = new ProjectIndex(tmpDir);
|
|
3055
|
+
index.build('**/*.js', { quiet: true });
|
|
3056
|
+
|
|
3057
|
+
// Negative depth should be clamped to 0
|
|
3058
|
+
const trace = index.trace('main', { depth: -5 });
|
|
3059
|
+
|
|
3060
|
+
assert.ok(trace, 'Trace should return a result');
|
|
3061
|
+
assert.strictEqual(trace.maxDepth, 0, 'maxDepth should be clamped to 0');
|
|
3062
|
+
assert.strictEqual(trace.root, 'main', 'Root should be main');
|
|
3063
|
+
} finally {
|
|
3064
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
it('graph should work with negative depth (clamped to 0)', () => {
|
|
3069
|
+
const tmpDir = path.join(require('os').tmpdir(), `ucn-test-neg-graph-${Date.now()}`);
|
|
3070
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
3071
|
+
|
|
3072
|
+
try {
|
|
3073
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name": "test"}');
|
|
3074
|
+
fs.writeFileSync(path.join(tmpDir, 'main.js'), `
|
|
3075
|
+
const { helper } = require('./helper');
|
|
3076
|
+
console.log(helper());
|
|
3077
|
+
`);
|
|
3078
|
+
fs.writeFileSync(path.join(tmpDir, 'helper.js'), `
|
|
3079
|
+
module.exports = { helper: () => 42 };
|
|
3080
|
+
`);
|
|
3081
|
+
|
|
3082
|
+
const index = new ProjectIndex(tmpDir);
|
|
3083
|
+
index.build('**/*.js', { quiet: true });
|
|
3084
|
+
|
|
3085
|
+
// Negative depth should be clamped to 0
|
|
3086
|
+
const graph = index.graph('main.js', { maxDepth: -10 });
|
|
3087
|
+
|
|
3088
|
+
assert.ok(graph.nodes.length > 0, 'Graph should have nodes');
|
|
3089
|
+
// With depth 0, should only show root and its direct imports (depth 0)
|
|
3090
|
+
assert.ok(graph.nodes.some(n => n.relativePath === 'main.js'), 'Should include main.js');
|
|
3091
|
+
} finally {
|
|
3092
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
});
|
|
3096
|
+
|
|
3097
|
+
// ============================================================================
|
|
3098
|
+
// REGRESSION: Double dash separator should end flag processing
|
|
3099
|
+
// ============================================================================
|
|
3100
|
+
|
|
3101
|
+
describe('Regression: double dash separator for arguments', () => {
|
|
3102
|
+
it('should allow searching for flag-like strings after --', () => {
|
|
3103
|
+
const fixtureDir = path.join(__dirname, 'fixtures', 'javascript');
|
|
3104
|
+
const { execSync } = require('child_process');
|
|
3105
|
+
const ucnPath = path.join(__dirname, '..', 'ucn.js');
|
|
3106
|
+
|
|
3107
|
+
// This should NOT error with "Unknown flag"
|
|
3108
|
+
const output = execSync(`node ${ucnPath} ${fixtureDir} find -- --test`, {
|
|
3109
|
+
encoding: 'utf8'
|
|
3110
|
+
});
|
|
3111
|
+
|
|
3112
|
+
// Should show "no symbols found" rather than "unknown flag"
|
|
3113
|
+
assert.ok(!output.includes('Unknown flag'), 'Should not treat --test as flag after --');
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
it('should process flags before -- normally', () => {
|
|
3117
|
+
const fixtureDir = path.join(__dirname, 'fixtures', 'javascript');
|
|
3118
|
+
const { execSync } = require('child_process');
|
|
3119
|
+
const ucnPath = path.join(__dirname, '..', 'ucn.js');
|
|
3120
|
+
|
|
3121
|
+
// Flags before -- should work
|
|
3122
|
+
const output = execSync(`node ${ucnPath} ${fixtureDir} find processData --json --`, {
|
|
3123
|
+
encoding: 'utf8'
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
// Should be valid JSON
|
|
3127
|
+
assert.ok(output.startsWith('{'), 'Should output JSON when --json flag is before --');
|
|
3128
|
+
JSON.parse(output); // Should not throw
|
|
3129
|
+
});
|
|
3130
|
+
});
|
|
3131
|
+
|
|
3132
|
+
// ============================================================================
|
|
3133
|
+
// RELIABILITY IMPROVEMENTS: AST-based search filtering
|
|
3134
|
+
// ============================================================================
|
|
3135
|
+
|
|
3136
|
+
describe('Reliability: AST-based search filtering', () => {
|
|
3137
|
+
it('should filter out matches in comments with --code-only', () => {
|
|
3138
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-search-'));
|
|
3139
|
+
try {
|
|
3140
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3141
|
+
// This comment mentions fetchData
|
|
3142
|
+
const x = 'fetchData in string';
|
|
3143
|
+
const result = fetchData(); // trailing comment fetchData
|
|
3144
|
+
/* block comment
|
|
3145
|
+
fetchData here too */
|
|
3146
|
+
`);
|
|
3147
|
+
const index = new ProjectIndex(tmpDir);
|
|
3148
|
+
index.build('**/*.js', { quiet: true });
|
|
3149
|
+
|
|
3150
|
+
const results = index.search('fetchData', { codeOnly: true });
|
|
3151
|
+
const allMatches = results.flatMap(r => r.matches);
|
|
3152
|
+
|
|
3153
|
+
// Should only find the actual code call on line 4
|
|
3154
|
+
assert.strictEqual(allMatches.length, 1, 'Should find only 1 code match');
|
|
3155
|
+
assert.ok(allMatches[0].content.includes('const result = fetchData()'), 'Should find the actual call');
|
|
3156
|
+
} finally {
|
|
3157
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3158
|
+
}
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
it('should include template literal expressions as code', () => {
|
|
3162
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-search-template-'));
|
|
3163
|
+
try {
|
|
3164
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3165
|
+
const str = \`fetchData in template\`;
|
|
3166
|
+
const dynamic = \`Result: \${fetchData()}\`;
|
|
3167
|
+
`);
|
|
3168
|
+
const index = new ProjectIndex(tmpDir);
|
|
3169
|
+
index.build('**/*.js', { quiet: true });
|
|
3170
|
+
|
|
3171
|
+
const results = index.search('fetchData', { codeOnly: true });
|
|
3172
|
+
const allMatches = results.flatMap(r => r.matches);
|
|
3173
|
+
|
|
3174
|
+
// Should find the expression inside ${}, not the string literal
|
|
3175
|
+
assert.strictEqual(allMatches.length, 1, 'Should find 1 match in template expression');
|
|
3176
|
+
assert.ok(allMatches[0].content.includes('${fetchData()}'), 'Should find the template expression');
|
|
3177
|
+
} finally {
|
|
3178
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
// ============================================================================
|
|
3184
|
+
// RELIABILITY IMPROVEMENTS: Stacktrace file matching
|
|
3185
|
+
// ============================================================================
|
|
3186
|
+
|
|
3187
|
+
describe('Reliability: Stacktrace file matching', () => {
|
|
3188
|
+
it('should parse various stack trace formats', () => {
|
|
3189
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-stack-'));
|
|
3190
|
+
try {
|
|
3191
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
3192
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'app.js'), `
|
|
3193
|
+
function processData(data) {
|
|
3194
|
+
throw new Error('test');
|
|
3195
|
+
}
|
|
3196
|
+
`);
|
|
3197
|
+
const index = new ProjectIndex(tmpDir);
|
|
3198
|
+
index.build('**/*.js', { quiet: true });
|
|
3199
|
+
|
|
3200
|
+
// Test Node.js format
|
|
3201
|
+
const nodeStack = index.parseStackTrace('at processData (src/app.js:3:11)');
|
|
3202
|
+
assert.strictEqual(nodeStack.frames.length, 1);
|
|
3203
|
+
assert.ok(nodeStack.frames[0].found);
|
|
3204
|
+
|
|
3205
|
+
// Test Firefox format
|
|
3206
|
+
const ffStack = index.parseStackTrace('processData@src/app.js:3:11');
|
|
3207
|
+
assert.strictEqual(ffStack.frames.length, 1);
|
|
3208
|
+
assert.ok(ffStack.frames[0].found);
|
|
3209
|
+
|
|
3210
|
+
// Test async format
|
|
3211
|
+
const asyncStack = index.parseStackTrace('at async processData (src/app.js:3:11)');
|
|
3212
|
+
assert.strictEqual(asyncStack.frames.length, 1);
|
|
3213
|
+
assert.ok(asyncStack.frames[0].found);
|
|
3214
|
+
} finally {
|
|
3215
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
it('should score path similarity and choose best match', () => {
|
|
3220
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-stack-sim-'));
|
|
3221
|
+
try {
|
|
3222
|
+
// Create files with similar names in different directories
|
|
3223
|
+
fs.mkdirSync(path.join(tmpDir, 'src', 'utils'), { recursive: true });
|
|
3224
|
+
fs.mkdirSync(path.join(tmpDir, 'lib', 'utils'), { recursive: true });
|
|
3225
|
+
|
|
3226
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'utils', 'helper.js'), `
|
|
3227
|
+
function helper() { console.log('src'); }
|
|
3228
|
+
`);
|
|
3229
|
+
fs.writeFileSync(path.join(tmpDir, 'lib', 'utils', 'helper.js'), `
|
|
3230
|
+
function helper() { console.log('lib'); }
|
|
3231
|
+
`);
|
|
3232
|
+
|
|
3233
|
+
const index = new ProjectIndex(tmpDir);
|
|
3234
|
+
index.build('**/*.js', { quiet: true });
|
|
3235
|
+
|
|
3236
|
+
// Should prefer more specific path
|
|
3237
|
+
const stack = index.parseStackTrace('at helper (src/utils/helper.js:2:10)');
|
|
3238
|
+
assert.strictEqual(stack.frames.length, 1);
|
|
3239
|
+
assert.ok(stack.frames[0].found);
|
|
3240
|
+
assert.ok(stack.frames[0].resolvedFile.includes('src/utils'), 'Should match src path');
|
|
3241
|
+
} finally {
|
|
3242
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
// ============================================================================
|
|
3248
|
+
// RELIABILITY IMPROVEMENTS: Callback detection for deadcode
|
|
3249
|
+
// ============================================================================
|
|
3250
|
+
|
|
3251
|
+
describe('Reliability: Callback detection for deadcode', () => {
|
|
3252
|
+
it('should not report functions used as callbacks as dead', () => {
|
|
3253
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-deadcode-'));
|
|
3254
|
+
try {
|
|
3255
|
+
fs.writeFileSync(path.join(tmpDir, 'handlers.js'), `
|
|
3256
|
+
function handleClick(e) { console.log(e); }
|
|
3257
|
+
function mapItem(item) { return item.toUpperCase(); }
|
|
3258
|
+
function unusedFn() { return 'dead'; }
|
|
3259
|
+
|
|
3260
|
+
document.addEventListener('click', handleClick);
|
|
3261
|
+
const items = ['a', 'b'].map(mapItem);
|
|
3262
|
+
`);
|
|
3263
|
+
const index = new ProjectIndex(tmpDir);
|
|
3264
|
+
index.build('**/*.js', { quiet: true });
|
|
3265
|
+
|
|
3266
|
+
const dead = index.deadcode({ includeExported: true });
|
|
3267
|
+
const deadNames = dead.map(d => d.name);
|
|
3268
|
+
|
|
3269
|
+
assert.ok(!deadNames.includes('handleClick'), 'handleClick should not be dead (event handler)');
|
|
3270
|
+
assert.ok(!deadNames.includes('mapItem'), 'mapItem should not be dead (array callback)');
|
|
3271
|
+
assert.ok(deadNames.includes('unusedFn'), 'unusedFn should be dead');
|
|
3272
|
+
} finally {
|
|
3273
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
|
|
3277
|
+
it('should not report re-exported functions as dead', () => {
|
|
3278
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-reexport-'));
|
|
3279
|
+
try {
|
|
3280
|
+
fs.writeFileSync(path.join(tmpDir, 'utils.js'), `
|
|
3281
|
+
export function formatDate(d) { return d.toString(); }
|
|
3282
|
+
export function unusedUtil() { return null; }
|
|
3283
|
+
`);
|
|
3284
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), `
|
|
3285
|
+
export { formatDate } from './utils';
|
|
3286
|
+
`);
|
|
3287
|
+
const index = new ProjectIndex(tmpDir);
|
|
3288
|
+
index.build('**/*.js', { quiet: true });
|
|
3289
|
+
|
|
3290
|
+
const dead = index.deadcode({ includeExported: true });
|
|
3291
|
+
const deadNames = dead.map(d => d.name);
|
|
3292
|
+
|
|
3293
|
+
assert.ok(!deadNames.includes('formatDate'), 'formatDate should not be dead (re-exported)');
|
|
3294
|
+
// Note: unusedUtil might or might not be dead depending on export tracking
|
|
3295
|
+
} finally {
|
|
3296
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
});
|
|
3300
|
+
|
|
3301
|
+
// ============================================================================
|
|
3302
|
+
// AST-BASED COMMENT/STRING DETECTION
|
|
3303
|
+
// ============================================================================
|
|
3304
|
+
|
|
3305
|
+
describe('AST-based Comment/String Detection', () => {
|
|
3306
|
+
it('detects inline comments correctly', () => {
|
|
3307
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-ast-test-'));
|
|
3308
|
+
try {
|
|
3309
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3310
|
+
const x = 5; // comment mentioning myFunc
|
|
3311
|
+
myFunc(); // this is a call
|
|
3312
|
+
// myFunc is mentioned here
|
|
3313
|
+
`);
|
|
3314
|
+
const index = new ProjectIndex(tmpDir);
|
|
3315
|
+
index.build('**/*.js', { quiet: true });
|
|
3316
|
+
|
|
3317
|
+
// isCommentOrStringAtPosition should detect the comment
|
|
3318
|
+
const content = fs.readFileSync(path.join(tmpDir, 'test.js'), 'utf-8');
|
|
3319
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3320
|
+
|
|
3321
|
+
// Line 2: "const x = 5; // comment mentioning myFunc"
|
|
3322
|
+
// Column 0 should be code, column after // should be comment
|
|
3323
|
+
assert.strictEqual(
|
|
3324
|
+
index.isCommentOrStringAtPosition(content, 2, 0, filePath),
|
|
3325
|
+
false,
|
|
3326
|
+
'Start of line 2 should be code'
|
|
3327
|
+
);
|
|
3328
|
+
assert.strictEqual(
|
|
3329
|
+
index.isCommentOrStringAtPosition(content, 2, 14, filePath),
|
|
3330
|
+
true,
|
|
3331
|
+
'Inside comment on line 2 should be comment'
|
|
3332
|
+
);
|
|
3333
|
+
|
|
3334
|
+
// Line 4: "// myFunc is mentioned here" - entire line is comment
|
|
3335
|
+
assert.strictEqual(
|
|
3336
|
+
index.isCommentOrStringAtPosition(content, 4, 0, filePath),
|
|
3337
|
+
true,
|
|
3338
|
+
'Comment-only line should be comment'
|
|
3339
|
+
);
|
|
3340
|
+
} finally {
|
|
3341
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
it('detects string literals correctly', () => {
|
|
3346
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-ast-test-'));
|
|
3347
|
+
try {
|
|
3348
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3349
|
+
const msg = "function call()";
|
|
3350
|
+
const real = call();
|
|
3351
|
+
`);
|
|
3352
|
+
const index = new ProjectIndex(tmpDir);
|
|
3353
|
+
index.build('**/*.js', { quiet: true });
|
|
3354
|
+
|
|
3355
|
+
const content = fs.readFileSync(path.join(tmpDir, 'test.js'), 'utf-8');
|
|
3356
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3357
|
+
|
|
3358
|
+
// Line 2: const msg = "function call()";
|
|
3359
|
+
// "function" inside the string should be detected as string
|
|
3360
|
+
assert.strictEqual(
|
|
3361
|
+
index.isCommentOrStringAtPosition(content, 2, 13, filePath),
|
|
3362
|
+
true,
|
|
3363
|
+
'Inside string literal should be string'
|
|
3364
|
+
);
|
|
3365
|
+
|
|
3366
|
+
// Line 3: const real = call();
|
|
3367
|
+
// "call" should be code
|
|
3368
|
+
assert.strictEqual(
|
|
3369
|
+
index.isCommentOrStringAtPosition(content, 3, 13, filePath),
|
|
3370
|
+
false,
|
|
3371
|
+
'Function call should be code'
|
|
3372
|
+
);
|
|
3373
|
+
} finally {
|
|
3374
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
|
|
3378
|
+
it('handles template literals with expressions', () => {
|
|
3379
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-ast-test-'));
|
|
3380
|
+
try {
|
|
3381
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), 'const x = `value is ${fn()} here`;');
|
|
3382
|
+
const index = new ProjectIndex(tmpDir);
|
|
3383
|
+
index.build('**/*.js', { quiet: true });
|
|
3384
|
+
|
|
3385
|
+
const content = fs.readFileSync(path.join(tmpDir, 'test.js'), 'utf-8');
|
|
3386
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3387
|
+
|
|
3388
|
+
// Inside template expression ${fn()} - "fn" should be code
|
|
3389
|
+
assert.strictEqual(
|
|
3390
|
+
index.isCommentOrStringAtPosition(content, 1, 22, filePath),
|
|
3391
|
+
false,
|
|
3392
|
+
'Inside template expression should be code'
|
|
3393
|
+
);
|
|
3394
|
+
|
|
3395
|
+
// Inside template string but outside expression - should be string
|
|
3396
|
+
assert.strictEqual(
|
|
3397
|
+
index.isCommentOrStringAtPosition(content, 1, 12, filePath),
|
|
3398
|
+
true,
|
|
3399
|
+
'Inside template string should be string'
|
|
3400
|
+
);
|
|
3401
|
+
} finally {
|
|
3402
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
|
|
3406
|
+
it('isInsideStringAST correctly identifies names in strings vs code', () => {
|
|
3407
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-ast-test-'));
|
|
3408
|
+
try {
|
|
3409
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3410
|
+
const msg = "call myFunc here";
|
|
3411
|
+
myFunc();
|
|
3412
|
+
`);
|
|
3413
|
+
const index = new ProjectIndex(tmpDir);
|
|
3414
|
+
index.build('**/*.js', { quiet: true });
|
|
3415
|
+
|
|
3416
|
+
const content = fs.readFileSync(path.join(tmpDir, 'test.js'), 'utf-8');
|
|
3417
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3418
|
+
const lines = content.split('\n');
|
|
3419
|
+
|
|
3420
|
+
// Line 2: myFunc appears inside string - should return true
|
|
3421
|
+
assert.strictEqual(
|
|
3422
|
+
index.isInsideStringAST(content, 2, lines[1], 'myFunc', filePath),
|
|
3423
|
+
true,
|
|
3424
|
+
'myFunc on line 2 is inside string'
|
|
3425
|
+
);
|
|
3426
|
+
|
|
3427
|
+
// Line 3: myFunc appears as code - should return false
|
|
3428
|
+
assert.strictEqual(
|
|
3429
|
+
index.isInsideStringAST(content, 3, lines[2], 'myFunc', filePath),
|
|
3430
|
+
false,
|
|
3431
|
+
'myFunc on line 3 is code'
|
|
3432
|
+
);
|
|
3433
|
+
} finally {
|
|
3434
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3435
|
+
}
|
|
3436
|
+
});
|
|
3437
|
+
|
|
3438
|
+
it('classifyUsageAST correctly classifies calls and definitions', () => {
|
|
3439
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-ast-test-'));
|
|
3440
|
+
try {
|
|
3441
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3442
|
+
function myFunc() {}
|
|
3443
|
+
myFunc();
|
|
3444
|
+
import { other } from './other';
|
|
3445
|
+
`);
|
|
3446
|
+
const index = new ProjectIndex(tmpDir);
|
|
3447
|
+
index.build('**/*.js', { quiet: true });
|
|
3448
|
+
|
|
3449
|
+
const content = fs.readFileSync(path.join(tmpDir, 'test.js'), 'utf-8');
|
|
3450
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3451
|
+
|
|
3452
|
+
// Line 2: function definition
|
|
3453
|
+
assert.strictEqual(
|
|
3454
|
+
index.classifyUsageAST(content, 2, 'myFunc', filePath),
|
|
3455
|
+
'definition',
|
|
3456
|
+
'Function declaration should be classified as definition'
|
|
3457
|
+
);
|
|
3458
|
+
|
|
3459
|
+
// Line 3: function call
|
|
3460
|
+
assert.strictEqual(
|
|
3461
|
+
index.classifyUsageAST(content, 3, 'myFunc', filePath),
|
|
3462
|
+
'call',
|
|
3463
|
+
'Function call should be classified as call'
|
|
3464
|
+
);
|
|
3465
|
+
|
|
3466
|
+
// Line 4: import
|
|
3467
|
+
assert.strictEqual(
|
|
3468
|
+
index.classifyUsageAST(content, 4, 'other', filePath),
|
|
3469
|
+
'import',
|
|
3470
|
+
'Import should be classified as import'
|
|
3471
|
+
);
|
|
3472
|
+
} finally {
|
|
3473
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3474
|
+
}
|
|
3475
|
+
});
|
|
3476
|
+
});
|
|
3477
|
+
|
|
3478
|
+
describe('Cache Performance Optimizations', () => {
|
|
3479
|
+
it('getCachedCalls uses mtime for fast cache validation', () => {
|
|
3480
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-cache-perf-'));
|
|
3481
|
+
try {
|
|
3482
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3483
|
+
function foo() { bar(); }
|
|
3484
|
+
function bar() { return 1; }
|
|
3485
|
+
`);
|
|
3486
|
+
const index = new ProjectIndex(tmpDir);
|
|
3487
|
+
index.build('**/*.js', { quiet: true });
|
|
3488
|
+
|
|
3489
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3490
|
+
|
|
3491
|
+
// First call - should parse
|
|
3492
|
+
const calls1 = index.getCachedCalls(filePath);
|
|
3493
|
+
assert.ok(calls1, 'First call should return calls');
|
|
3494
|
+
assert.ok(calls1.length > 0, 'Should find calls');
|
|
3495
|
+
|
|
3496
|
+
// Check cache entry has mtime
|
|
3497
|
+
const cached = index.callsCache.get(filePath);
|
|
3498
|
+
assert.ok(cached, 'Cache entry should exist');
|
|
3499
|
+
assert.ok(cached.mtime, 'Cache should have mtime');
|
|
3500
|
+
assert.ok(cached.hash, 'Cache should have hash');
|
|
3501
|
+
|
|
3502
|
+
// Second call - should use mtime cache (no reparse)
|
|
3503
|
+
const calls2 = index.getCachedCalls(filePath);
|
|
3504
|
+
assert.deepStrictEqual(calls2, calls1, 'Second call should return same result');
|
|
3505
|
+
} finally {
|
|
3506
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3507
|
+
}
|
|
3508
|
+
});
|
|
3509
|
+
|
|
3510
|
+
it('getCachedCalls with includeContent avoids double file read', () => {
|
|
3511
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-cache-perf-'));
|
|
3512
|
+
try {
|
|
3513
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3514
|
+
function foo() { bar(); }
|
|
3515
|
+
function bar() { return 1; }
|
|
3516
|
+
`);
|
|
3517
|
+
const index = new ProjectIndex(tmpDir);
|
|
3518
|
+
index.build('**/*.js', { quiet: true });
|
|
3519
|
+
|
|
3520
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3521
|
+
|
|
3522
|
+
// Call with includeContent
|
|
3523
|
+
const result = index.getCachedCalls(filePath, { includeContent: true });
|
|
3524
|
+
assert.ok(result, 'Should return result');
|
|
3525
|
+
assert.ok(result.calls, 'Should have calls');
|
|
3526
|
+
assert.ok(result.content, 'Should have content');
|
|
3527
|
+
assert.ok(result.content.includes('function foo'), 'Content should be the file');
|
|
3528
|
+
} finally {
|
|
3529
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3530
|
+
}
|
|
3531
|
+
});
|
|
3532
|
+
|
|
3533
|
+
it('callsCache is persisted to disk and restored', () => {
|
|
3534
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-cache-persist-'));
|
|
3535
|
+
try {
|
|
3536
|
+
fs.writeFileSync(path.join(tmpDir, 'test.js'), `
|
|
3537
|
+
function processData() {
|
|
3538
|
+
helper();
|
|
3539
|
+
console.log('done');
|
|
3540
|
+
}
|
|
3541
|
+
function helper() { return 42; }
|
|
3542
|
+
`);
|
|
3543
|
+
// Build and populate cache
|
|
3544
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
3545
|
+
index1.build('**/*.js', { quiet: true });
|
|
3546
|
+
|
|
3547
|
+
// Trigger callsCache population
|
|
3548
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3549
|
+
index1.getCachedCalls(filePath);
|
|
3550
|
+
|
|
3551
|
+
// Verify callsCache is populated
|
|
3552
|
+
assert.ok(index1.callsCache.size > 0, 'callsCache should be populated');
|
|
3553
|
+
|
|
3554
|
+
// Save cache
|
|
3555
|
+
const cachePath = path.join(tmpDir, 'test-cache.json');
|
|
3556
|
+
index1.saveCache(cachePath);
|
|
3557
|
+
|
|
3558
|
+
// Verify cache file has callsCache
|
|
3559
|
+
const cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
3560
|
+
assert.strictEqual(cacheData.version, 2, 'Cache version should be 2');
|
|
3561
|
+
assert.ok(Array.isArray(cacheData.callsCache), 'Cache should have callsCache array');
|
|
3562
|
+
assert.ok(cacheData.callsCache.length > 0, 'callsCache should have entries');
|
|
3563
|
+
|
|
3564
|
+
// Load in new instance
|
|
3565
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
3566
|
+
const loaded = index2.loadCache(cachePath);
|
|
3567
|
+
assert.ok(loaded, 'Cache should load successfully');
|
|
3568
|
+
|
|
3569
|
+
// Verify callsCache is restored
|
|
3570
|
+
assert.ok(index2.callsCache.size > 0, 'callsCache should be restored');
|
|
3571
|
+
|
|
3572
|
+
// Verify calls are usable without reparsing
|
|
3573
|
+
const calls = index2.getCachedCalls(filePath);
|
|
3574
|
+
assert.ok(calls, 'Should get calls from restored cache');
|
|
3575
|
+
assert.ok(calls.some(c => c.name === 'helper'), 'Should find helper call');
|
|
3576
|
+
} finally {
|
|
3577
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
|
|
3581
|
+
it('findCallers is fast after cache load (no reparse)', () => {
|
|
3582
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-cache-perf-'));
|
|
3583
|
+
try {
|
|
3584
|
+
// Create multiple files
|
|
3585
|
+
for (let i = 0; i < 10; i++) {
|
|
3586
|
+
fs.writeFileSync(path.join(tmpDir, `file${i}.js`), `
|
|
3587
|
+
function caller${i}() { helper(); }
|
|
3588
|
+
`);
|
|
3589
|
+
}
|
|
3590
|
+
fs.writeFileSync(path.join(tmpDir, 'helper.js'), `
|
|
3591
|
+
function helper() { return 42; }
|
|
3592
|
+
`);
|
|
3593
|
+
|
|
3594
|
+
// Build and warm up cache
|
|
3595
|
+
const index1 = new ProjectIndex(tmpDir);
|
|
3596
|
+
index1.build('**/*.js', { quiet: true });
|
|
3597
|
+
|
|
3598
|
+
// Time first findCallers (populates callsCache)
|
|
3599
|
+
const start1 = Date.now();
|
|
3600
|
+
const callers1 = index1.findCallers('helper');
|
|
3601
|
+
const time1 = Date.now() - start1;
|
|
3602
|
+
|
|
3603
|
+
// Save cache
|
|
3604
|
+
const cachePath = path.join(tmpDir, 'perf-cache.json');
|
|
3605
|
+
index1.saveCache(cachePath);
|
|
3606
|
+
|
|
3607
|
+
// Load in new instance
|
|
3608
|
+
const index2 = new ProjectIndex(tmpDir);
|
|
3609
|
+
index2.loadCache(cachePath);
|
|
3610
|
+
|
|
3611
|
+
// Time findCallers after cache load
|
|
3612
|
+
const start2 = Date.now();
|
|
3613
|
+
const callers2 = index2.findCallers('helper');
|
|
3614
|
+
const time2 = Date.now() - start2;
|
|
3615
|
+
|
|
3616
|
+
// Verify results are same
|
|
3617
|
+
assert.strictEqual(callers1.length, callers2.length, 'Same number of callers');
|
|
3618
|
+
assert.strictEqual(callers1.length, 10, 'Should find 10 callers');
|
|
3619
|
+
|
|
3620
|
+
// Cache-loaded should be reasonably fast (not doing full reparse)
|
|
3621
|
+
// Note: First call might be faster due to mtime check, second call uses persisted data
|
|
3622
|
+
assert.ok(time2 < time1 * 3 || time2 < 100,
|
|
3623
|
+
`Cache-loaded findCallers (${time2}ms) should not be much slower than warm (${time1}ms)`);
|
|
3624
|
+
} finally {
|
|
3625
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3626
|
+
}
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
it('mtime change triggers reparse but hash match skips reparse', () => {
|
|
3630
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-cache-mtime-'));
|
|
3631
|
+
try {
|
|
3632
|
+
const filePath = path.join(tmpDir, 'test.js');
|
|
3633
|
+
fs.writeFileSync(filePath, `function foo() { bar(); }`);
|
|
3634
|
+
|
|
3635
|
+
const index = new ProjectIndex(tmpDir);
|
|
3636
|
+
index.build('**/*.js', { quiet: true });
|
|
3637
|
+
|
|
3638
|
+
// Get initial cache
|
|
3639
|
+
index.getCachedCalls(filePath);
|
|
3640
|
+
const cached1 = index.callsCache.get(filePath);
|
|
3641
|
+
const originalMtime = cached1.mtime;
|
|
3642
|
+
const originalHash = cached1.hash;
|
|
3643
|
+
|
|
3644
|
+
// Touch file (change mtime but not content)
|
|
3645
|
+
const now = new Date();
|
|
3646
|
+
fs.utimesSync(filePath, now, now);
|
|
3647
|
+
|
|
3648
|
+
// Get calls again - should update mtime but not reparse (hash matches)
|
|
3649
|
+
index.getCachedCalls(filePath);
|
|
3650
|
+
const cached2 = index.callsCache.get(filePath);
|
|
3651
|
+
|
|
3652
|
+
assert.notStrictEqual(cached2.mtime, originalMtime, 'mtime should be updated');
|
|
3653
|
+
assert.strictEqual(cached2.hash, originalHash, 'hash should be same (content unchanged)');
|
|
3654
|
+
} finally {
|
|
3655
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3656
|
+
}
|
|
3657
|
+
});
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
console.log('UCN v3 Test Suite');
|
|
3661
|
+
console.log('Run with: node --test test/parser.test.js');
|