@tng-sh/js 0.2.0 → 0.2.1
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 +243 -4
- 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 +90 -167
- 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.1
|
|
31
|
+
.version('0.2.1');
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Copy text to system clipboard
|
|
@@ -253,7 +253,9 @@ 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')
|
|
257
259
|
.option('--json', 'Output results as JSON events (machine-readable)')
|
|
258
260
|
|
|
259
261
|
.action(async (options) => {
|
|
@@ -344,6 +346,12 @@ program
|
|
|
344
346
|
return;
|
|
345
347
|
}
|
|
346
348
|
|
|
349
|
+
|
|
350
|
+
if (options.impact) {
|
|
351
|
+
await runImpact(options.file, options.method, options.json);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
347
355
|
if (!options.type && !options.audit) {
|
|
348
356
|
console.log(chalk.red('Error: --type <type> is required.'));
|
|
349
357
|
process.exit(1);
|
|
@@ -353,11 +361,32 @@ program
|
|
|
353
361
|
if (options.clones) {
|
|
354
362
|
await runClones(options.file, options.level || 'all', options.json);
|
|
355
363
|
} else if (options.deadcode) {
|
|
356
|
-
|
|
364
|
+
if (options.all) {
|
|
365
|
+
await runDeadCodeRepo(options.json);
|
|
366
|
+
} else {
|
|
367
|
+
await runDeadCode(options.file, options.json);
|
|
368
|
+
}
|
|
357
369
|
} else {
|
|
358
370
|
console.log(chalk.yellow('Specify a method with -m, use --outline to see methods, or run "tng i" for full selection.'));
|
|
359
371
|
}
|
|
360
|
-
|
|
372
|
+
if (options.xray) {
|
|
373
|
+
// ... (existing xray logic) ...
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (options.impact) {
|
|
378
|
+
await runImpact(options.file, options.method, options.json);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!options.type && !options.audit) {
|
|
383
|
+
console.log(chalk.red('Error: --type <type> is required.'));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
generateTest(options.file, options.method, options.type, options.audit, options.json);
|
|
387
|
+
} else if (options.deadcode && options.all && !options.file && !options.method) {
|
|
388
|
+
await runDeadCodeRepo(options.json);
|
|
389
|
+
} else if (options.file && !options.method) {
|
|
361
390
|
launchInteractive();
|
|
362
391
|
}
|
|
363
392
|
});
|
|
@@ -439,7 +468,7 @@ async function runDeadCode(filePath, jsonMode = false) {
|
|
|
439
468
|
jsonSession.start();
|
|
440
469
|
try {
|
|
441
470
|
const resultJson = detectDeadCode(absolutePath);
|
|
442
|
-
jsonSession.
|
|
471
|
+
jsonSession.showDeadCode(JSON.parse(resultJson));
|
|
443
472
|
jsonSession.stop();
|
|
444
473
|
} catch (e) {
|
|
445
474
|
jsonSession.displayError(e.message);
|
|
@@ -469,6 +498,144 @@ async function runDeadCode(filePath, jsonMode = false) {
|
|
|
469
498
|
}
|
|
470
499
|
}
|
|
471
500
|
|
|
501
|
+
function listDeadCodeFiles(projectRoot) {
|
|
502
|
+
const { listDeadcodeFiles } = require('../index');
|
|
503
|
+
try {
|
|
504
|
+
const filesJson = listDeadcodeFiles(projectRoot);
|
|
505
|
+
const files = JSON.parse(filesJson);
|
|
506
|
+
if (!Array.isArray(files)) return [];
|
|
507
|
+
return files.filter((f) => !f.endsWith('.d.ts'));
|
|
508
|
+
} catch (e) {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function runDeadCodeRepo(jsonMode = false) {
|
|
514
|
+
const { detectDeadCode } = require('../index');
|
|
515
|
+
const projectRoot = process.cwd();
|
|
516
|
+
const files = listDeadCodeFiles(projectRoot);
|
|
517
|
+
|
|
518
|
+
if (files.length === 0) {
|
|
519
|
+
console.log(chalk.yellow('No files found for dead code analysis.'));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const runAnalysis = async () => {
|
|
524
|
+
const aggregateIssues = [];
|
|
525
|
+
for (let i = 0; i < files.length; i++) {
|
|
526
|
+
const filePath = files[i];
|
|
527
|
+
const rel = path.relative(projectRoot, filePath);
|
|
528
|
+
try {
|
|
529
|
+
const resultJson = detectDeadCode(filePath);
|
|
530
|
+
const issues = JSON.parse(resultJson);
|
|
531
|
+
if (Array.isArray(issues)) {
|
|
532
|
+
for (const issue of issues) {
|
|
533
|
+
aggregateIssues.push({
|
|
534
|
+
issue_type: issue.issue_type,
|
|
535
|
+
line: issue.line,
|
|
536
|
+
message: `[${rel}] ${issue.message}`,
|
|
537
|
+
code_snippet: issue.code_snippet,
|
|
538
|
+
file: rel
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch (e) {
|
|
543
|
+
aggregateIssues.push({
|
|
544
|
+
issue_type: 'analysis_error',
|
|
545
|
+
line: 0,
|
|
546
|
+
message: `[${rel}] Failed to analyze: ${e.message}`,
|
|
547
|
+
code_snippet: '',
|
|
548
|
+
file: rel
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return aggregateIssues;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
if (jsonMode) {
|
|
556
|
+
const { JsonSession } = require('../lib/jsonSession');
|
|
557
|
+
const jsonSession = new JsonSession();
|
|
558
|
+
jsonSession.start();
|
|
559
|
+
const issues = await runAnalysis();
|
|
560
|
+
const out = {
|
|
561
|
+
file: projectRoot,
|
|
562
|
+
dead_code: issues.map(i => ({
|
|
563
|
+
type: i.issue_type,
|
|
564
|
+
line: i.line,
|
|
565
|
+
message: i.message,
|
|
566
|
+
code: i.code_snippet,
|
|
567
|
+
file: i.file
|
|
568
|
+
}))
|
|
569
|
+
};
|
|
570
|
+
jsonSession.showDeadCode(out);
|
|
571
|
+
jsonSession.stop();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const total = files.length;
|
|
576
|
+
const aggregateIssues = [];
|
|
577
|
+
const start = Date.now();
|
|
578
|
+
|
|
579
|
+
const renderBar = (current, totalCount) => {
|
|
580
|
+
const width = 20;
|
|
581
|
+
const ratio = totalCount === 0 ? 1 : current / totalCount;
|
|
582
|
+
const filled = Math.round(ratio * width);
|
|
583
|
+
const bar = '#'.repeat(filled) + '-'.repeat(width - filled);
|
|
584
|
+
const percent = Math.round(ratio * 100);
|
|
585
|
+
return `Scanning project... [${bar}] ${percent}%`;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
process.stdout.write(renderBar(0, total));
|
|
589
|
+
for (let i = 0; i < files.length; i++) {
|
|
590
|
+
const filePath = files[i];
|
|
591
|
+
const rel = path.relative(projectRoot, filePath);
|
|
592
|
+
try {
|
|
593
|
+
const resultJson = detectDeadCode(filePath);
|
|
594
|
+
const issues = JSON.parse(resultJson);
|
|
595
|
+
if (Array.isArray(issues)) {
|
|
596
|
+
for (const issue of issues) {
|
|
597
|
+
aggregateIssues.push({
|
|
598
|
+
file: rel,
|
|
599
|
+
line: issue.line,
|
|
600
|
+
message: issue.message,
|
|
601
|
+
code_snippet: issue.code_snippet,
|
|
602
|
+
issue_type: issue.issue_type
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} catch (e) {
|
|
607
|
+
aggregateIssues.push({
|
|
608
|
+
file: rel,
|
|
609
|
+
line: 0,
|
|
610
|
+
message: `Failed to analyze: ${e.message}`,
|
|
611
|
+
code_snippet: '',
|
|
612
|
+
issue_type: 'analysis_error'
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (i % 10 === 0 || i === total - 1) {
|
|
617
|
+
process.stdout.write('\r' + renderBar(i + 1, total));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
process.stdout.write('\n');
|
|
621
|
+
|
|
622
|
+
if (aggregateIssues.length === 0) {
|
|
623
|
+
console.log('No dead code detected.');
|
|
624
|
+
const seconds = ((Date.now() - start) / 1000).toFixed(2);
|
|
625
|
+
console.log(`Summary: Found 0 dead items in ${seconds}s.`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log('Found dead code:');
|
|
630
|
+
for (const issue of aggregateIssues) {
|
|
631
|
+
const snippet = issue.code_snippet ? ` (${issue.code_snippet.trim()})` : '';
|
|
632
|
+
console.log(`- ${issue.file}:${issue.line}${snippet}`);
|
|
633
|
+
}
|
|
634
|
+
const seconds = ((Date.now() - start) / 1000).toFixed(2);
|
|
635
|
+
console.log(`Summary: Found ${aggregateIssues.length} dead items in ${seconds}s.`);
|
|
636
|
+
console.log("[Tip] Run 'tng audit' to fix logic bugs in the remaining code.");
|
|
637
|
+
}
|
|
638
|
+
|
|
472
639
|
/**
|
|
473
640
|
* @command fix
|
|
474
641
|
* Apply a specific fix to a file
|
|
@@ -654,4 +821,76 @@ program.on('--help', () => {
|
|
|
654
821
|
console.log('');
|
|
655
822
|
});
|
|
656
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Logic to run impact analysis
|
|
826
|
+
*/
|
|
827
|
+
async function runImpact(filePath, methodName, jsonMode = false) {
|
|
828
|
+
const { analyzeImpact } = require('../index');
|
|
829
|
+
const absolutePath = path.resolve(filePath);
|
|
830
|
+
const projectRoot = process.cwd();
|
|
831
|
+
|
|
832
|
+
if (!fs.existsSync(absolutePath)) {
|
|
833
|
+
console.log(chalk.red(`File not found: ${filePath}`));
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (jsonMode) {
|
|
838
|
+
try {
|
|
839
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
|
|
840
|
+
console.log(resultJson);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
console.error(JSON.stringify({ error: e.message }));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
console.log(chalk.blue(`💥 Analyzing impact for ${methodName} in ${filePath}...`));
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
|
|
852
|
+
const result = JSON.parse(resultJson);
|
|
853
|
+
|
|
854
|
+
if (result.status === 'error') {
|
|
855
|
+
console.log(chalk.red(`\nError: ${result.diffs[0].message}`));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
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')));
|
|
860
|
+
|
|
861
|
+
if (result.diffs.length > 0) {
|
|
862
|
+
console.log(chalk.bold('\nChanges:'));
|
|
863
|
+
result.diffs.forEach(diff => {
|
|
864
|
+
const color = diff.severity === 'breaking' ? chalk.red : (diff.severity === 'warning' ? chalk.yellow : chalk.white);
|
|
865
|
+
const icon = diff.severity === 'breaking' ? '❌' : (diff.severity === 'warning' ? '⚠️' : 'ℹ️');
|
|
866
|
+
// Handle optional name
|
|
867
|
+
const nameStr = diff.name ? ` (${diff.name})` : '';
|
|
868
|
+
console.log(` ${icon} ${color(diff.message)}${color(nameStr)}`);
|
|
869
|
+
});
|
|
870
|
+
} else {
|
|
871
|
+
console.log(chalk.green(' No changes detected in logic signature.'));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (result.impacted_files.length > 0) {
|
|
875
|
+
console.log(chalk.bold(`\nImpacted Files (${result.impacted_files.length}):`));
|
|
876
|
+
result.impacted_files.forEach(f => {
|
|
877
|
+
// Relativize path
|
|
878
|
+
const relPath = path.relative(projectRoot, f.file);
|
|
879
|
+
console.log(chalk.dim(` • ${relPath}:${f.line}`));
|
|
880
|
+
// simple truncate code
|
|
881
|
+
const codePreview = f.code.trim().replace(/\n/g, ' ').substring(0, 60) + (f.code.length > 60 ? '...' : '');
|
|
882
|
+
console.log(chalk.gray(` ${codePreview}`));
|
|
883
|
+
});
|
|
884
|
+
} else if (result.status === 'breaking') {
|
|
885
|
+
console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
} catch (e) {
|
|
889
|
+
console.log(chalk.red(`Analysis failed: ${e.message}`));
|
|
890
|
+
if (e.message.includes("git")) {
|
|
891
|
+
console.log(chalk.yellow("Note: Impact analysis requires the file to be committed to git."));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
657
896
|
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): 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
|
@@ -34,6 +34,9 @@ class GenerateTestsUI {
|
|
|
34
34
|
} else if (choice === 'audit') {
|
|
35
35
|
const result = await this._showFileSelection(true);
|
|
36
36
|
if (result === 'exit') return 'exit';
|
|
37
|
+
} else if (choice === 'impact') {
|
|
38
|
+
const result = await this._showFileSelection(false, false, false, true);
|
|
39
|
+
if (result === 'exit') return 'exit';
|
|
37
40
|
} else if (choice === 'xray') {
|
|
38
41
|
const result = await this._showFileSelection(false, true);
|
|
39
42
|
if (result === 'exit') return 'exit';
|
|
@@ -105,9 +108,10 @@ class GenerateTestsUI {
|
|
|
105
108
|
return { success: false, message: e.message };
|
|
106
109
|
}
|
|
107
110
|
});
|
|
111
|
+
return false
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
async _showFileSelection(isAudit = false, isXray = false, isTrace = false) {
|
|
114
|
+
async _showFileSelection(isAudit = false, isXray = false, isTrace = false, isImpact = false) {
|
|
111
115
|
const files = await this._getUserFiles();
|
|
112
116
|
|
|
113
117
|
if (files.length === 0) {
|
|
@@ -123,6 +127,7 @@ class GenerateTestsUI {
|
|
|
123
127
|
|
|
124
128
|
let title = 'Select JavaScript File';
|
|
125
129
|
if (isAudit) title = 'Select JavaScript File to Audit';
|
|
130
|
+
else if (isImpact) title = 'Select JavaScript File to Impact Audit';
|
|
126
131
|
else if (isXray) title = 'Select File for X-Ray';
|
|
127
132
|
else if (isTrace) title = 'Select File for Symbolic Trace';
|
|
128
133
|
|
|
@@ -132,25 +137,25 @@ class GenerateTestsUI {
|
|
|
132
137
|
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
133
138
|
|
|
134
139
|
const selectedFile = path.resolve(cwd, selectedName);
|
|
135
|
-
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace);
|
|
140
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace, isImpact);
|
|
136
141
|
if (result === 'main_menu') return 'main_menu';
|
|
137
142
|
return result;
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false) {
|
|
145
|
+
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false, isImpact = false) {
|
|
141
146
|
let outline;
|
|
142
147
|
try {
|
|
143
148
|
const result = getFileOutline(filePath);
|
|
144
149
|
outline = JSON.parse(result);
|
|
145
150
|
} catch (e) {
|
|
146
151
|
console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
|
|
147
|
-
return this._showFileSelection(isAudit);
|
|
152
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
const methods = outline.methods || [];
|
|
151
156
|
if (methods.length === 0) {
|
|
152
157
|
this.goUiSession.showNoItems('methods');
|
|
153
|
-
return this._showFileSelection(isAudit);
|
|
158
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
const fileName = path.basename(filePath);
|
|
@@ -162,46 +167,58 @@ class GenerateTestsUI {
|
|
|
162
167
|
|
|
163
168
|
let title = 'Select Method';
|
|
164
169
|
if (isAudit) title = `Select Method to Audit for ${fileName}`;
|
|
170
|
+
else if (isImpact) title = `Select Method to Impact Audit for ${fileName}`;
|
|
165
171
|
else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
|
|
166
172
|
else if (isTrace) title = `Select Method to Trace for ${fileName}`;
|
|
167
173
|
|
|
168
174
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
169
175
|
|
|
170
|
-
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace);
|
|
176
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
171
177
|
|
|
172
178
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
173
179
|
|
|
174
180
|
if (selectedMethod) {
|
|
175
181
|
if (isTrace) {
|
|
176
182
|
await this._launchTrace(filePath, selectedMethod.name);
|
|
177
|
-
return this._showFileSelection(isAudit, isXray, isTrace);
|
|
183
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
if (isXray) {
|
|
181
187
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
|
|
182
188
|
if (choice === 'main_menu') return 'main_menu';
|
|
183
|
-
return this._showFileSelection(isAudit, isXray);
|
|
189
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
if (isImpact) {
|
|
193
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, false, false, true);
|
|
194
|
+
if (choice === 'main_menu') return 'main_menu';
|
|
195
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
196
|
+
}
|
|
191
197
|
|
|
192
198
|
if (isAudit) {
|
|
199
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, true);
|
|
193
200
|
if (choice === 'main_menu') return 'main_menu';
|
|
194
|
-
return this._showFileSelection(isAudit, isXray);
|
|
201
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
const testType = this.goUiSession.showJsTestMenu();
|
|
205
|
+
if (testType === 'back') return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
206
|
+
|
|
207
|
+
const finalType = testType === 'auto' ? null : testType;
|
|
208
|
+
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, false);
|
|
209
|
+
|
|
197
210
|
if (choice && choice.file_path && !choice.error) {
|
|
198
211
|
this._showPostGenerationMenu(choice);
|
|
199
212
|
}
|
|
200
213
|
}
|
|
201
|
-
return this._showFileSelection(isAudit, isXray);
|
|
214
|
+
return this._showFileSelection(isAudit, isXray, isTrace, isImpact);
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false) {
|
|
217
|
+
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false, isImpact = false) {
|
|
218
|
+
if (isImpact) {
|
|
219
|
+
return this._handleImpactFlow(filePath, method);
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
if (!this._hasApiKey()) {
|
|
206
223
|
return { error: 'No API key' };
|
|
207
224
|
}
|
|
@@ -229,169 +246,75 @@ class GenerateTestsUI {
|
|
|
229
246
|
return true;
|
|
230
247
|
}
|
|
231
248
|
|
|
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;
|
|
249
|
+
async _handleImpactFlow(filePath, method) {
|
|
250
|
+
const { analyzeImpact } = require('../index');
|
|
251
|
+
const projectRoot = process.cwd();
|
|
250
252
|
|
|
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
|
-
});
|
|
253
|
+
console.log(chalk.blue(`\n🔍 Analyzing impact for ${method.name}...`));
|
|
255
254
|
|
|
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
|
-
});
|
|
255
|
+
let impactResult;
|
|
256
|
+
try {
|
|
257
|
+
const resultJson = await this.goUiSession.showProgress('Analyzing impact...', async (progress) => {
|
|
258
|
+
progress.update('Tracing method versions...', { percent: 30 });
|
|
259
|
+
// Artificial delay for UX
|
|
260
|
+
await new Promise(r => setTimeout(r, 300));
|
|
313
261
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
262
|
+
const json = analyzeImpact(projectRoot, filePath, method.name);
|
|
263
|
+
progress.update('Comparing logic...', { percent: 80 });
|
|
264
|
+
|
|
265
|
+
return json;
|
|
266
|
+
});
|
|
318
267
|
|
|
268
|
+
impactResult = JSON.parse(resultJson);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error(chalk.red(`\nImpact Analysis failed: ${e.message}\n`));
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await this.goUiSession.showImpactResults(impactResult);
|
|
275
|
+
return { message: 'Impact audit complete' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async _handleAuditFlow(filePath, method, testType) {
|
|
279
|
+
const { runAudit } = require('../index');
|
|
280
|
+
const config = loadConfig();
|
|
281
|
+
const fileName = path.basename(filePath);
|
|
282
|
+
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
283
|
+
|
|
284
|
+
console.log(chalk.blue(`\n🔍 Auditing ${displayName}...`));
|
|
285
|
+
|
|
286
|
+
let auditResult;
|
|
319
287
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
288
|
+
const resultJson = await this.goUiSession.showProgress(`Auditing ${displayName}...`, async (progress) => {
|
|
289
|
+
progress.update('Preparing audit...', { percent: 10 });
|
|
290
|
+
|
|
291
|
+
const json = runAudit(
|
|
292
|
+
filePath,
|
|
293
|
+
method.name,
|
|
294
|
+
method.class_name || null,
|
|
295
|
+
testType || null,
|
|
296
|
+
JSON.stringify(config),
|
|
297
|
+
(msg, percent) => {
|
|
298
|
+
if (typeof percent === 'number') {
|
|
299
|
+
progress.update(msg, { percent });
|
|
300
|
+
} else {
|
|
301
|
+
progress.update(msg, { step_increment: false });
|
|
334
302
|
}
|
|
335
303
|
}
|
|
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
|
-
}
|
|
304
|
+
);
|
|
373
305
|
|
|
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
|
-
}
|
|
306
|
+
progress.update('Rendering results...', { percent: 95 });
|
|
307
|
+
return json;
|
|
308
|
+
});
|
|
387
309
|
|
|
388
|
-
|
|
310
|
+
auditResult = JSON.parse(resultJson);
|
|
389
311
|
} catch (e) {
|
|
390
|
-
console.error(chalk.red(`\nAudit
|
|
312
|
+
console.error(chalk.red(`\nAudit failed: ${e.message}\n`));
|
|
391
313
|
return null;
|
|
392
|
-
} finally {
|
|
393
|
-
await worker.terminate();
|
|
394
314
|
}
|
|
315
|
+
|
|
316
|
+
await this.goUiSession.showAuditResults(auditResult);
|
|
317
|
+
return { message: 'Audit complete' };
|
|
395
318
|
}
|
|
396
319
|
|
|
397
320
|
async _handleTestGenerationFlow(filePath, method, testType, displayName) {
|
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.1",
|
|
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
|