ts-analyzer 1.3.0 → 1.3.2
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/README.md +30 -16
- 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/README.md
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
# ts-analyzer
|
|
2
2
|
|
|
3
|
-
ts-analyzer
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`ts-analyzer` is a comprehensive static analysis tool for checking TypeScript and JavaScript code quality, focusing on type safety, structural complexity, and identifying potential anti-patterns.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
- Cyclomatic complexity and nesting depth analysis.
|
|
9
|
-
- Detection of anti-patterns like magic numbers, callback hell, and god files.
|
|
10
|
-
- Formatted output in Text, JSON, and HTML.
|
|
9
|
+
## ✨ Features
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- **TypeScript Safety Metrics**: Calculates your project's true type coverage by tracking `any` usage, explicit generics, optional properties, type assertions, and non-null assertions.
|
|
12
|
+
- **Code Complexity Analysis**: Measures Cyclomatic Complexity and Nesting Depth to ensure your code remains readable and maintainable.
|
|
13
|
+
- **Anti-Pattern & Code Smell Detection**: Highlights potential red flags such as "Magic Numbers", "Callback Hell", "God Files" (>500 lines), and overly complex functions (excessive parameters).
|
|
14
|
+
- **Flexible Reporting**: Generate beautiful HTML dashboards, parsable JSON payloads, or read insights directly in your terminal.
|
|
15
|
+
- **High Reliability**: The tool is extensively tested with a robust **~88% test coverage** powered by Vitest, ensuring calculations remain accurate.
|
|
16
|
+
|
|
17
|
+
## 🚀 Quick Start
|
|
18
|
+
|
|
19
|
+
Run it directly on any project using `npx`:
|
|
13
20
|
|
|
14
21
|
```bash
|
|
15
22
|
npx ts-analyzer
|
|
16
23
|
```
|
|
17
24
|
|
|
18
|
-
## Usage
|
|
25
|
+
## 📦 Usage
|
|
19
26
|
|
|
20
27
|
Analyze a specific directory:
|
|
21
28
|
```bash
|
|
@@ -23,22 +30,29 @@ ts-analyzer /path/to/project
|
|
|
23
30
|
```
|
|
24
31
|
|
|
25
32
|
Available options:
|
|
26
|
-
- `-f, --format <type>`: `text`, `json`, or `html
|
|
27
|
-
- `-e, --exclude <patterns>`: Comma-separated ignore patterns.
|
|
28
|
-
- `--no-safety`: Skip type-safety analysis.
|
|
29
|
-
- `--no-complexity`: Skip complexity analysis.
|
|
33
|
+
- `-f, --format <type>`: Select report format (`text`, `json`, or `html`).
|
|
34
|
+
- `-e, --exclude <patterns>`: Comma-separated ignore patterns (e.g. `node_modules,dist`).
|
|
35
|
+
- `--no-safety`: Skip type-safety analysis to speed up processing.
|
|
36
|
+
- `--no-complexity`: Skip structural complexity analysis.
|
|
37
|
+
|
|
38
|
+
## 🛠 Development
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
The repository uses modern open-source tooling, and features comprehensive test suites.
|
|
41
|
+
|
|
42
|
+
Install dependencies:
|
|
43
|
+
```bash
|
|
44
|
+
npm install
|
|
45
|
+
```
|
|
32
46
|
|
|
33
|
-
Run tests:
|
|
47
|
+
Run tests (powered by Vitest):
|
|
34
48
|
```bash
|
|
35
49
|
npm test
|
|
36
50
|
```
|
|
37
51
|
|
|
38
|
-
Generate coverage:
|
|
52
|
+
Generate coverage reports:
|
|
39
53
|
```bash
|
|
40
54
|
npm run test:coverage
|
|
41
55
|
```
|
|
42
56
|
|
|
43
|
-
## License
|
|
57
|
+
## 📝 License
|
|
44
58
|
MIT
|
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
|
+
|