@tng-sh/js 0.2.1 ā 0.2.5
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/bin/tng.js +200 -115
- 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 +1 -1
- package/lib/generateTestsUi.js +46 -38
- package/lib/ignore.js +56 -0
- package/package.json +1 -1
- package/tng_sh_js.linux-arm64-gnu.node +0 -0
- package/tng_sh_js.linux-x64-gnu.node +0 -0
package/bin/tng.js
CHANGED
|
@@ -5,6 +5,7 @@ const chalk = require('chalk');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { loadConfig } = require('../lib/config');
|
|
8
|
+
const { isIgnoredPath, filterIgnoredPaths, ignoredMessage } = require('../lib/ignore');
|
|
8
9
|
const { saveTestFile } = require('../lib/saveFile');
|
|
9
10
|
const { ping, getUserStats } = require('../index');
|
|
10
11
|
const { applyFix } = require('../lib/fixApplier');
|
|
@@ -28,7 +29,7 @@ process.on('uncaughtException', (err) => {
|
|
|
28
29
|
program
|
|
29
30
|
.name('tng')
|
|
30
31
|
.description('TNG - Advanced Code Audit, Test Generation, Visualization, Clone Detection, and Dead Code Analysis for JavaScript/TypeScript')
|
|
31
|
-
.version('0.2.
|
|
32
|
+
.version('0.2.5');
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Copy text to system clipboard
|
|
@@ -92,7 +93,11 @@ module.exports = {
|
|
|
92
93
|
TEST_FRAMEWORK: "jest",
|
|
93
94
|
|
|
94
95
|
// Test Directory
|
|
95
|
-
TEST_DIRECTORY: "tests"
|
|
96
|
+
TEST_DIRECTORY: "tests",
|
|
97
|
+
|
|
98
|
+
// Ignore paths (relative to project root)
|
|
99
|
+
IGNORE_FILES: [],
|
|
100
|
+
IGNORE_FOLDERS: []
|
|
96
101
|
};
|
|
97
102
|
`;
|
|
98
103
|
fs.writeFileSync(configPath, content);
|
|
@@ -189,11 +194,16 @@ program
|
|
|
189
194
|
process.exit(1);
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
const config = loadConfig();
|
|
198
|
+
const resolvedPath = path.resolve(options.file);
|
|
199
|
+
if (isIgnoredPath(resolvedPath, config)) {
|
|
200
|
+
console.log(chalk.red(ignoredMessage()));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
console.log(chalk.blue(`š Generating X-Ray for ${options.method} in ${options.file}...`));
|
|
193
205
|
|
|
194
206
|
try {
|
|
195
|
-
const config = loadConfig();
|
|
196
|
-
|
|
197
207
|
// Re-use logic similar to _handleXrayFlow but tailored for CLI non-interactive output
|
|
198
208
|
const { generateTest } = require('../index');
|
|
199
209
|
|
|
@@ -256,139 +266,183 @@ program
|
|
|
256
266
|
.option('--all', 'Run dead code detection across the entire repo (respects .gitignore)')
|
|
257
267
|
.option('--xray', 'Generate X-Ray visualization (Mermaid flowchart)')
|
|
258
268
|
.option('--impact', 'Run impact analysis (blast radius check) on a method')
|
|
269
|
+
.option('--callsites', 'Find in-repo call sites for a method')
|
|
270
|
+
.option('--class <name>', 'Class/module name to disambiguate methods when multiple classes define the same method in a file')
|
|
259
271
|
.option('--json', 'Output results as JSON events (machine-readable)')
|
|
260
272
|
|
|
261
273
|
.action(async (options) => {
|
|
262
|
-
if (options.
|
|
263
|
-
if (options.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const traceJson = getSymbolicTrace(
|
|
269
|
-
path.resolve(options.file),
|
|
270
|
-
options.method,
|
|
271
|
-
null
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (options.json) {
|
|
275
|
-
console.log(traceJson);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Create temp file for the trace
|
|
280
|
-
const tmpDir = require('os').tmpdir();
|
|
281
|
-
const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
282
|
-
fs.writeFileSync(tmpFile, traceJson);
|
|
274
|
+
if (options.callsites) {
|
|
275
|
+
if (!options.method) {
|
|
276
|
+
console.log(chalk.red('Error: --method is required for call site analysis.'));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
283
279
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
280
|
+
try {
|
|
281
|
+
const { findCallSites } = require('../index');
|
|
282
|
+
const projectRoot = process.cwd();
|
|
283
|
+
const resultJson = findCallSites(projectRoot, options.method);
|
|
287
284
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
if (options.json) {
|
|
286
|
+
console.log(resultJson);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
292
289
|
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
const parsed = JSON.parse(resultJson);
|
|
291
|
+
const sites = Array.isArray(parsed) ? parsed : (parsed.call_sites || parsed.sites || parsed);
|
|
292
|
+
if (!sites || sites.length === 0) {
|
|
293
|
+
console.log(chalk.green('\nNo call sites found in this project. You might be safe!'));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
295
296
|
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
console.log(chalk.blue(`\nFound ${sites.length} call sites:\n`));
|
|
298
|
+
sites.slice(0, 200).forEach((site) => {
|
|
299
|
+
const file = site.file || 'unknown';
|
|
300
|
+
const line = site.line || 0;
|
|
301
|
+
const content = site.content || '';
|
|
302
|
+
console.log(`${chalk.yellow(file)}:${chalk.cyan(line)} ${content}`);
|
|
303
|
+
});
|
|
304
|
+
if (sites.length > 200) {
|
|
305
|
+
console.log(chalk.dim(`\n... ${sites.length - 200} more`));
|
|
299
306
|
}
|
|
300
307
|
return;
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.log(chalk.red(`Error: ${e.message}`));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// 1. Symbolic Trace
|
|
314
|
+
if (options.method && options.file && options.trace) {
|
|
315
|
+
const { getSymbolicTrace } = require('../index');
|
|
316
|
+
const GoUISession = require('../lib/goUiSession');
|
|
317
|
+
const config = loadConfig();
|
|
318
|
+
const resolvedPath = path.resolve(options.file);
|
|
319
|
+
if (isIgnoredPath(resolvedPath, config)) {
|
|
320
|
+
console.log(chalk.red(ignoredMessage()));
|
|
321
|
+
process.exit(1);
|
|
301
322
|
}
|
|
302
323
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
324
|
+
try {
|
|
325
|
+
const traceJson = getSymbolicTrace(
|
|
326
|
+
resolvedPath,
|
|
327
|
+
options.method,
|
|
328
|
+
options.class || null
|
|
329
|
+
);
|
|
306
330
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
null,
|
|
312
|
-
'visualize',
|
|
313
|
-
JSON.stringify(config),
|
|
314
|
-
(msg, percent) => {
|
|
315
|
-
if (options.json) return;
|
|
316
|
-
if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
if (options.json) {
|
|
321
|
-
console.log(resultJson);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
331
|
+
if (options.json) {
|
|
332
|
+
console.log(traceJson);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
324
335
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let explanation = "";
|
|
330
|
-
try {
|
|
331
|
-
const parsed = JSON.parse(resultJson);
|
|
332
|
-
mermaidCode = parsed.mermaid_code || resultJson;
|
|
333
|
-
explanation = parsed.explanation || "";
|
|
334
|
-
} catch (e) {
|
|
335
|
-
mermaidCode = resultJson;
|
|
336
|
-
}
|
|
336
|
+
// Create temp file for the trace
|
|
337
|
+
const tmpDir = require('os').tmpdir();
|
|
338
|
+
const tmpFile = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
339
|
+
fs.writeFileSync(tmpFile, traceJson);
|
|
337
340
|
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
// Launch Go UI
|
|
342
|
+
const session = new GoUISession();
|
|
343
|
+
const binaryPath = session._binaryPath;
|
|
340
344
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
345
|
+
const { spawnSync } = require('child_process');
|
|
346
|
+
spawnSync(binaryPath, ['js-trace-results', '--file', tmpFile], {
|
|
347
|
+
stdio: 'inherit'
|
|
348
|
+
});
|
|
348
349
|
|
|
350
|
+
// Cleanup
|
|
351
|
+
try { fs.unlinkSync(tmpFile); } catch (e) { }
|
|
349
352
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.log(chalk.red(`Trace failed: ${e.message}`));
|
|
355
|
+
process.exit(1);
|
|
353
356
|
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
354
359
|
|
|
355
|
-
|
|
356
|
-
|
|
360
|
+
// 2. X-Ray
|
|
361
|
+
if (options.method && options.file && options.xray) {
|
|
362
|
+
const config = loadConfig();
|
|
363
|
+
const resolvedPath = path.resolve(options.file);
|
|
364
|
+
if (isIgnoredPath(resolvedPath, config)) {
|
|
365
|
+
console.log(chalk.red(ignoredMessage()));
|
|
357
366
|
process.exit(1);
|
|
358
367
|
}
|
|
359
|
-
generateTest
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
const { generateTest: nativeGenerateTest } = require('../index');
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const resultJson = nativeGenerateTest(
|
|
372
|
+
resolvedPath,
|
|
373
|
+
options.method,
|
|
374
|
+
options.class || null,
|
|
375
|
+
'visualize',
|
|
376
|
+
JSON.stringify(config),
|
|
377
|
+
(msg, percent) => {
|
|
378
|
+
if (options.json) return;
|
|
379
|
+
if (percent % 20 === 0) console.log(chalk.dim(`[${percent}%] ${msg}`));
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (options.json) {
|
|
384
|
+
console.log(resultJson);
|
|
385
|
+
return;
|
|
368
386
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
387
|
+
|
|
388
|
+
const GoUISession = require('../lib/goUiSession');
|
|
389
|
+
const session = new GoUISession();
|
|
390
|
+
let mermaidCode = "";
|
|
391
|
+
let explanation = "";
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(resultJson);
|
|
394
|
+
mermaidCode = parsed.mermaid_code || resultJson;
|
|
395
|
+
explanation = parsed.explanation || "";
|
|
396
|
+
} catch (e) {
|
|
397
|
+
mermaidCode = resultJson;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
copyToClipboard(mermaidCode);
|
|
401
|
+
await session.showXrayResults(options.method, '', mermaidCode, explanation);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.log(chalk.red(`X-Ray failed: ${e.message}`));
|
|
404
|
+
process.exit(1);
|
|
375
405
|
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
376
408
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
409
|
+
// 3. Impact Analysis
|
|
410
|
+
if (options.method && options.file && options.impact) {
|
|
411
|
+
await runImpact(options.file, options.method, options.json, options.class || null);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 4. Clone Detection
|
|
416
|
+
if (options.file && options.clones) {
|
|
417
|
+
await runClones(options.file, options.level || 'all', options.json);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 5. Dead Code Analysis
|
|
422
|
+
if (options.deadcode) {
|
|
423
|
+
if (options.all) {
|
|
424
|
+
await runDeadCodeRepo(options.json);
|
|
425
|
+
} else if (options.file) {
|
|
426
|
+
await runDeadCode(options.file, options.json);
|
|
427
|
+
} else {
|
|
428
|
+
console.log(chalk.red('Error: --file <path> or --all is required for deadcode analysis.'));
|
|
429
|
+
process.exit(1);
|
|
380
430
|
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
381
433
|
|
|
434
|
+
// 6. Test Generation / Audit (Method + File required)
|
|
435
|
+
if (options.method && options.file) {
|
|
382
436
|
if (!options.type && !options.audit) {
|
|
383
437
|
console.log(chalk.red('Error: --type <type> is required.'));
|
|
384
438
|
process.exit(1);
|
|
385
439
|
}
|
|
386
|
-
generateTest(options.file, options.method, options.type, options.audit, options.json);
|
|
387
|
-
|
|
388
|
-
await runDeadCodeRepo(options.json);
|
|
389
|
-
} else if (options.file && !options.method) {
|
|
390
|
-
launchInteractive();
|
|
440
|
+
generateTest(options.file, options.method, options.type, options.audit, options.json, options.class || null);
|
|
441
|
+
return;
|
|
391
442
|
}
|
|
443
|
+
|
|
444
|
+
// 7. Interactive Fallback
|
|
445
|
+
launchInteractive();
|
|
392
446
|
});
|
|
393
447
|
|
|
394
448
|
/**
|
|
@@ -396,6 +450,7 @@ program
|
|
|
396
450
|
*/
|
|
397
451
|
async function runClones(filePath, level, jsonMode = false) {
|
|
398
452
|
const { analyzeClones } = require('../index');
|
|
453
|
+
const config = loadConfig();
|
|
399
454
|
const absolutePath = path.resolve(filePath);
|
|
400
455
|
const projectRoot = process.cwd();
|
|
401
456
|
|
|
@@ -403,6 +458,10 @@ async function runClones(filePath, level, jsonMode = false) {
|
|
|
403
458
|
console.log(chalk.red(`File not found: ${filePath}`));
|
|
404
459
|
process.exit(1);
|
|
405
460
|
}
|
|
461
|
+
if (isIgnoredPath(absolutePath, config)) {
|
|
462
|
+
console.log(chalk.red(ignoredMessage()));
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
406
465
|
|
|
407
466
|
const GoUISession = require('../lib/goUiSession');
|
|
408
467
|
const session = new GoUISession();
|
|
@@ -452,12 +511,17 @@ async function runClones(filePath, level, jsonMode = false) {
|
|
|
452
511
|
*/
|
|
453
512
|
async function runDeadCode(filePath, jsonMode = false) {
|
|
454
513
|
const { detectDeadCode } = require('../index');
|
|
514
|
+
const config = loadConfig();
|
|
455
515
|
const absolutePath = path.resolve(filePath);
|
|
456
516
|
|
|
457
517
|
if (!fs.existsSync(absolutePath)) {
|
|
458
518
|
console.log(chalk.red(`File not found: ${filePath}`));
|
|
459
519
|
process.exit(1);
|
|
460
520
|
}
|
|
521
|
+
if (isIgnoredPath(absolutePath, config)) {
|
|
522
|
+
console.log(chalk.red(ignoredMessage()));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
461
525
|
|
|
462
526
|
const GoUISession = require('../lib/goUiSession');
|
|
463
527
|
const session = new GoUISession();
|
|
@@ -498,13 +562,14 @@ async function runDeadCode(filePath, jsonMode = false) {
|
|
|
498
562
|
}
|
|
499
563
|
}
|
|
500
564
|
|
|
501
|
-
function listDeadCodeFiles(projectRoot) {
|
|
565
|
+
function listDeadCodeFiles(projectRoot, config) {
|
|
502
566
|
const { listDeadcodeFiles } = require('../index');
|
|
503
567
|
try {
|
|
504
568
|
const filesJson = listDeadcodeFiles(projectRoot);
|
|
505
569
|
const files = JSON.parse(filesJson);
|
|
506
570
|
if (!Array.isArray(files)) return [];
|
|
507
|
-
|
|
571
|
+
const withoutTypes = files.filter((f) => !f.endsWith('.d.ts'));
|
|
572
|
+
return filterIgnoredPaths(withoutTypes, config, projectRoot);
|
|
508
573
|
} catch (e) {
|
|
509
574
|
return [];
|
|
510
575
|
}
|
|
@@ -513,7 +578,8 @@ function listDeadCodeFiles(projectRoot) {
|
|
|
513
578
|
async function runDeadCodeRepo(jsonMode = false) {
|
|
514
579
|
const { detectDeadCode } = require('../index');
|
|
515
580
|
const projectRoot = process.cwd();
|
|
516
|
-
const
|
|
581
|
+
const config = loadConfig();
|
|
582
|
+
const files = listDeadCodeFiles(projectRoot, config);
|
|
517
583
|
|
|
518
584
|
if (files.length === 0) {
|
|
519
585
|
console.log(chalk.yellow('No files found for dead code analysis.'));
|
|
@@ -674,7 +740,7 @@ program
|
|
|
674
740
|
/**
|
|
675
741
|
* Logic to generate test or run audit (delegates to native binary)
|
|
676
742
|
*/
|
|
677
|
-
async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false) {
|
|
743
|
+
async function generateTest(filePath, methodName, testType, auditMode = false, jsonMode = false, className = null) {
|
|
678
744
|
const config = loadConfig();
|
|
679
745
|
|
|
680
746
|
// Initialize JSON session if requested
|
|
@@ -706,6 +772,16 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
706
772
|
}
|
|
707
773
|
process.exit(1);
|
|
708
774
|
}
|
|
775
|
+
if (isIgnoredPath(absolutePath, config)) {
|
|
776
|
+
const message = ignoredMessage();
|
|
777
|
+
if (jsonSession) {
|
|
778
|
+
jsonSession.displayError(message);
|
|
779
|
+
jsonSession.stop();
|
|
780
|
+
} else {
|
|
781
|
+
console.log(chalk.red(message));
|
|
782
|
+
}
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
709
785
|
|
|
710
786
|
const { runAudit, generateTest: nativeGenerateTest } = require('../index');
|
|
711
787
|
|
|
@@ -744,7 +820,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
744
820
|
resultJson = runAudit(
|
|
745
821
|
absolutePath,
|
|
746
822
|
methodName,
|
|
747
|
-
|
|
823
|
+
className,
|
|
748
824
|
testType || null,
|
|
749
825
|
JSON.stringify(config),
|
|
750
826
|
callback
|
|
@@ -753,7 +829,7 @@ async function generateTest(filePath, methodName, testType, auditMode = false, j
|
|
|
753
829
|
resultJson = nativeGenerateTest(
|
|
754
830
|
absolutePath,
|
|
755
831
|
methodName,
|
|
756
|
-
|
|
832
|
+
className,
|
|
757
833
|
testType || null,
|
|
758
834
|
JSON.stringify(config),
|
|
759
835
|
callback
|
|
@@ -814,6 +890,10 @@ program.on('--help', () => {
|
|
|
814
890
|
console.log(' 3: Fuzzy (Fuzzy structural, catches patterns with small variations)');
|
|
815
891
|
console.log(' all: Runs all detection levels (default)');
|
|
816
892
|
console.log('');
|
|
893
|
+
console.log('Ignore Paths (from tng.config.js):');
|
|
894
|
+
console.log(' IGNORE_FILES: [ "relative/path/file.js", ... ]');
|
|
895
|
+
console.log(' IGNORE_FOLDERS: [ "relative/path/folder", ... ]');
|
|
896
|
+
console.log('');
|
|
817
897
|
console.log('X-Ray Visualization:');
|
|
818
898
|
console.log(' tng src/components/MyComponent.js render --xray');
|
|
819
899
|
console.log(' tng --file=api/handler.js --method=post --xray --json');
|
|
@@ -824,8 +904,9 @@ program.on('--help', () => {
|
|
|
824
904
|
/**
|
|
825
905
|
* Logic to run impact analysis
|
|
826
906
|
*/
|
|
827
|
-
async function runImpact(filePath, methodName, jsonMode = false) {
|
|
907
|
+
async function runImpact(filePath, methodName, jsonMode = false, className = null) {
|
|
828
908
|
const { analyzeImpact } = require('../index');
|
|
909
|
+
const config = loadConfig();
|
|
829
910
|
const absolutePath = path.resolve(filePath);
|
|
830
911
|
const projectRoot = process.cwd();
|
|
831
912
|
|
|
@@ -833,10 +914,14 @@ async function runImpact(filePath, methodName, jsonMode = false) {
|
|
|
833
914
|
console.log(chalk.red(`File not found: ${filePath}`));
|
|
834
915
|
process.exit(1);
|
|
835
916
|
}
|
|
917
|
+
if (isIgnoredPath(absolutePath, config)) {
|
|
918
|
+
console.log(chalk.red(ignoredMessage()));
|
|
919
|
+
process.exit(1);
|
|
920
|
+
}
|
|
836
921
|
|
|
837
922
|
if (jsonMode) {
|
|
838
923
|
try {
|
|
839
|
-
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
|
|
924
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
|
|
840
925
|
console.log(resultJson);
|
|
841
926
|
} catch (e) {
|
|
842
927
|
console.error(JSON.stringify({ error: e.message }));
|
|
@@ -848,7 +933,7 @@ async function runImpact(filePath, methodName, jsonMode = false) {
|
|
|
848
933
|
console.log(chalk.blue(`š„ Analyzing impact for ${methodName} in ${filePath}...`));
|
|
849
934
|
|
|
850
935
|
try {
|
|
851
|
-
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName);
|
|
936
|
+
const resultJson = analyzeImpact(projectRoot, absolutePath, methodName, className);
|
|
852
937
|
const result = JSON.parse(resultJson);
|
|
853
938
|
|
|
854
939
|
if (result.status === 'error') {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -69,4 +69,4 @@ export declare function listDeadcodeFiles(projectRoot: string): string
|
|
|
69
69
|
* Analyzes the impact of changes in a specific method compared to git HEAD.
|
|
70
70
|
* Returns a JSON string containing breaking changes and impacted files.
|
|
71
71
|
*/
|
|
72
|
-
export declare function analyzeImpact(projectRoot: string, filePath: string, methodName: string): string
|
|
72
|
+
export declare function analyzeImpact(projectRoot: string, filePath: string, methodName: string, className?: string | undefined | null): string
|
package/lib/generateTestsUi.js
CHANGED
|
@@ -5,8 +5,8 @@ const chalk = require('chalk');
|
|
|
5
5
|
const GoUISession = require('./goUiSession');
|
|
6
6
|
const { getFileOutline, generateTest, ping, getUserStats } = require('../index');
|
|
7
7
|
const { loadConfig } = require('./config');
|
|
8
|
+
const { filterIgnoredPaths } = require('./ignore');
|
|
8
9
|
const { saveTestFile } = require('./saveFile');
|
|
9
|
-
const { applyFix } = require('./fixApplier');
|
|
10
10
|
|
|
11
11
|
class GenerateTestsUI {
|
|
12
12
|
constructor(cliMode = false) {
|
|
@@ -127,19 +127,23 @@ class GenerateTestsUI {
|
|
|
127
127
|
|
|
128
128
|
let title = 'Select JavaScript File';
|
|
129
129
|
if (isAudit) title = 'Select JavaScript File to Audit';
|
|
130
|
-
else if (isImpact) title = 'Select JavaScript File
|
|
130
|
+
else if (isImpact) title = 'Select JavaScript File for Regression Check';
|
|
131
131
|
else if (isXray) title = 'Select File for X-Ray';
|
|
132
132
|
else if (isTrace) title = 'Select File for Symbolic Trace';
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
while (true) {
|
|
135
|
+
const selectedName = this.goUiSession.showListView(title, items);
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
if (selectedName === 'back') return 'back';
|
|
138
|
+
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const selectedFile = path.resolve(cwd, selectedName);
|
|
141
|
+
const result = await this._showMethodsForFile(selectedFile, isAudit, isXray, isTrace, isImpact);
|
|
142
|
+
|
|
143
|
+
if (result === 'main_menu') return 'main_menu';
|
|
144
|
+
if (result === 'exit') return 'exit';
|
|
145
|
+
// If result is 'back', we loop again
|
|
146
|
+
}
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
async _showMethodsForFile(filePath, isAudit = false, isXray = false, isTrace = false, isImpact = false) {
|
|
@@ -149,60 +153,60 @@ class GenerateTestsUI {
|
|
|
149
153
|
outline = JSON.parse(result);
|
|
150
154
|
} catch (e) {
|
|
151
155
|
console.error(chalk.red(`\nError parsing file: ${e.message}\n`));
|
|
152
|
-
return
|
|
156
|
+
return 'back';
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
const methods = outline.methods || [];
|
|
156
160
|
if (methods.length === 0) {
|
|
157
161
|
this.goUiSession.showNoItems('methods');
|
|
158
|
-
return
|
|
162
|
+
return 'back';
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
const fileName = path.basename(filePath);
|
|
162
166
|
const items = methods.map(m => ({
|
|
163
|
-
name: m.class_name ? `${m.class_name}
|
|
167
|
+
name: m.class_name ? `${m.class_name}#${m.name}` : m.name,
|
|
164
168
|
path: `Function in ${fileName}`,
|
|
165
169
|
methodData: m
|
|
166
170
|
}));
|
|
167
171
|
|
|
168
172
|
let title = 'Select Method';
|
|
169
173
|
if (isAudit) title = `Select Method to Audit for ${fileName}`;
|
|
170
|
-
else if (isImpact) title = `Select Method
|
|
174
|
+
else if (isImpact) title = `Select Method for Regression Check for ${fileName}`;
|
|
171
175
|
else if (isXray) title = `Select Method to X-Ray for ${fileName}`;
|
|
172
176
|
else if (isTrace) title = `Select Method to Trace for ${fileName}`;
|
|
173
177
|
|
|
174
178
|
const selectedDisplay = this.goUiSession.showListView(title, items);
|
|
175
179
|
|
|
176
|
-
if (selectedDisplay === 'back' || !selectedDisplay) return
|
|
180
|
+
if (selectedDisplay === 'back' || !selectedDisplay) return 'back';
|
|
177
181
|
|
|
178
182
|
const selectedMethod = items.find(i => i.name === selectedDisplay)?.methodData;
|
|
179
183
|
|
|
180
184
|
if (selectedMethod) {
|
|
181
185
|
if (isTrace) {
|
|
182
|
-
await this._launchTrace(filePath, selectedMethod.name);
|
|
183
|
-
return
|
|
186
|
+
await this._launchTrace(filePath, selectedMethod.name, selectedMethod.class_name || null);
|
|
187
|
+
return 'back';
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
if (isXray) {
|
|
187
191
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, 'visualize', false, true);
|
|
188
192
|
if (choice === 'main_menu') return 'main_menu';
|
|
189
|
-
return
|
|
193
|
+
return 'back';
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
if (isImpact) {
|
|
193
197
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, false, false, true);
|
|
194
198
|
if (choice === 'main_menu') return 'main_menu';
|
|
195
|
-
return
|
|
199
|
+
return 'back';
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
if (isAudit) {
|
|
199
203
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, null, true);
|
|
200
204
|
if (choice === 'main_menu') return 'main_menu';
|
|
201
|
-
return
|
|
205
|
+
return 'back';
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
const testType = this.goUiSession.showJsTestMenu();
|
|
205
|
-
if (testType === 'back') return
|
|
209
|
+
if (testType === 'back') return 'back';
|
|
206
210
|
|
|
207
211
|
const finalType = testType === 'auto' ? null : testType;
|
|
208
212
|
const choice = await this._generateTestsForMethod(filePath, selectedMethod, finalType, false);
|
|
@@ -211,7 +215,7 @@ class GenerateTestsUI {
|
|
|
211
215
|
this._showPostGenerationMenu(choice);
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
|
-
return
|
|
218
|
+
return 'back';
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
async _generateTestsForMethod(filePath, method, testType, isAudit = false, isXray = false, isImpact = false) {
|
|
@@ -250,7 +254,7 @@ class GenerateTestsUI {
|
|
|
250
254
|
const { analyzeImpact } = require('../index');
|
|
251
255
|
const projectRoot = process.cwd();
|
|
252
256
|
|
|
253
|
-
console.log(chalk.blue(`\
|
|
257
|
+
console.log(chalk.blue(`\nAnalyzing impact for ${method.name}...`));
|
|
254
258
|
|
|
255
259
|
let impactResult;
|
|
256
260
|
try {
|
|
@@ -259,7 +263,7 @@ class GenerateTestsUI {
|
|
|
259
263
|
// Artificial delay for UX
|
|
260
264
|
await new Promise(r => setTimeout(r, 300));
|
|
261
265
|
|
|
262
|
-
const json = analyzeImpact(projectRoot, filePath, method.name);
|
|
266
|
+
const json = analyzeImpact(projectRoot, filePath, method.name, method.class_name || null);
|
|
263
267
|
progress.update('Comparing logic...', { percent: 80 });
|
|
264
268
|
|
|
265
269
|
return json;
|
|
@@ -272,7 +276,7 @@ class GenerateTestsUI {
|
|
|
272
276
|
}
|
|
273
277
|
|
|
274
278
|
await this.goUiSession.showImpactResults(impactResult);
|
|
275
|
-
return { message: '
|
|
279
|
+
return { message: 'Regression check complete' };
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
async _handleAuditFlow(filePath, method, testType) {
|
|
@@ -281,7 +285,7 @@ class GenerateTestsUI {
|
|
|
281
285
|
const fileName = path.basename(filePath);
|
|
282
286
|
const displayName = method.class_name ? `${method.class_name}#${method.name}` : `${fileName}#${method.name}`;
|
|
283
287
|
|
|
284
|
-
console.log(chalk.blue(`\
|
|
288
|
+
console.log(chalk.blue(`\nAuditing ${displayName}...`));
|
|
285
289
|
|
|
286
290
|
let auditResult;
|
|
287
291
|
try {
|
|
@@ -572,7 +576,7 @@ class GenerateTestsUI {
|
|
|
572
576
|
}
|
|
573
577
|
}
|
|
574
578
|
} catch (e) {
|
|
575
|
-
console.log(chalk.yellow(`\n
|
|
579
|
+
console.log(chalk.yellow(`\n Failed to open editor: ${e.message}`));
|
|
576
580
|
}
|
|
577
581
|
}
|
|
578
582
|
|
|
@@ -589,18 +593,20 @@ class GenerateTestsUI {
|
|
|
589
593
|
];
|
|
590
594
|
|
|
591
595
|
try {
|
|
592
|
-
|
|
596
|
+
const files = await glob(patterns, {
|
|
593
597
|
ignore,
|
|
594
598
|
absolute: true,
|
|
595
599
|
onlyFiles: true
|
|
596
600
|
});
|
|
601
|
+
const config = loadConfig();
|
|
602
|
+
return filterIgnoredPaths(files, config, process.cwd());
|
|
597
603
|
} catch (e) {
|
|
598
604
|
console.error(chalk.red(`\nError searching for files: ${e.message}\n`));
|
|
599
605
|
return [];
|
|
600
606
|
}
|
|
601
607
|
}
|
|
602
608
|
|
|
603
|
-
async _launchTrace(filePath, methodName) {
|
|
609
|
+
async _launchTrace(filePath, methodName, className) {
|
|
604
610
|
const { getSymbolicTrace } = require('../index');
|
|
605
611
|
const fs = require('fs');
|
|
606
612
|
const path = require('path');
|
|
@@ -610,7 +616,7 @@ class GenerateTestsUI {
|
|
|
610
616
|
// 1. Generate Trace (with Spinner)
|
|
611
617
|
const result = this.goUiSession.showSpinner(`Tracing ${methodName}...`, () => {
|
|
612
618
|
try {
|
|
613
|
-
const traceJson = getSymbolicTrace(filePath, methodName, null);
|
|
619
|
+
const traceJson = getSymbolicTrace(filePath, methodName, className || null);
|
|
614
620
|
const tmpDir = require('os').tmpdir();
|
|
615
621
|
const f = path.join(tmpDir, `trace-${Date.now()}.json`);
|
|
616
622
|
fs.writeFileSync(f, traceJson);
|
|
@@ -649,19 +655,21 @@ class GenerateTestsUI {
|
|
|
649
655
|
path: path.dirname(file)
|
|
650
656
|
}));
|
|
651
657
|
|
|
652
|
-
|
|
658
|
+
while (true) {
|
|
659
|
+
const selectedName = this.goUiSession.showListView('Select File for Duplicate Detection', items);
|
|
653
660
|
|
|
654
|
-
|
|
655
|
-
|
|
661
|
+
if (selectedName === 'back') return 'back';
|
|
662
|
+
if (!selectedName || selectedName === 'exit') return 'exit';
|
|
656
663
|
|
|
657
|
-
|
|
664
|
+
const selectedFile = path.resolve(cwd, selectedName);
|
|
658
665
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
666
|
+
// Show level selection
|
|
667
|
+
const level = this.goUiSession.showCloneMenu();
|
|
668
|
+
if (level === 'back') continue;
|
|
662
669
|
|
|
663
|
-
|
|
664
|
-
|
|
670
|
+
await this._runClones(selectedFile, level);
|
|
671
|
+
return 'main_menu';
|
|
672
|
+
}
|
|
665
673
|
}
|
|
666
674
|
|
|
667
675
|
async _runClones(filePath, level) {
|
package/lib/ignore.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function normalizeEntry(entry) {
|
|
4
|
+
if (typeof entry !== 'string') return null;
|
|
5
|
+
const trimmed = entry.trim();
|
|
6
|
+
if (!trimmed) return null;
|
|
7
|
+
return trimmed.replace(/\\/g, '/');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveConfigPath(entry, projectRoot) {
|
|
11
|
+
const normalized = normalizeEntry(entry);
|
|
12
|
+
if (!normalized) return null;
|
|
13
|
+
if (path.isAbsolute(normalized)) {
|
|
14
|
+
return path.resolve(normalized);
|
|
15
|
+
}
|
|
16
|
+
return path.resolve(projectRoot, normalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isIgnoredPath(filePath, config = {}, projectRoot = process.cwd()) {
|
|
20
|
+
if (!filePath) return false;
|
|
21
|
+
|
|
22
|
+
const absolutePath = path.resolve(filePath);
|
|
23
|
+
const ignoreFiles = Array.isArray(config.IGNORE_FILES) ? config.IGNORE_FILES : [];
|
|
24
|
+
const ignoreFolders = Array.isArray(config.IGNORE_FOLDERS) ? config.IGNORE_FOLDERS : [];
|
|
25
|
+
|
|
26
|
+
for (const entry of ignoreFiles) {
|
|
27
|
+
const resolved = resolveConfigPath(entry, projectRoot);
|
|
28
|
+
if (resolved && resolved === absolutePath) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const entry of ignoreFolders) {
|
|
34
|
+
const resolved = resolveConfigPath(entry, projectRoot);
|
|
35
|
+
if (!resolved) continue;
|
|
36
|
+
if (absolutePath === resolved) return true;
|
|
37
|
+
if (absolutePath.startsWith(resolved + path.sep)) return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function filterIgnoredPaths(paths, config = {}, projectRoot = process.cwd()) {
|
|
44
|
+
if (!Array.isArray(paths) || paths.length === 0) return [];
|
|
45
|
+
return paths.filter((p) => !isIgnoredPath(p, config, projectRoot));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ignoredMessage() {
|
|
49
|
+
return 'Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed.';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
isIgnoredPath,
|
|
54
|
+
filterIgnoredPaths,
|
|
55
|
+
ignoredMessage
|
|
56
|
+
};
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|