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 CHANGED
@@ -1,21 +1,28 @@
1
1
  # ts-analyzer
2
2
 
3
- ts-analyzer is a static analysis tool for checking TypeScript code quality, focusing on type safety and structural complexity.
3
+ ![npm](https://img.shields.io/npm/v/ts-analyzer)
4
+ ![coverage](https://img.shields.io/badge/coverage-88%25-brightgreen)
5
+ ![license](https://img.shields.io/npm/l/ts-analyzer)
4
6
 
5
- ## Features
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
- - TypeScript safety metrics (type coverage, any usage, assertions).
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
- ## Quick Start
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
- ## Development
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-analyzer",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "repository": "github:amir-valizadeh/ts-analyzer",
5
5
  "description": "Comprehensive TypeScript code analyzer with type safety and complexity metrics",
6
6
  "main": "dist/src/index.js",
@@ -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
  });
@@ -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
+