ts-analyzer 1.3.0 → 1.3.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/package.json +1 -1
- package/test/html-formatter.test.ts +51 -0
- package/test/index.test.ts +66 -0
- package/test/line-counting.test.ts +6 -0
- package/test/ts-safety.test.ts +59 -1
package/package.json
CHANGED
|
@@ -59,4 +59,55 @@ describe('generateHtmlReport', () => {
|
|
|
59
59
|
expect(html).toContain('TypeScript Safety');
|
|
60
60
|
expect(html).toContain('Code Complexity');
|
|
61
61
|
});
|
|
62
|
+
it('should generate an HTML string without optional stats (branch coverage)', () => {
|
|
63
|
+
const mockStats: Partial<ProjectStats> = {
|
|
64
|
+
files: 0,
|
|
65
|
+
totalLines: 0,
|
|
66
|
+
codeLines: 0,
|
|
67
|
+
commentLines: 0,
|
|
68
|
+
emptyLines: 0,
|
|
69
|
+
formatNumber: (n) => n.toString(),
|
|
70
|
+
formattedFileTypes: []
|
|
71
|
+
};
|
|
72
|
+
const html = generateHtmlReport(mockStats as ProjectStats);
|
|
73
|
+
expect(html).not.toContain('TypeScript Safety');
|
|
74
|
+
expect(html).not.toContain('Code Complexity');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should cover warning and danger branches for stats', () => {
|
|
78
|
+
const mockStats: Partial<ProjectStats> = {
|
|
79
|
+
files: 10,
|
|
80
|
+
totalLines: 1000,
|
|
81
|
+
codeLines: 800,
|
|
82
|
+
commentLines: 100,
|
|
83
|
+
emptyLines: 100,
|
|
84
|
+
formatNumber: (n) => n.toString(),
|
|
85
|
+
formattedFileTypes: [],
|
|
86
|
+
typescriptSafety: {
|
|
87
|
+
tsFileCount: 1, tsPercentage: '100', avgTypeCoverage: '50', // < 80 triggers warning
|
|
88
|
+
totalAnyCount: 15, // > 10 triggers danger
|
|
89
|
+
totalAssertions: 0, totalNonNullAssertions: 0, avgTypeSafetyScore: 50, overallComplexity: 'High'
|
|
90
|
+
},
|
|
91
|
+
codeComplexity: {
|
|
92
|
+
analyzedFiles: 1, avgComplexity: '10', // > 5 triggers warning
|
|
93
|
+
maxComplexity: 20, // > 15 triggers danger
|
|
94
|
+
avgNestingDepth: '1.5', maxNestingDepth: 3, avgFunctionSize: '20', totalFunctions: 1,
|
|
95
|
+
complexFiles: 1, // > 0 triggers danger
|
|
96
|
+
overallComplexity: 'High',
|
|
97
|
+
codeSmells: {
|
|
98
|
+
magicNumbers: 5, // > 0
|
|
99
|
+
callbackHell: 5, // > 0
|
|
100
|
+
godFiles: 5, // > 0
|
|
101
|
+
excessiveParameters: 5 // > 0
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const html = generateHtmlReport(mockStats as ProjectStats);
|
|
107
|
+
expect(html).toContain('warning');
|
|
108
|
+
expect(html).toContain('#ffc107'); // magicNumbers uses warning color
|
|
109
|
+
expect(html).toContain('#dc3545'); // danger color
|
|
110
|
+
});
|
|
62
111
|
});
|
|
112
|
+
|
|
113
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { analyzeProject } from '../src/index.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('analyzeProject', () => {
|
|
7
|
+
const testDir = path.join(process.cwd(), 'temp_index_test_dir');
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
11
|
+
await fs.writeFile(path.join(testDir, 'file1.ts'), 'const a = 1;\nconsole.log(a);');
|
|
12
|
+
await fs.writeFile(path.join(testDir, 'file1_duplicate.ts'), 'const b = 2;');
|
|
13
|
+
await fs.writeFile(path.join(testDir, 'file2.js'), '// comment\nlet b = 2;');
|
|
14
|
+
await fs.writeFile(path.join(testDir, 'ignored.txt'), 'not counted');
|
|
15
|
+
|
|
16
|
+
const subDir = path.join(testDir, 'subdir');
|
|
17
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
18
|
+
await fs.writeFile(path.join(subDir, 'file3.css'), 'body { margin: 0; }');
|
|
19
|
+
|
|
20
|
+
const ignoreDir = path.join(testDir, 'node_modules');
|
|
21
|
+
await fs.mkdir(ignoreDir, { recursive: true });
|
|
22
|
+
await fs.writeFile(path.join(ignoreDir, 'lib.ts'), 'const ignored = true;');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should correctly analyze a project directory with default options', async () => {
|
|
30
|
+
const stats = await analyzeProject(testDir, {
|
|
31
|
+
analyzeSafety: false,
|
|
32
|
+
analyzeComplexity: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(stats.files).toBe(4); // file1.ts, file1_duplicate.ts, file2.js, file3.css
|
|
36
|
+
expect(stats.fileTypes['.ts']).toBeDefined();
|
|
37
|
+
expect(stats.fileTypes['.js']).toBeDefined();
|
|
38
|
+
expect(stats.fileTypes['.css']).toBeDefined();
|
|
39
|
+
expect(stats.typescriptSafety).toBeNull();
|
|
40
|
+
expect(stats.codeComplexity).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should include safety and complexity when requested', async () => {
|
|
44
|
+
const stats = await analyzeProject(testDir, {
|
|
45
|
+
analyzeSafety: true,
|
|
46
|
+
analyzeComplexity: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(stats.typescriptSafety).not.toBeNull();
|
|
50
|
+
expect(stats.codeComplexity).not.toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should respect custom ignore options and extra extensions', async () => {
|
|
54
|
+
const stats = await analyzeProject(testDir, {
|
|
55
|
+
excludePatterns: ['subdir'],
|
|
56
|
+
additionalExtensions: ['.txt'],
|
|
57
|
+
analyzeSafety: false,
|
|
58
|
+
analyzeComplexity: false
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(stats.files).toBe(4); // file1.ts, file1_duplicate.ts, file2.js, ignored.txt
|
|
62
|
+
// subdir is excluded, so file3.css is skipped
|
|
63
|
+
expect(stats.fileTypes['.css']).toBeUndefined();
|
|
64
|
+
expect(stats.fileTypes['.txt']).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -42,4 +42,10 @@ const b = 2;
|
|
|
42
42
|
expect(stats.total).toBe(1); // fs.readFile('...') on empty string often gives 1 line array [""]
|
|
43
43
|
expect(stats.code).toBe(0);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('should handle reading errors gracefully', async () => {
|
|
47
|
+
const stats = await countLines('/invalid/path/that/does/not/exist.ts');
|
|
48
|
+
expect(stats.total).toBe(0);
|
|
49
|
+
expect(stats.code).toBe(0);
|
|
50
|
+
});
|
|
45
51
|
});
|
package/test/ts-safety.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { analyzeTypeScriptSafety } from '../src/typescript-safety.js';
|
|
2
|
+
import { analyzeTypeScriptSafety, calculateProjectTypeSafety } from '../src/typescript-safety.js';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
@@ -47,4 +47,62 @@ const b = <string>"foo";
|
|
|
47
47
|
|
|
48
48
|
expect(metrics.typeAssertions).toBeGreaterThan(0);
|
|
49
49
|
});
|
|
50
|
+
it('should detect complex types, interfaces, non-null assertions, and generics', async () => {
|
|
51
|
+
const content = `
|
|
52
|
+
interface MyInterface<T> {
|
|
53
|
+
value?: T;
|
|
54
|
+
}
|
|
55
|
+
const arr: Array<string> = [];
|
|
56
|
+
const obj: MyInterface<number> = {};
|
|
57
|
+
const val = obj.value!;
|
|
58
|
+
const func = <U>(arg: U) => arg;
|
|
59
|
+
class MyClass {
|
|
60
|
+
prop = 123;
|
|
61
|
+
method() { return this.prop; }
|
|
62
|
+
}
|
|
63
|
+
export type MyType = string | number;
|
|
64
|
+
import { something } from "somewhere";
|
|
65
|
+
`;
|
|
66
|
+
await fs.writeFile(testFilePath, content);
|
|
67
|
+
const metrics = await analyzeTypeScriptSafety(testFilePath);
|
|
68
|
+
|
|
69
|
+
expect(metrics.nonNullAssertions).toBeGreaterThan(0);
|
|
70
|
+
expect(metrics.optionalProperties).toBeGreaterThan(0);
|
|
71
|
+
expect(metrics.genericsCount).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle un-parseable or missing files gracefully', async () => {
|
|
75
|
+
const metrics = await analyzeTypeScriptSafety('/non/existent/path/this/should/throw/error.ts');
|
|
76
|
+
expect(metrics.typeCoverage).toBe('0.0');
|
|
77
|
+
});
|
|
50
78
|
});
|
|
79
|
+
|
|
80
|
+
describe('calculateProjectTypeSafety', () => {
|
|
81
|
+
it('should return default zero metrics for no TS files', async () => {
|
|
82
|
+
const metrics = await calculateProjectTypeSafety('.', ['index.js', 'style.css']);
|
|
83
|
+
expect(metrics.tsFileCount).toBe(0);
|
|
84
|
+
expect(metrics.avgTypeSafetyScore).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should calculate averages across multiple files', async () => {
|
|
88
|
+
// We will pass the src directory and check if it handles it.
|
|
89
|
+
// We can just rely on the existing files in the project or mock files.
|
|
90
|
+
// Let's create two temp files
|
|
91
|
+
const tempPath1 = path.join(process.cwd(), 'temp_s_test1.ts');
|
|
92
|
+
const tempPath2 = path.join(process.cwd(), 'temp_s_test2.tsx');
|
|
93
|
+
|
|
94
|
+
await fs.writeFile(tempPath1, 'const x: number = 1;');
|
|
95
|
+
await fs.writeFile(tempPath2, 'const y: any = 2;');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const metrics = await calculateProjectTypeSafety(process.cwd(), ['temp_s_test1.ts', 'temp_s_test2.tsx']);
|
|
99
|
+
expect(metrics.tsFileCount).toBe(2);
|
|
100
|
+
expect(metrics.totalAnyCount).toBe(1);
|
|
101
|
+
expect(metrics.tsPercentage).toBe('100.0');
|
|
102
|
+
} finally {
|
|
103
|
+
try { await fs.unlink(tempPath1); } catch(e) {}
|
|
104
|
+
try { await fs.unlink(tempPath2); } catch(e) {}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|