@tng-sh/js 0.2.0 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/js-pie.iml +11 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/bin/tng.js +358 -86
- package/binaries/go-ui-darwin-amd64 +0 -0
- package/binaries/go-ui-darwin-arm64 +0 -0
- package/binaries/go-ui-linux-amd64 +0 -0
- package/binaries/go-ui-linux-arm64 +0 -0
- package/index.d.ts +7 -0
- package/index.js +3 -1
- package/lib/generateTestsUi.js +94 -172
- package/lib/goUiSession.js +32 -1
- package/lib/jsonSession.js +4 -0
- package/package.json +4 -3
- package/tng_sh_js.linux-arm64-gnu.node +0 -0
- package/tng_sh_js.linux-x64-gnu.node +0 -0
package/.idea/js-pie.iml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
|
4
|
+
<shared />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="NewModuleRootManager">
|
|
7
|
+
<content url="file://$MODULE_DIR$" />
|
|
8
|
+
<orderEntry type="inheritedJdk" />
|
|
9
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
10
|
+
</component>
|
|
11
|
+
</module>
|
package/.idea/vcs.xml
ADDED
package/bin/tng.js
CHANGED
|
@@ -28,7 +28,7 @@ process.on('uncaughtException', (err) => {
|
|
|
28
28
|
program
|
|
29
29
|
.name('tng')
|
|
30
30
|
.description('TNG - Advanced Code Audit, Test Generation, Visualization, Clone Detection, and Dead Code Analysis for JavaScript/TypeScript')
|
|
31
|
-
.version('0.
|
|
31
|
+
.version('0.2.4');
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Copy text to system clipboard
|
|
@@ -253,113 +253,175 @@ program
|
|
|
253
253
|
.option('-c, --clones', 'Run duplicate code detection')
|
|
254
254
|
.option('-l, --level <level>', 'Set clone detection level (1: token, 2: structural, 3: fuzzy, or all)', 'all')
|
|
255
255
|
.option('-d, --deadcode', 'Run dead code detection (JS/TS/React)')
|
|
256
|
+
.option('--all', 'Run dead code detection across the entire repo (respects .gitignore)')
|
|
256
257
|
.option('--xray', 'Generate X-Ray visualization (Mermaid flowchart)')
|
|
258
|
+
.option('--impact', 'Run impact analysis (blast radius check) on a method')
|
|
259
|
+
.option('--callsites', 'Find in-repo call sites for a method')
|
|
260
|
+
.option('--class <name>', 'Class/module name to disambiguate methods when multiple classes define the same method in a file')
|
|
257
261
|
.option('--json', 'Output results as JSON events (machine-readable)')
|
|
258
262
|
|
|
259
263
|
.action(async (options) => {
|
|
260
|
-
if (options.
|
|
261
|
-
if (options.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
const traceJson = getSymbolicTrace(
|
|
267
|
-
path.resolve(options.file),
|
|
268
|
-
options.method,
|
|
269
|
-
null
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
if (options.json) {
|
|
273
|
-
console.log(traceJson);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Create temp file for the trace
|
|
278
|
-
const tmpDir = require('os').tmpdir();
|
|
279
|
-
const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
280
|
-
fs.writeFileSync(tmpFile, traceJson);
|
|
264
|
+
if (options.callsites) {
|
|
265
|
+
if (!options.method) {
|
|
266
|
+
console.log(chalk.red('Error: --method is required for call site analysis.'));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
281
269
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
270
|
+
try {
|
|
271
|
+
const { findCallSites } = require('../index');
|
|
272
|
+
const projectRoot = process.cwd();
|
|
273
|
+
const resultJson = findCallSites(projectRoot, options.method);
|
|
285
274
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
275
|
+
if (options.json) {
|
|
276
|
+
console.log(resultJson);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
290
279
|
|
|
291
|
-
|
|
292
|
-
|
|
280
|
+
const parsed = JSON.parse(resultJson);
|
|
281
|
+
const sites = Array.isArray(parsed) ? parsed : (parsed.call_sites || parsed.sites || parsed);
|
|
282
|
+
if (!sites || sites.length === 0) {
|
|
283
|
+
console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
293
286
|
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
287
|
+
console.log(chalk.blue(`\nFound ${sites.length} call sites:\n`));
|
|
288
|
+
sites.slice(0, 200).forEach((site) => {
|
|
289
|
+
const file = site.file || 'unknown';
|
|
290
|
+
const line = site.line || 0;
|
|
291
|
+
const content = site.content || '';
|
|
292
|
+
console.log(`${chalk.yellow(file)}:${chalk.cyan(line)} ${content}`);
|
|
293
|
+
});
|
|
294
|
+
if (sites.length > 200) {
|
|
295
|
+
console.log(chalk.dim(`\n... ${sites.length - 200} more`));
|
|
297
296
|
}
|
|
298
297
|
return;
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
300
|
+
process.exit(1);
|
|
299
301
|
}
|
|
302
|
+
}
|
|
303
|
+
// 1. Symbolic Trace
|
|
304
|
+
if (options.method && options.file && options.trace) {
|
|
305
|
+
const { getSymbolicTrace } = require('../index');
|
|
306
|
+
const GoUISession = require('../lib/goUiSession');
|
|
300
307
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
try {
|
|
309
|
+
const traceJson = getSymbolicTrace(
|
|
310
|
+
path.resolve(options.file),
|
|
311
|
+
options.method,
|
|
312
|
+
options.class || null
|
|
313
|
+
);
|
|
304
314
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
315
|
+
if (options.json) {
|
|
316
|
+
console.log(traceJson);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create temp file for the trace
|
|
321
|
+
const tmpDir = require('os').tmpdir();
|
|
322
|
+
const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
323
|
+
fs.writeFileSync(tmpFile, traceJson);
|
|
324
|
+
|
|
325
|
+
// Launch Go UI
|
|
326
|
+
const session = new GoUISession();
|
|
327
|
+
const binaryPath = session._binaryPath;
|
|
328
|
+
|
|
329
|
+
const { spawnSync } = require('child_process');
|
|
330
|
+
spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
|
|
331
|
+
stdio: 'inherit'
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Cleanup
|
|
335
|
+
try { fs.unlinkSync(tmpFile); } catch (e) { }
|
|
336
|
+
|
|
337
|
+
} catch (e) {
|
|
338
|
+
console.log(chalk.red(`Trace failed: ${e.message}`));
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 2. X-Ray
|
|
345
|
+
if (options.method && options.file && options.xray) {
|
|
346
|
+
const config = loadConfig();
|
|
347
|
+
const { generateTest: nativeGenerateTest } = require('../index');
|
|
322
348
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
mermaidCode = resultJson;
|
|
349
|
+
try {
|
|
350
|
+
const resultJson = nativeGenerateTest(
|
|
351
|
+
path.resolve(options.file),
|
|
352
|
+
options.method,
|
|
353
|
+
options.class || null,
|
|
354
|
+
'visualize',
|
|
355
|
+
JSON.stringify(config),
|
|
356
|
+
(msg, percent) => {
|
|
357
|
+
if (options.json) return;
|
|
358
|
+
if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
|
|
334
359
|
}
|
|
360
|
+
);
|
|
335
361
|
|
|
336
|
-
|
|
337
|
-
|
|
362
|
+
if (options.json) {
|
|
363
|
+
console.log(resultJson);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
338
366
|
|
|
339
|
-
|
|
367
|
+
const GoUISession = require('../lib/goUiSession');
|
|
368
|
+
const session = new GoUISession();
|
|
369
|
+
let mermaidCode = "";
|
|
370
|
+
let explanation = "";
|
|
371
|
+
try {
|
|
372
|
+
const parsed = JSON.parse(resultJson);
|
|
373
|
+
mermaidCode = parsed.mermaid_code || resultJson;
|
|
374
|
+
explanation = parsed.explanation || "";
|
|
340
375
|
} catch (e) {
|
|
341
|
-
|
|
342
|
-
process.exit(1);
|
|
376
|
+
mermaidCode = resultJson;
|
|
343
377
|
}
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
378
|
|
|
347
|
-
|
|
348
|
-
|
|
379
|
+
copyToClipboard(mermaidCode);
|
|
380
|
+
await session.showXrayResults(options.method, '', mermaidCode, explanation);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.log(chalk.red(`X-Ray failed: ${e.message}`));
|
|
349
383
|
process.exit(1);
|
|
350
384
|
}
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 3. Impact Analysis
|
|
389
|
+
if (options.method && options.file && options.impact) {
|
|
390
|
+
await runImpact(options.file, options.method, options.json, options.class || null);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 4. Clone Detection
|
|
395
|
+
if (options.file && options.clones) {
|
|
396
|
+
await runClones(options.file, options.level || 'all', options.json);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 5. Dead Code Analysis
|
|
401
|
+
if (options.deadcode) {
|
|
402
|
+
if (options.all) {
|
|
403
|
+
await runDeadCodeRepo(options.json);
|
|
404
|
+
} else if (options.file) {
|
|
356
405
|
await runDeadCode(options.file, options.json);
|
|
357
406
|
} else {
|
|
358
|
-
console.log(chalk.
|
|
407
|
+
console.log(chalk.red('Error: --file <path> or --all is required for deadcode analysis.'));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 6. Test Generation / Audit (Method + File required)
|
|
414
|
+
if (options.method && options.file) {
|
|
415
|
+
if (!options.type && !options.audit) {
|
|
416
|
+
console.log(chalk.red('Error: --type <type> is required.'));
|
|
417
|
+
process.exit(1);
|
|
359
418
|
}
|
|
360
|
-
|
|
361
|
-
|
|
419
|
+
generateTest(options.file, options.method, options.type, options.audit, options.json, options.class || null);
|
|
420
|
+
return;
|
|
362
421
|
}
|
|
422
|
+
|
|
423
|
+
// 7. Interactive Fallback
|
|
424
|
+
launchInteractive();
|
|
363
425
|
});
|
|
364
426
|
|
|
365
427
|
/**
|
|
@@ -439,7 +501,7 @@ async function runDeadCode(filePath, jsonMode = false) {
|
|
|
439
501
|
jsonSession.start();
|
|
440
502
|
try {
|
|
441
503
|
const resultJson = detectDeadCode(absolutePath);
|
|
442
|
-
jsonSession.
|
|
504
|
+
jsonSession.showDeadCode(JSON.parse(resultJson));
|
|
443
505
|
jsonSession.stop();
|
|
444
506
|
} catch (e) {
|
|
445
507
|
jsonSession.displayError(e.message);
|
|
@@ -469,6 +531,144 @@ async function runDeadCode(filePath, jsonMode = false) {
|
|
|
469
531
|
}
|
|
470
532
|
}
|
|
471
533
|
|
|
534
|
+
function listDeadCodeFiles(projectRoot) {
|
|
535
|
+
const { listDeadcodeFiles } = require('../index');
|
|
536
|
+
try {
|
|
537
|
+
const filesJson = listDeadcodeFiles(projectRoot);
|
|
538
|
+
const files = JSON.parse(filesJson);
|
|
539
|
+
if (!Array.isArray(files)) return [];
|
|
540
|
+
return files.filter((f) => !f.endsWith('.d.ts'));
|
|
541
|
+
} catch (e) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function runDeadCodeRepo(jsonMode = false) {
|
|
547
|
+
const { detectDeadCode } = require('../index');
|
|
548
|
+
const projectRoot = process.cwd();
|
|
549
|
+
const files = listDeadCodeFiles(projectRoot);
|
|
550
|
+
|
|
551
|
+
if (files.length === 0) {
|
|
552
|
+
console.log(chalk.yellow('No files found for dead code analysis.'));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const runAnalysis = async () => {
|
|
557
|
+
const aggregateIssues = [];
|
|
558
|
+
for (let i = 0; i < files.length; i++) {
|
|
559
|
+
const filePath = files[i];
|
|
560
|
+
const rel = path.relative(projectRoot, filePath);
|
|
561
|
+
try {
|
|
562
|
+
const resultJson = detectDeadCode(filePath);
|
|
563
|
+
const issues = JSON.parse(resultJson);
|
|
564
|
+
if (Array.isArray(issues)) {
|
|
565
|
+
for (const issue of issues) {
|
|
566
|
+
aggregateIssues.push({
|
|
567
|
+
issue_type: issue.issue_type,
|
|
568
|
+
line: issue.line,
|
|
569
|
+
message: `[${rel}] ${issue.message}`,
|
|
570
|
+
code_snippet: issue.code_snippet,
|
|
571
|
+
file: rel
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} catch (e) {
|
|
576
|
+
aggregateIssues.push({
|
|
577
|
+
issue_type: 'analysis_error',
|
|
578
|
+
line: 0,
|
|
579
|
+
message: `[${rel}] Failed to analyze: ${e.message}`,
|
|
580
|
+
code_snippet: '',
|
|
581
|
+
file: rel
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return aggregateIssues;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (jsonMode) {
|
|
589
|
+
const { JsonSession } = require('../lib/jsonSession');
|
|
590
|
+
const jsonSession = new JsonSession();
|
|
591
|
+
jsonSession.start();
|
|
592
|
+
const issues = await runAnalysis();
|
|
593
|
+
const out = {
|
|
594
|
+
file: projectRoot,
|
|
595
|
+
dead_code: issues.map(i => ({
|
|
596
|
+
type: i.issue_type,
|
|
597
|
+
line: i.line,
|
|
598
|
+
message: i.message,
|
|
599
|
+
code: i.code_snippet,
|
|
600
|
+
file: i.file
|
|
601
|
+
}))
|
|
602
|
+
};
|
|
603
|
+
jsonSession.showDeadCode(out);
|
|
604
|
+
jsonSession.stop();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const total = files.length;
|
|
609
|
+
const aggregateIssues = [];
|
|
610
|
+
const start = Date.now();
|
|
611
|
+
|
|
612
|
+
const renderBar = (current, totalCount) => {
|
|
613
|
+
const width = 20;
|
|
614
|
+
const ratio = totalCount === 0 ? 1 : current / totalCount;
|
|
615
|
+
const filled = Math.round(ratio * width);
|
|
616
|
+
const bar = '#'.repeat(filled) + '-'.repeat(width - filled);
|
|
617
|
+
const percent = Math.round(ratio * 100);
|
|
618
|
+
return `Scanning project... [${bar}] ${percent}%`;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
process.stdout.write(renderBar(0, total));
|
|
622
|
+
for (let i = 0; i < files.length; i++) {
|
|
623
|
+
const filePath = files[i];
|
|
624
|
+
const rel = path.relative(projectRoot, filePath);
|
|
625
|
+
try {
|
|
626
|
+
const resultJson = detectDeadCode(filePath);
|
|
627
|
+
const issues = JSON.parse(resultJson);
|
|
628
|
+
if (Array.isArray(issues)) {
|
|
629
|
+
for (const issue of issues) {
|
|
630
|
+
aggregateIssues.push({
|
|
631
|
+
file: rel,
|
|
632
|
+
line: issue.line,
|
|
633
|
+
message: issue.message,
|
|
634
|
+
code_snippet: issue.code_snippet,
|
|
635
|
+
issue_type: issue.issue_type
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch (e) {
|
|
640
|
+
aggregateIssues.push({
|
|
641
|
+
file: rel,
|
|
642
|
+
line: 0,
|
|
643
|
+
message: `Failed to analyze: ${e.message}`,
|
|
644
|
+
code_snippet: '',
|
|
645
|
+
issue_type: 'analysis_error'
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (i % 10 === 0 || i === total - 1) {
|
|
650
|
+
process.stdout.write('\r' + renderBar(i + 1, total));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
process.stdout.write('\n');
|
|
654
|
+
|
|
655
|
+
if (aggregateIssues.length === 0) {
|
|
656
|
+
console.log('No dead code detected.');
|
|
657
|
+
const seconds = ((Date.now() - start) / 1000).toFixed(2);
|
|
658
|
+
console.log(`Summary: Found 0 dead items in ${seconds}s.`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log('Found dead code:');
|
|
663
|
+
for (const issue of aggregateIssues) {
|
|
664
|
+
const snippet = issue.code_snippet ? ` (${issue.code_snippet.trim()})` : '';
|
|
665
|
+
console.log(`- ${issue.file}:${issue.line}${snippet}`);
|
|
666
|
+
}
|
|
667
|
+
const seconds = ((Date.now() - start) / 1000).toFixed(2);
|
|
668
|
+
console.log(`Summary: Found ${aggregateIssues.length} dead items in ${seconds}s.`);
|
|
669
|
+
console.log("[Tip] Run 'tng audit' to fix logic bugs in the remaining code.");
|
|
670
|
+
}
|
|
671
|
+
|
|
472
672
|
/**
|
|
473
673
|
* @command fix
|
|
474
674
|
* Apply a specific fix to a file
|
|
@@ -507,7 +707,7 @@ program
|
|
|
507
707
|
/**
|
|
508
708
|
* Logic to generate test or run audit (delegates to native binary)
|
|
509
709
|
*/
|
|
510
|
-
async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false) {
|
|
710
|
+
async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false, className = null) {
|
|
511
711
|
const config = loadConfig();
|
|
512
712
|
|
|
513
713
|
// Initialize JSON session if requested
|
|
@@ -577,7 +777,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
577
777
|
resultJson = runAudit(
|
|
578
778
|
absolutePath,
|
|
579
779
|
methodName,
|
|
580
|
-
|
|
780
|
+
className,
|
|
581
781
|
testType || null,
|
|
582
782
|
JSON.stringify(config),
|
|
583
783
|
callback
|
|
@@ -586,7 +786,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
586
786
|
resultJson = nativeGenerateTest(
|
|
587
787
|
absolutePath,
|
|
588
788
|
methodName,
|
|
589
|
-
|
|
789
|
+
className,
|
|
590
790
|
testType || null,
|
|
591
791
|
JSON.stringify(config),
|
|
592
792
|
callback
|
|
@@ -654,4 +854,76 @@ program.on('--help', () => {
|
|
|
654
854
|
console.log('');
|
|
655
855
|
});
|
|
656
856
|
|
|
857
|
+
/**
|
|
858
|
+
* Logic to run impact analysis
|
|
859
|
+
*/
|
|
860
|
+
async function runImpact(filePath, methodName, jsonMode = false, className = null) {
|
|
861
|
+
const { analyzeImpact } = require('../index');
|
|
862
|
+
const absolutePath = path.resolve(filePath);
|
|
863
|
+
const projectRoot = process.cwd();
|
|
864
|
+
|
|
865
|
+
if (!fs.existsSync(absolutePath)) {
|
|
866
|
+
console.log(chalk.red(`File not found: ${filePath}`));
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (jsonMode) {
|
|
871
|
+
try {
|
|
872
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
|
|
873
|
+
console.log(resultJson);
|
|
874
|
+
} catch (e) {
|
|
875
|
+
console.error(JSON.stringify({ error: e.message }));
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
console.log(chalk.blue(`💥 Analyzing impact for ${methodName} in ${filePath}...`));
|
|
882
|
+
|
|
883
|
+
try {
|
|
884
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
|
|
885
|
+
const result = JSON.parse(resultJson);
|
|
886
|
+
|
|
887
|
+
if (result.status === 'error') {
|
|
888
|
+
console.log(chalk.red(`\nError: ${result.diffs[0].message}`));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
console.log(chalk.bold(`\nStatus: `) + (result.status === 'breaking' ? chalk.red.bold('BREAKING CHANGES DETECTED') : result.status === 'warning' ? chalk.yellow.bold('WARNINGS FOUND') : chalk.green.bold('SAFE')));
|
|
893
|
+
|
|
894
|
+
if (result.diffs.length > 0) {
|
|
895
|
+
console.log(chalk.bold('\nChanges:'));
|
|
896
|
+
result.diffs.forEach(diff => {
|
|
897
|
+
const color = diff.severity === 'breaking' ? chalk.red : (diff.severity === 'warning' ? chalk.yellow : chalk.white);
|
|
898
|
+
const icon = diff.severity === 'breaking' ? '❌' : (diff.severity === 'warning' ? '⚠️' : 'ℹ️');
|
|
899
|
+
// Handle optional name
|
|
900
|
+
const nameStr = diff.name ? ` (${diff.name})` : '';
|
|
901
|
+
console.log(` ${icon} ${color(diff.message)}${color(nameStr)}`);
|
|
902
|
+
});
|
|
903
|
+
} else {
|
|
904
|
+
console.log(chalk.green(' No changes detected in logic signature.'));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (result.impacted_files.length > 0) {
|
|
908
|
+
console.log(chalk.bold(`\nImpacted Files (${result.impacted_files.length}):`));
|
|
909
|
+
result.impacted_files.forEach(f => {
|
|
910
|
+
// Relativize path
|
|
911
|
+
const relPath = path.relative(projectRoot, f.file);
|
|
912
|
+
console.log(chalk.dim(` • ${relPath}:${f.line}`));
|
|
913
|
+
// simple truncate code
|
|
914
|
+
const codePreview = f.code.trim().replace(/\n/g, ' ').substring(0, 60) + (f.code.length > 60 ? '...' : '');
|
|
915
|
+
console.log(chalk.gray(` ${codePreview}`));
|
|
916
|
+
});
|
|
917
|
+
} else if (result.status === 'breaking') {
|
|
918
|
+
console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
} catch (e) {
|
|
922
|
+
console.log(chalk.red(`Analysis failed: ${e.message}`));
|
|
923
|
+
if (e.message.includes("git")) {
|
|
924
|
+
console.log(chalk.yellow("Note: Impact analysis requires the file to be committed to git."));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
657
929
|
program.parse(process.argv);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -63,3 +63,10 @@ export declare function getSymbolicTrace(filePath: string, methodName: string, c
|
|
|
63
63
|
export declare function analyzeClones(projectRoot: string, filePath: string, level: string): string
|
|
64
64
|
/** Analyzes a file for dead code. */
|
|
65
65
|
export declare function detectDeadCode(filePath: string): string
|
|
66
|
+
/** List repo files for dead code analysis (gitignore-aware). */
|
|
67
|
+
export declare function listDeadcodeFiles(projectRoot: string): string
|
|
68
|
+
/**
|
|
69
|
+
* Analyzes the impact of changes in a specific method compared to git HEAD.
|
|
70
|
+
* Returns a JSON string containing breaking changes and impacted files.
|
|
71
|
+
*/
|
|
72
|
+
export declare function analyzeImpact(projectRoot: string, filePath: string, methodName: string, className?: string | undefined | null): string
|
package/index.js
CHANGED
|
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|
|
310
310
|
throw new Error(`Failed to load native binding`)
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace, analyzeClones, detectDeadCode } = nativeBinding
|
|
313
|
+
const { getFileOutline, getProjectMetadata, findCallSites, ping, submitJob, getUserStats, runAudit, applyEdit, applyEditsAtomic, generateTest, getSymbolicTrace, analyzeClones, detectDeadCode, listDeadcodeFiles, analyzeImpact } = nativeBinding
|
|
314
314
|
|
|
315
315
|
module.exports.getFileOutline = getFileOutline
|
|
316
316
|
module.exports.getProjectMetadata = getProjectMetadata
|
|
@@ -325,3 +325,5 @@ module.exports.generateTest = generateTest
|
|
|
325
325
|
module.exports.getSymbolicTrace = getSymbolicTrace
|
|
326
326
|
module.exports.analyzeClones = analyzeClones
|
|
327
327
|
module.exports.detectDeadCode = detectDeadCode
|
|
328
|
+
module.exports.listDeadcodeFiles = listDeadcodeFiles
|
|
329
|
+
module.exports.analyzeImpact = analyzeImpact
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -6,7 +6,6 @@ const GoUISession = require('./goUiSession');
|
|
|
6
6
|
const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
|
|
7
7
|
const { loadConfig } = require('./config');
|
|
8
8
|
const { saveTestFile } = require('./saveFile');
|
|
9
|
-
const { applyFix } = require('./fixApplier');
|
|
10
9
|
|
|
11
10
|
class GenerateTestsUI {
|
|
12
11
|
constructor(cliMode = false) {
|
|
@@ -34,6 +33,9 @@ class GenerateTestsUI {
|
|
|
34
33
|
} else if (choice === 'audit') {
|
|
35
34
|
const result = await this._showFileSelection(true);
|
|
36
35
|
if (result === 'exit') return 'exit';
|
|
36
|
+
} else if (choice === 'impact') {
|
|
37
|
+
const result = await this._showFileSelection(false, false, false, true);
|
|
38
|
+
if (result === 'exit') return 'exit';
|
|
37
39
|
} else if (choice === 'xray') {
|
|
38
40
|
const result = await this._showFileSelection(false, true);
|
|
39
41
|
if (result === 'exit') return 'exit';
|
|
@@ -105,9 +107,10 @@ class GenerateTestsUI {
|
|
|
105
107
|
return { success: false, message: e.message };
|
|
106
108
|
}
|
|
107
109
|
});
|
|
110
|
+
return false
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
|
|
113
|
+
async _showFileSelection(isAudit = false, isXray = false, isTrace = false, isImpact = false) {
|
|
111
114
|
const files = await this._getUserFiles();
|
|
112
115
|
|
|
113
116
|
if (files.length === 0) {
|
|
@@ -123,6 +126,7 @@ class GenerateTestsUI {
|
|
|
123
126
|
|
|
124
127
|
let title = 'Select JavaScript File';
|
|
125
128
|
if (isAudit) title = 'Select JavaScript File to Audit';
|
|
129
|
+
else if (isImpact) title = 'Select JavaScript File for Regression Check';
|
|
126
130
|
else if (isXray) title = 'Select File for X-Ray';
|
|
127
131
|
else if (isTrace) title = 'Select File for Symbolic Trace';
|
|
128
132
|
|
|
@@ -132,76 +136,88 @@ class GenerateTestsUI {
|
|
|
132
136
|
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
133
137
|
|
|
134
138
|
const selectedFile = path.resolve(cwd, selectedName);
|
|
135
|
-
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
|
|
139
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace, isImpact);
|
|
136
140
|
if (result === 'main_menu') return 'main_menu';
|
|
137
141
|
return result;
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
|
|
144
|
+
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false, isImpact = false) {
|
|
141
145
|
let outline;
|
|
142
146
|
try {
|
|
143
147
|
const result = getFileOutline(filePath);
|
|
144
148
|
outline = JSON.parse(result);
|
|
145
149
|
} catch (e) {
|
|
146
150
|
console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
|
|
147
|
-
return this._showFileSelection(isAudit);
|
|
151
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
const methods = outline.methods || [];
|
|
151
155
|
if (methods.length === 0) {
|
|
152
156
|
this.goUiSession.showNoItems('methods');
|
|
153
|
-
return this._showFileSelection(isAudit);
|
|
157
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
const fileName = path.basename(filePath);
|
|
157
161
|
const items = methods.map(m => ({
|
|
158
|
-
name: m.class_name ? `${m.class_name}
|
|
162
|
+
name: m.class_name ? `${m.class_name}#${m.name}` : m.name,
|
|
159
163
|
path: `Function in ${fileName}`,
|
|
160
164
|
methodData: m
|
|
161
165
|
}));
|
|
162
166
|
|
|
163
167
|
let title = 'Select Method';
|
|
164
168
|
if (isAudit) title = `Select Method to Audit for ${fileName}`;
|
|
169
|
+
else if (isImpact) title = `Select Method for Regression Check for ${fileName}`;
|
|
165
170
|
else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
|
|
166
171
|
else if (isTrace) title = `Select Method to Trace for ${fileName}`;
|
|
167
172
|
|
|
168
173
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
169
174
|
|
|
170
|
-
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
|
|
175
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
171
176
|
|
|
172
177
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
173
178
|
|
|
174
179
|
if (selectedMethod) {
|
|
175
180
|
if (isTrace) {
|
|
176
|
-
await this._launchTrace(filePath, selectedMethod.name);
|
|
177
|
-
return this._showFileSelection(isAudit, isXray, isTrace);
|
|
181
|
+
await this._launchTrace(filePath, selectedMethod.name, selectedMethod.class_name || null);
|
|
182
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
if (isXray) {
|
|
181
186
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
|
|
182
187
|
if (choice === 'main_menu') return 'main_menu';
|
|
183
|
-
return this._showFileSelection(isAudit, isXray);
|
|
188
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
if (isImpact) {
|
|
192
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, false, false, true);
|
|
193
|
+
if (choice === 'main_menu') return 'main_menu';
|
|
194
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
195
|
+
}
|
|
191
196
|
|
|
192
197
|
if (isAudit) {
|
|
198
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, true);
|
|
193
199
|
if (choice === 'main_menu') return 'main_menu';
|
|
194
|
-
return this._showFileSelection(isAudit, isXray);
|
|
200
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
const testType = this.goUiSession.showJsTestMenu();
|
|
204
|
+
if (testType === 'back') return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
205
|
+
|
|
206
|
+
const finalType = testType === 'auto' ? null : testType;
|
|
207
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, false);
|
|
208
|
+
|
|
197
209
|
if (choice && choice.file_path && !choice.error) {
|
|
198
210
|
this._showPostGenerationMenu(choice);
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
|
-
return this._showFileSelection(isAudit, isXray);
|
|
213
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
|
|
216
|
+
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false, isImpact = false) {
|
|
217
|
+
if (isImpact) {
|
|
218
|
+
return this._handleImpactFlow(filePath, method);
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
if (!this._hasApiKey()) {
|
|
206
222
|
return { error: 'No API key' };
|
|
207
223
|
}
|
|
@@ -229,169 +245,75 @@ class GenerateTestsUI {
|
|
|
229
245
|
return true;
|
|
230
246
|
}
|
|
231
247
|
|
|
232
|
-
async
|
|
233
|
-
const {
|
|
234
|
-
const
|
|
235
|
-
method.name,
|
|
236
|
-
method.class_name,
|
|
237
|
-
method.source_code || ''
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
if (!streamingUi) return null;
|
|
241
|
-
|
|
242
|
-
let results = {
|
|
243
|
-
issues: [],
|
|
244
|
-
behaviours: [],
|
|
245
|
-
method_name: method.name,
|
|
246
|
-
class_name: method.class_name,
|
|
247
|
-
method_source_with_lines: method.source_code
|
|
248
|
-
};
|
|
249
|
-
let auditFinished = false;
|
|
248
|
+
async _handleImpactFlow(filePath, method) {
|
|
249
|
+
const { analyzeImpact } = require('../index');
|
|
250
|
+
const projectRoot = process.cwd();
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
const worker = new Worker(path.join(__dirname, 'auditWorker.js'), {
|
|
253
|
-
workerData: { filePath, methodName: method.name, className: method.class_name || null, testType: testType || null }
|
|
254
|
-
});
|
|
252
|
+
console.log(chalk.blue(`\n🔍 Analyzing impact for ${method.name}...`));
|
|
255
253
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const data = JSON.parse(msg.data);
|
|
263
|
-
|
|
264
|
-
// Handle metadata updates from stream
|
|
265
|
-
if (data.method_name) results.method_name = data.method_name;
|
|
266
|
-
if (data.class_name) results.class_name = data.class_name;
|
|
267
|
-
if (data.method_source_with_lines) results.method_source_with_lines = data.method_source_with_lines;
|
|
268
|
-
if (data.source_code) results.method_source_with_lines = data.source_code;
|
|
269
|
-
|
|
270
|
-
// Only push if it looks like an actual audit item
|
|
271
|
-
if (data.test_name || data.summary || data.category) {
|
|
272
|
-
const id = `${data.category}-${data.test_name}-${data.line_number}`;
|
|
273
|
-
if (!itemIds.has(id)) {
|
|
274
|
-
if (data.category === 'behavior' || data.category === 'behaviour') results.behaviours.push(data);
|
|
275
|
-
else results.issues.push(data);
|
|
276
|
-
itemIds.add(id);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
} catch (e) { }
|
|
280
|
-
}
|
|
281
|
-
} else if (msg.type === 'result') {
|
|
282
|
-
try {
|
|
283
|
-
const finalResults = typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data;
|
|
284
|
-
|
|
285
|
-
// Merge final results with our local ones, preserving 'fixed' state
|
|
286
|
-
const mergeList = (localList, incomingList) => {
|
|
287
|
-
if (!incomingList || incomingList.length === 0) return localList;
|
|
288
|
-
|
|
289
|
-
return incomingList.map(incomingItem => {
|
|
290
|
-
const localItem = localList.find(it =>
|
|
291
|
-
it.test_name === incomingItem.test_name &&
|
|
292
|
-
it.line_number === incomingItem.line_number
|
|
293
|
-
);
|
|
294
|
-
if (localItem && localItem.fixed) {
|
|
295
|
-
return { ...incomingItem, fixed: true };
|
|
296
|
-
}
|
|
297
|
-
return incomingItem;
|
|
298
|
-
});
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
results.issues = mergeList(results.issues, finalResults.issues || finalResults.findings);
|
|
302
|
-
results.behaviours = mergeList(results.behaviours, finalResults.behaviours);
|
|
303
|
-
|
|
304
|
-
if (finalResults.method_source_with_lines) results.method_source_with_lines = finalResults.method_source_with_lines;
|
|
305
|
-
if (finalResults.method_name) results.method_name = finalResults.method_name;
|
|
306
|
-
} catch (e) { }
|
|
307
|
-
auditFinished = true;
|
|
308
|
-
} else if (msg.type === 'error') {
|
|
309
|
-
console.error(chalk.red(`Audit error: ${msg.data}`));
|
|
310
|
-
auditFinished = true;
|
|
311
|
-
}
|
|
312
|
-
});
|
|
254
|
+
let impactResult;
|
|
255
|
+
try {
|
|
256
|
+
const resultJson = await this.goUiSession.showProgress('Analyzing impact...', async (progress) => {
|
|
257
|
+
progress.update('Tracing method versions...', { percent: 30 });
|
|
258
|
+
// Artificial delay for UX
|
|
259
|
+
await new Promise(r => setTimeout(r, 300));
|
|
313
260
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
261
|
+
const json = analyzeImpact(projectRoot, filePath, method.name, method.class_name || null);
|
|
262
|
+
progress.update('Comparing logic...', { percent: 80 });
|
|
263
|
+
|
|
264
|
+
return json;
|
|
265
|
+
});
|
|
318
266
|
|
|
267
|
+
impactResult = JSON.parse(resultJson);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error(chalk.red(`\nImpact Analysis failed: ${e.message}\n`));
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await this.goUiSession.showImpactResults(impactResult);
|
|
274
|
+
return { message: 'Regression check complete' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async _handleAuditFlow(filePath, method, testType) {
|
|
278
|
+
const { runAudit } = require('../index');
|
|
279
|
+
const config = loadConfig();
|
|
280
|
+
const fileName = path.basename(filePath);
|
|
281
|
+
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
282
|
+
|
|
283
|
+
console.log(chalk.blue(`\n🔍 Auditing ${displayName}...`));
|
|
284
|
+
|
|
285
|
+
let auditResult;
|
|
319
286
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
287
|
+
const resultJson = await this.goUiSession.showProgress(`Auditing ${displayName}...`, async (progress) => {
|
|
288
|
+
progress.update('Preparing audit...', { percent: 10 });
|
|
289
|
+
|
|
290
|
+
const json = runAudit(
|
|
291
|
+
filePath,
|
|
292
|
+
method.name,
|
|
293
|
+
method.class_name || null,
|
|
294
|
+
testType || null,
|
|
295
|
+
JSON.stringify(config),
|
|
296
|
+
(msg, percent) => {
|
|
297
|
+
if (typeof percent === 'number') {
|
|
298
|
+
progress.update(msg, { percent });
|
|
299
|
+
} else {
|
|
300
|
+
progress.update(msg, { step_increment: false });
|
|
334
301
|
}
|
|
335
302
|
}
|
|
336
|
-
|
|
337
|
-
return null;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
currentChoice = getResponse(streamingUi.outputFile);
|
|
341
|
-
|
|
342
|
-
// Action loop: handles fix/open and returns to UI
|
|
343
|
-
while (currentChoice && (currentChoice.action === 'fix' || currentChoice.action === 'open')) {
|
|
344
|
-
const itemIndex = typeof currentChoice.index === 'number' ? currentChoice.index : 0;
|
|
345
|
-
|
|
346
|
-
if (currentChoice.action === 'fix') {
|
|
347
|
-
console.log(chalk.cyan(`Applying fix...`));
|
|
348
|
-
const fixResult = await applyFix(filePath, currentChoice.item);
|
|
349
|
-
if (fixResult.success) {
|
|
350
|
-
console.log(chalk.green(`✓ Fix applied successfully.`));
|
|
351
|
-
// Mark as fixed locally
|
|
352
|
-
[results.issues, results.behaviours].forEach(list => {
|
|
353
|
-
list.forEach(it => {
|
|
354
|
-
if (it.test_name === currentChoice.item.test_name && it.line_number === currentChoice.item.line_number) {
|
|
355
|
-
it.fixed = true;
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
} else {
|
|
360
|
-
console.log(chalk.red(`❌ Failed to apply fix: ${fixResult.error || 'Unknown error'}`));
|
|
361
|
-
}
|
|
362
|
-
} else if (currentChoice.action === 'open') {
|
|
363
|
-
this._openInEditor(filePath, currentChoice.item.line_number);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Ensure source has line numbers for static UI if it doesn't already
|
|
367
|
-
if (results.method_source_with_lines && !/^\s*\d+:/.test(results.method_source_with_lines)) {
|
|
368
|
-
results.method_source_with_lines = results.method_source_with_lines
|
|
369
|
-
.split('\n')
|
|
370
|
-
.map((line, i) => `${i + 1}: ${line}`)
|
|
371
|
-
.join('\n');
|
|
372
|
-
}
|
|
303
|
+
);
|
|
373
304
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
currentChoice = null;
|
|
378
|
-
} else {
|
|
379
|
-
try {
|
|
380
|
-
const parsed = JSON.parse(choiceStr);
|
|
381
|
-
currentChoice = typeof parsed === 'object' ? parsed : { action: choiceStr };
|
|
382
|
-
} catch (e) {
|
|
383
|
-
currentChoice = { action: choiceStr };
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
305
|
+
progress.update('Rendering results...', { percent: 95 });
|
|
306
|
+
return json;
|
|
307
|
+
});
|
|
387
308
|
|
|
388
|
-
|
|
309
|
+
auditResult = JSON.parse(resultJson);
|
|
389
310
|
} catch (e) {
|
|
390
|
-
console.error(chalk.red(`\nAudit
|
|
311
|
+
console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
|
|
391
312
|
return null;
|
|
392
|
-
} finally {
|
|
393
|
-
await worker.terminate();
|
|
394
313
|
}
|
|
314
|
+
|
|
315
|
+
await this.goUiSession.showAuditResults(auditResult);
|
|
316
|
+
return { message: 'Audit complete' };
|
|
395
317
|
}
|
|
396
318
|
|
|
397
319
|
async _handleTestGenerationFlow(filePath, method, testType, displayName) {
|
|
@@ -677,7 +599,7 @@ class GenerateTestsUI {
|
|
|
677
599
|
}
|
|
678
600
|
}
|
|
679
601
|
|
|
680
|
-
async _launchTrace(filePath, methodName) {
|
|
602
|
+
async _launchTrace(filePath, methodName, className) {
|
|
681
603
|
const { getSymbolicTrace } = require('../index');
|
|
682
604
|
const fs = require('fs');
|
|
683
605
|
const path = require('path');
|
|
@@ -687,7 +609,7 @@ class GenerateTestsUI {
|
|
|
687
609
|
// 1. Generate Trace (with Spinner)
|
|
688
610
|
const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
|
|
689
611
|
try {
|
|
690
|
-
const traceJson = getSymbolicTrace(filePath, methodName, null);
|
|
612
|
+
const traceJson = getSymbolicTrace(filePath, methodName, className || null);
|
|
691
613
|
const tmpDir = require('os').tmpdir();
|
|
692
614
|
const f = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
693
615
|
fs.writeFileSync(f, traceJson);
|
package/lib/goUiSession.js
CHANGED
|
@@ -103,7 +103,18 @@ class GoUISession {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
showListView(title, items) {
|
|
106
|
-
|
|
106
|
+
// Limit items to prevent triggering command line length limits
|
|
107
|
+
// optimized items (relative paths) allow for more items
|
|
108
|
+
let displayItems = items;
|
|
109
|
+
if (items.length > 500) {
|
|
110
|
+
displayItems = items.slice(0, 500);
|
|
111
|
+
displayItems.push({
|
|
112
|
+
name: `... and ${items.length - 500} more files (use CLI for full list)`,
|
|
113
|
+
path: 'INFO'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const dataJson = JSON.stringify({ title, items: displayItems });
|
|
107
118
|
const outputFile = this._trackTempFile(this._createTempFile('list-view-output', '.txt'));
|
|
108
119
|
|
|
109
120
|
try {
|
|
@@ -308,6 +319,25 @@ class GoUISession {
|
|
|
308
319
|
console.error('Test results display error:', error.message);
|
|
309
320
|
}
|
|
310
321
|
}
|
|
322
|
+
|
|
323
|
+
async showImpactResults(impactResult) {
|
|
324
|
+
const dataJson = JSON.stringify(impactResult);
|
|
325
|
+
const inputFile = this._trackTempFile(this._createTempFile('impact-data', '.json'));
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
fs.writeFileSync(inputFile, dataJson);
|
|
329
|
+
|
|
330
|
+
spawnSync(this._binaryPath, ['js-impact-results', '--file', inputFile], {
|
|
331
|
+
stdio: 'inherit',
|
|
332
|
+
env: process.env
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error('Impact results display error:', error.message);
|
|
336
|
+
} finally {
|
|
337
|
+
this._cleanupTempFile(inputFile);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
311
341
|
async showAuditResults(auditResult, index = 0) {
|
|
312
342
|
const dataJson = JSON.stringify(auditResult);
|
|
313
343
|
const inputFile = this._trackTempFile(this._createTempFile('audit-data', '.json'));
|
|
@@ -351,6 +381,7 @@ class GoUISession {
|
|
|
351
381
|
}
|
|
352
382
|
}
|
|
353
383
|
|
|
384
|
+
|
|
354
385
|
async showClones(filePath, results) {
|
|
355
386
|
const dataJson = JSON.stringify({ file_path: filePath, matches: results });
|
|
356
387
|
const inputFile = this._trackTempFile(this._createTempFile('clone-data', '.json'));
|
package/lib/jsonSession.js
CHANGED
|
@@ -74,6 +74,10 @@ class JsonSession {
|
|
|
74
74
|
this.emitEvent('clones', { file_path: filePath, matches: results });
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
showDeadCode(result) {
|
|
78
|
+
this.emitEvent('dead_code', result || {});
|
|
79
|
+
}
|
|
80
|
+
|
|
77
81
|
showTestResults(title, passed, failed, errors, total, results = []) {
|
|
78
82
|
this.emitEvent('test_results', {
|
|
79
83
|
title: title || 'Test Results',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tng-sh/js",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "TNG JavaScript CLI",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"test": "node test.js"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@napi-rs/cli": "^2.18.0"
|
|
36
|
+
"@napi-rs/cli": "^2.18.0",
|
|
37
|
+
"@tng-sh/js": "^0.2.0"
|
|
37
38
|
},
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">= 10"
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
"fast-glob": "^3.3.3",
|
|
45
46
|
"prettier": "^2.8.8"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|
|
Binary file
|
|
Binary file
|