pi-lens 1.3.3 → 1.3.4
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/CHANGELOG.md +5 -0
- package/README.md +6 -0
- package/clients/ast-grep-client.test.ts +152 -0
- package/clients/biome-client.test.ts +151 -0
- package/clients/complexity-client.test.ts +268 -0
- package/clients/complexity-client.ts +531 -0
- package/clients/dependency-checker.test.ts +84 -0
- package/clients/go-client.test.ts +130 -0
- package/clients/go-client.ts +245 -0
- package/clients/jscpd-client.test.ts +154 -0
- package/clients/knip-client.test.ts +137 -0
- package/clients/metrics-client.test.ts +186 -0
- package/clients/metrics-client.ts +285 -0
- package/clients/ruff-client.test.ts +147 -0
- package/clients/rust-client.test.ts +108 -0
- package/clients/rust-client.ts +266 -0
- package/clients/test-runner-client.test.ts +228 -0
- package/clients/test-runner-client.ts +649 -0
- package/clients/todo-scanner.test.ts +279 -0
- package/clients/type-coverage-client.test.ts +122 -0
- package/clients/typescript-client.test.ts +129 -0
- package/index.ts +309 -2
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,13 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
5
5
|
## [1.4.0] - 2026-03-23
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
|
+
- **Test runner feedback**: Runs corresponding test file on every write (vitest, jest, pytest). Silent if no test file exists. Disable with `--no-tests`.
|
|
9
|
+
- **Complexity metrics**: AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length.
|
|
10
|
+
- **`/lens-metrics` command**: Full project complexity scan.
|
|
8
11
|
- **Design smell rules**: New `long-method`, `long-parameter-list`, and `large-class` rules for structural quality checks.
|
|
9
12
|
- **`/design-review` command**: Analyze files for design smells. Usage: `/design-review [path]`
|
|
13
|
+
- **Go language support**: New Go client for Go projects.
|
|
14
|
+
- **Rust language support**: New Rust client for Rust projects.
|
|
10
15
|
|
|
11
16
|
### Changed
|
|
12
17
|
- **Improved ast-grep tool descriptions**: Better pattern guidance to prevent overly broad searches.
|
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-codi
|
|
|
14
14
|
| **ast-grep** | 60+ structural rules: `no-var`, `no-eval`, `no-debugger`, `no-as-any`, `prefer-template`, `no-throw-string`, `no-hardcoded-secrets`, `no-return-await`, nested ternaries, strict equality, and more |
|
|
15
15
|
| **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fixes on every write by default |
|
|
16
16
|
| **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
|
|
17
|
+
| **Test Runner** | Runs corresponding test file when you edit source code (vitest, jest, pytest). Silent if no test file exists. |
|
|
18
|
+
| **Complexity Metrics** | AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length. |
|
|
17
19
|
|
|
18
20
|
### Pre-write hints
|
|
19
21
|
|
|
@@ -71,6 +73,7 @@ Example:
|
|
|
71
73
|
| `/check-deps` | Circular dependency scan (requires madge) |
|
|
72
74
|
| `/format [file|--all]` | Apply Biome formatting |
|
|
73
75
|
| `/design-review [path]` | Analyze files for design smells (long methods, large classes, etc.) |
|
|
76
|
+
| `/lens-metrics [path]` | Full project complexity scan (Maintainability Index, Cognitive/Cyclomatic Complexity, Halstead Volume) |
|
|
74
77
|
|
|
75
78
|
### On-demand tools
|
|
76
79
|
|
|
@@ -112,6 +115,9 @@ pip install ruff
|
|
|
112
115
|
| `--no-ruff` | `false` | Disable Ruff |
|
|
113
116
|
| `--no-lsp` | `false` | Disable TypeScript LSP |
|
|
114
117
|
| `--no-madge` | `false` | Disable circular dependency checking |
|
|
118
|
+
| `--no-tests` | `false` | Disable test runner on write |
|
|
119
|
+
| `--no-go` | `false` | Disable Go linting |
|
|
120
|
+
| `--no-rust` | `false` | Disable Rust linting |
|
|
115
121
|
| `--lens-verbose` | `false` | Enable verbose logging |
|
|
116
122
|
|
|
117
123
|
---
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { AstGrepClient } from "./ast-grep-client.js";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("AstGrepClient", () => {
|
|
8
|
+
let client: AstGrepClient;
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
function createTempFile(name: string, content: string): string {
|
|
12
|
+
const filePath = path.join(tmpDir, name);
|
|
13
|
+
const dir = path.dirname(filePath);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(filePath, content);
|
|
18
|
+
return filePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
client = new AstGrepClient();
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-astgrep-test-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (tmpDir && fs.existsSync(tmpDir)) {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("isAvailable", () => {
|
|
33
|
+
it("should check ast-grep availability", () => {
|
|
34
|
+
const available = client.isAvailable();
|
|
35
|
+
expect(typeof available).toBe("boolean");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("scanFile", () => {
|
|
40
|
+
it("should return empty array for non-existent files", () => {
|
|
41
|
+
if (!client.isAvailable()) return;
|
|
42
|
+
const result = client.scanFile("/nonexistent/file.ts");
|
|
43
|
+
expect(result).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should detect var usage (no-var rule)", () => {
|
|
47
|
+
if (!client.isAvailable()) return;
|
|
48
|
+
|
|
49
|
+
const content = `
|
|
50
|
+
var x = 1;
|
|
51
|
+
var y = 2;
|
|
52
|
+
`;
|
|
53
|
+
const filePath = createTempFile("test.ts", content);
|
|
54
|
+
const result = client.scanFile(filePath);
|
|
55
|
+
|
|
56
|
+
// Should detect var usage
|
|
57
|
+
expect(result.some(d => d.rule === "no-var")).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should detect console.log usage", () => {
|
|
61
|
+
if (!client.isAvailable()) return;
|
|
62
|
+
|
|
63
|
+
const content = `
|
|
64
|
+
console.log("test");
|
|
65
|
+
`;
|
|
66
|
+
const filePath = createTempFile("test.ts", content);
|
|
67
|
+
const result = client.scanFile(filePath);
|
|
68
|
+
|
|
69
|
+
// May detect console.log depending on rules
|
|
70
|
+
expect(Array.isArray(result)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("formatDiagnostics", () => {
|
|
75
|
+
it("should format diagnostics for display", () => {
|
|
76
|
+
const diags = [
|
|
77
|
+
{
|
|
78
|
+
line: 1,
|
|
79
|
+
column: 0,
|
|
80
|
+
endLine: 1,
|
|
81
|
+
endColumn: 10,
|
|
82
|
+
severity: "warning" as const,
|
|
83
|
+
message: "Unexpected var, use let or const instead",
|
|
84
|
+
rule: "no-var",
|
|
85
|
+
file: "test.ts",
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const formatted = client.formatDiagnostics(diags);
|
|
90
|
+
expect(formatted).toContain("ast-grep");
|
|
91
|
+
expect(formatted).toContain("no-var");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should categorize by severity", () => {
|
|
95
|
+
const diags = [
|
|
96
|
+
{
|
|
97
|
+
line: 1, column: 0, endLine: 1, endColumn: 10,
|
|
98
|
+
severity: "warning" as const, message: "Warning", rule: "rule1", file: "test.ts",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
line: 2, column: 0, endLine: 2, endColumn: 10,
|
|
102
|
+
severity: "error" as const, message: "Error", rule: "rule2", file: "test.ts",
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const formatted = client.formatDiagnostics(diags);
|
|
107
|
+
expect(formatted).toContain("warning(s)");
|
|
108
|
+
expect(formatted).toContain("error(s)");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should show fixable indicator", () => {
|
|
112
|
+
const diags = [
|
|
113
|
+
{
|
|
114
|
+
line: 1, column: 0, endLine: 1, endColumn: 10,
|
|
115
|
+
severity: "warning" as const, message: "Use const", rule: "prefer-const",
|
|
116
|
+
file: "test.ts", fix: "const",
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const formatted = client.formatDiagnostics(diags);
|
|
121
|
+
expect(formatted).toContain("fixable");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("search", () => {
|
|
126
|
+
it("should search for patterns", async () => {
|
|
127
|
+
if (!client.isAvailable()) return;
|
|
128
|
+
|
|
129
|
+
createTempFile("test.ts", `
|
|
130
|
+
function test() {
|
|
131
|
+
console.log("hello");
|
|
132
|
+
}
|
|
133
|
+
`);
|
|
134
|
+
|
|
135
|
+
const result = await client.search("console.log($MSG)", "typescript", [tmpDir]);
|
|
136
|
+
|
|
137
|
+
expect(result.matches.length).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return empty matches for no match", async () => {
|
|
141
|
+
if (!client.isAvailable()) return;
|
|
142
|
+
|
|
143
|
+
createTempFile("test.ts", `
|
|
144
|
+
const x = 1;
|
|
145
|
+
`);
|
|
146
|
+
|
|
147
|
+
const result = await client.search("console.log($MSG)", "typescript", [tmpDir]);
|
|
148
|
+
|
|
149
|
+
expect(result.matches.length).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { BiomeClient } from "./biome-client.js";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("BiomeClient", () => {
|
|
8
|
+
let client: BiomeClient;
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
function createTempFile(name: string, content: string): string {
|
|
12
|
+
const filePath = path.join(tmpDir, name);
|
|
13
|
+
const dir = path.dirname(filePath);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(filePath, content);
|
|
18
|
+
return filePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
client = new BiomeClient();
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-biome-test-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (tmpDir && fs.existsSync(tmpDir)) {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("isSupportedFile", () => {
|
|
33
|
+
it("should support JS/TS files", () => {
|
|
34
|
+
expect(client.isSupportedFile("test.js")).toBe(true);
|
|
35
|
+
expect(client.isSupportedFile("test.jsx")).toBe(true);
|
|
36
|
+
expect(client.isSupportedFile("test.ts")).toBe(true);
|
|
37
|
+
expect(client.isSupportedFile("test.tsx")).toBe(true);
|
|
38
|
+
expect(client.isSupportedFile("test.mjs")).toBe(true);
|
|
39
|
+
expect(client.isSupportedFile("test.cjs")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should support CSS and JSON", () => {
|
|
43
|
+
expect(client.isSupportedFile("style.css")).toBe(true);
|
|
44
|
+
expect(client.isSupportedFile("config.json")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should not support unsupported files", () => {
|
|
48
|
+
expect(client.isSupportedFile("test.py")).toBe(false);
|
|
49
|
+
expect(client.isSupportedFile("test.md")).toBe(false);
|
|
50
|
+
expect(client.isSupportedFile("test.txt")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("isAvailable", () => {
|
|
55
|
+
it("should check biome availability", () => {
|
|
56
|
+
const available = client.isAvailable();
|
|
57
|
+
// Just verify it doesn't throw - actual availability depends on environment
|
|
58
|
+
expect(typeof available).toBe("boolean");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("checkFile", () => {
|
|
63
|
+
it("should return empty array for non-existent files", () => {
|
|
64
|
+
if (!client.isAvailable()) return;
|
|
65
|
+
const result = client.checkFile("/nonexistent/file.ts");
|
|
66
|
+
expect(result).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should return array of diagnostics for TS files", () => {
|
|
70
|
+
if (!client.isAvailable()) return;
|
|
71
|
+
|
|
72
|
+
const content = `
|
|
73
|
+
const x: number = "string";
|
|
74
|
+
`;
|
|
75
|
+
const filePath = createTempFile("test.ts", content);
|
|
76
|
+
const result = client.checkFile(filePath);
|
|
77
|
+
|
|
78
|
+
// Should return an array (may or may not have issues)
|
|
79
|
+
expect(Array.isArray(result)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("formatDiagnostics", () => {
|
|
84
|
+
it("should format diagnostics for display", () => {
|
|
85
|
+
const diags = [
|
|
86
|
+
{
|
|
87
|
+
line: 1,
|
|
88
|
+
column: 0,
|
|
89
|
+
endLine: 1,
|
|
90
|
+
endColumn: 10,
|
|
91
|
+
severity: "error" as const,
|
|
92
|
+
message: "Unexpected var",
|
|
93
|
+
rule: "noVar",
|
|
94
|
+
category: "lint" as const,
|
|
95
|
+
fixable: true,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
100
|
+
expect(formatted).toContain("Biome");
|
|
101
|
+
expect(formatted).toContain("1 issue");
|
|
102
|
+
expect(formatted).toContain("noVar");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should show fixable count", () => {
|
|
106
|
+
const diags = [
|
|
107
|
+
{
|
|
108
|
+
line: 1, column: 0, endLine: 1, endColumn: 10,
|
|
109
|
+
severity: "error" as const, message: "Error 1", rule: "rule1",
|
|
110
|
+
category: "lint" as const, fixable: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
line: 2, column: 0, endLine: 2, endColumn: 10,
|
|
114
|
+
severity: "warning" as const, message: "Warning 1", rule: "rule2",
|
|
115
|
+
category: "lint" as const, fixable: false,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
120
|
+
expect(formatted).toContain("1 fixable");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should truncate long diagnostic lists", () => {
|
|
124
|
+
const diags = Array.from({ length: 20 }, (_, i) => ({
|
|
125
|
+
line: i + 1, column: 0, endLine: i + 1, endColumn: 10,
|
|
126
|
+
severity: "warning" as const, message: `Warning ${i}`, rule: `rule${i}`,
|
|
127
|
+
category: "lint" as const, fixable: false,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const formatted = client.formatDiagnostics(diags, "test.ts");
|
|
131
|
+
expect(formatted).toContain("...");
|
|
132
|
+
expect(formatted).toContain("5 more");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("formatFile", () => {
|
|
137
|
+
it("should format a file", () => {
|
|
138
|
+
if (!client.isAvailable()) return;
|
|
139
|
+
|
|
140
|
+
const content = `const x={a:1,b:2}`;
|
|
141
|
+
const filePath = createTempFile("test.ts", content);
|
|
142
|
+
|
|
143
|
+
const result = client.formatFile(filePath);
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
|
|
146
|
+
// Check if file was formatted (should have spaces)
|
|
147
|
+
const formatted = fs.readFileSync(filePath, "utf-8");
|
|
148
|
+
expect(formatted).toContain(": "); // Should have spaces after colons
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ComplexityClient } from "./complexity-client.js";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("ComplexityClient", () => {
|
|
8
|
+
const client = new ComplexityClient();
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
// Create temp dir for test files
|
|
12
|
+
function createTempFile(name: string, content: string): string {
|
|
13
|
+
const filePath = path.join(tmpDir, name);
|
|
14
|
+
fs.writeFileSync(filePath, content);
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Setup before each test
|
|
19
|
+
function setup() {
|
|
20
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-test-"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cleanup after each test
|
|
24
|
+
function cleanup() {
|
|
25
|
+
if (tmpDir && fs.existsSync(tmpDir)) {
|
|
26
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("isSupportedFile", () => {
|
|
31
|
+
it("should support TypeScript files", () => {
|
|
32
|
+
expect(client.isSupportedFile("test.ts")).toBe(true);
|
|
33
|
+
expect(client.isSupportedFile("test.tsx")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should support JavaScript files", () => {
|
|
37
|
+
expect(client.isSupportedFile("test.js")).toBe(true);
|
|
38
|
+
expect(client.isSupportedFile("test.jsx")).toBe(true);
|
|
39
|
+
expect(client.isSupportedFile("test.mjs")).toBe(true);
|
|
40
|
+
expect(client.isSupportedFile("test.cjs")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should not support non-TS/JS files", () => {
|
|
44
|
+
expect(client.isSupportedFile("test.py")).toBe(false);
|
|
45
|
+
expect(client.isSupportedFile("test.json")).toBe(false);
|
|
46
|
+
expect(client.isSupportedFile("test.md")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("analyzeFile", () => {
|
|
51
|
+
it("should return null for non-existent files", () => {
|
|
52
|
+
const result = client.analyzeFile("/nonexistent/file.ts");
|
|
53
|
+
expect(result).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should analyze a simple function", () => {
|
|
57
|
+
setup();
|
|
58
|
+
try {
|
|
59
|
+
const content = `
|
|
60
|
+
function greet(name: string): string {
|
|
61
|
+
return "Hello, " + name;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
const filePath = createTempFile("simple.ts", content);
|
|
65
|
+
const result = client.analyzeFile(filePath);
|
|
66
|
+
|
|
67
|
+
expect(result).not.toBeNull();
|
|
68
|
+
expect(result!.functionCount).toBe(1);
|
|
69
|
+
expect(result!.cyclomaticComplexity).toBe(1);
|
|
70
|
+
expect(result!.cognitiveComplexity).toBe(0);
|
|
71
|
+
expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(1);
|
|
72
|
+
} finally {
|
|
73
|
+
cleanup();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should detect if statements in cyclomatic complexity", () => {
|
|
78
|
+
setup();
|
|
79
|
+
try {
|
|
80
|
+
const content = `
|
|
81
|
+
function check(x: number): string {
|
|
82
|
+
if (x > 0) {
|
|
83
|
+
return "positive";
|
|
84
|
+
} else if (x < 0) {
|
|
85
|
+
return "negative";
|
|
86
|
+
} else {
|
|
87
|
+
return "zero";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
const filePath = createTempFile("if-test.ts", content);
|
|
92
|
+
const result = client.analyzeFile(filePath);
|
|
93
|
+
|
|
94
|
+
expect(result).not.toBeNull();
|
|
95
|
+
// 1 base + 1 if + 1 else-if = 3
|
|
96
|
+
expect(result!.cyclomaticComplexity).toBeGreaterThanOrEqual(3);
|
|
97
|
+
} finally {
|
|
98
|
+
cleanup();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should calculate maintainability index", () => {
|
|
103
|
+
setup();
|
|
104
|
+
try {
|
|
105
|
+
const content = `
|
|
106
|
+
function simple(): number {
|
|
107
|
+
return 42;
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const filePath = createTempFile("mi-test.ts", content);
|
|
111
|
+
const result = client.analyzeFile(filePath);
|
|
112
|
+
|
|
113
|
+
expect(result).not.toBeNull();
|
|
114
|
+
expect(result!.maintainabilityIndex).toBeGreaterThan(0);
|
|
115
|
+
expect(result!.maintainabilityIndex).toBeLessThanOrEqual(100);
|
|
116
|
+
} finally {
|
|
117
|
+
cleanup();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should detect deep nesting", () => {
|
|
122
|
+
setup();
|
|
123
|
+
try {
|
|
124
|
+
const content = `
|
|
125
|
+
function deepNest(arr: number[][][][]): number {
|
|
126
|
+
for (let i = 0; i < arr.length; i++) {
|
|
127
|
+
for (let j = 0; j < arr[i].length; j++) {
|
|
128
|
+
for (let k = 0; k < arr[i][j].length; k++) {
|
|
129
|
+
for (let l = 0; l < arr[i][j][k].length; l++) {
|
|
130
|
+
if (arr[i][j][k][l] > 0) {
|
|
131
|
+
return arr[i][j][k][l];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
const filePath = createTempFile("nesting-test.ts", content);
|
|
141
|
+
const result = client.analyzeFile(filePath);
|
|
142
|
+
|
|
143
|
+
expect(result).not.toBeNull();
|
|
144
|
+
expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(5);
|
|
145
|
+
} finally {
|
|
146
|
+
cleanup();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should count cognitive complexity with nesting penalty", () => {
|
|
151
|
+
setup();
|
|
152
|
+
try {
|
|
153
|
+
const content = `
|
|
154
|
+
function nested(x: number, y: number): number {
|
|
155
|
+
if (x > 0) {
|
|
156
|
+
if (y > 0) {
|
|
157
|
+
if (x > y) {
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
const filePath = createTempFile("cognitive-test.ts", content);
|
|
166
|
+
const result = client.analyzeFile(filePath);
|
|
167
|
+
|
|
168
|
+
expect(result).not.toBeNull();
|
|
169
|
+
// Cognitive: 1 (if) + 2 (nested if) + 3 (deeply nested if) = 6
|
|
170
|
+
expect(result!.cognitiveComplexity).toBeGreaterThanOrEqual(6);
|
|
171
|
+
} finally {
|
|
172
|
+
cleanup();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should calculate halstead volume", () => {
|
|
177
|
+
setup();
|
|
178
|
+
try {
|
|
179
|
+
const content = `
|
|
180
|
+
function add(a: number, b: number): number {
|
|
181
|
+
return a + b;
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
const filePath = createTempFile("halstead-test.ts", content);
|
|
185
|
+
const result = client.analyzeFile(filePath);
|
|
186
|
+
|
|
187
|
+
expect(result).not.toBeNull();
|
|
188
|
+
expect(result!.halsteadVolume).toBeGreaterThan(0);
|
|
189
|
+
} finally {
|
|
190
|
+
cleanup();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should measure function length", () => {
|
|
195
|
+
setup();
|
|
196
|
+
try {
|
|
197
|
+
const shortContent = `function short() { return 1; }`;
|
|
198
|
+
const longContent = `
|
|
199
|
+
function long(): number {
|
|
200
|
+
const a = 1;
|
|
201
|
+
const b = 2;
|
|
202
|
+
const c = 3;
|
|
203
|
+
const d = 4;
|
|
204
|
+
const e = 5;
|
|
205
|
+
const f = 6;
|
|
206
|
+
const g = 7;
|
|
207
|
+
const h = 8;
|
|
208
|
+
const i = 9;
|
|
209
|
+
const j = 10;
|
|
210
|
+
return a + b + c + d + e + f + g + h + i + j;
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
const shortPath = createTempFile("short.ts", shortContent);
|
|
214
|
+
const longPath = createTempFile("long.ts", longContent);
|
|
215
|
+
|
|
216
|
+
const shortResult = client.analyzeFile(shortPath);
|
|
217
|
+
const longResult = client.analyzeFile(longPath);
|
|
218
|
+
|
|
219
|
+
expect(shortResult!.maxFunctionLength).toBeLessThan(longResult!.maxFunctionLength);
|
|
220
|
+
} finally {
|
|
221
|
+
cleanup();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("formatMetrics", () => {
|
|
227
|
+
it("should format metrics for display", () => {
|
|
228
|
+
const metrics = {
|
|
229
|
+
filePath: "test.ts",
|
|
230
|
+
maxNestingDepth: 4,
|
|
231
|
+
avgFunctionLength: 15,
|
|
232
|
+
maxFunctionLength: 30,
|
|
233
|
+
functionCount: 3,
|
|
234
|
+
cyclomaticComplexity: 4,
|
|
235
|
+
maxCyclomaticComplexity: 8,
|
|
236
|
+
cognitiveComplexity: 12,
|
|
237
|
+
halsteadVolume: 200,
|
|
238
|
+
maintainabilityIndex: 75,
|
|
239
|
+
linesOfCode: 100,
|
|
240
|
+
commentLines: 10,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const formatted = client.formatMetrics(metrics);
|
|
244
|
+
expect(formatted).toContain("test.ts");
|
|
245
|
+
expect(formatted).toContain("75/100");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should warn about low maintainability", () => {
|
|
249
|
+
const metrics = {
|
|
250
|
+
filePath: "bad.ts",
|
|
251
|
+
maxNestingDepth: 8,
|
|
252
|
+
avgFunctionLength: 60,
|
|
253
|
+
maxFunctionLength: 100,
|
|
254
|
+
functionCount: 5,
|
|
255
|
+
cyclomaticComplexity: 15,
|
|
256
|
+
maxCyclomaticComplexity: 25,
|
|
257
|
+
cognitiveComplexity: 50,
|
|
258
|
+
halsteadVolume: 800,
|
|
259
|
+
maintainabilityIndex: 25,
|
|
260
|
+
linesOfCode: 500,
|
|
261
|
+
commentLines: 10,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const formatted = client.formatMetrics(metrics);
|
|
265
|
+
expect(formatted).toContain("✗"); // Low MI indicator
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|