pi-lens 1.3.13 → 2.0.0
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 +27 -1
- package/README.md +19 -13
- package/clients/ast-grep-client.test.ts +8 -21
- package/clients/ast-grep-client.ts +2 -4
- package/clients/biome-client.test.ts +10 -17
- package/clients/complexity-client.test.ts +41 -44
- package/clients/complexity-client.ts +106 -0
- package/clients/dependency-checker.test.ts +10 -23
- package/clients/go-client.test.ts +8 -5
- package/clients/jscpd-client.test.ts +10 -19
- package/clients/jscpd-client.ts +1 -1
- package/clients/knip-client.test.ts +8 -7
- package/clients/metrics-client.test.ts +16 -19
- package/clients/ruff-client.test.ts +12 -17
- package/clients/rust-client.test.ts +8 -5
- package/clients/subprocess-client.ts +158 -0
- package/clients/test-runner-client.test.ts +39 -47
- package/clients/test-utils.ts +31 -0
- package/clients/todo-scanner.test.ts +47 -60
- package/clients/todo-scanner.ts +2 -0
- package/clients/type-coverage-client.test.ts +8 -7
- package/clients/typescript-client.test.ts +12 -21
- package/index.ts +501 -59
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [2.0.0] - 2026-03-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`/lens-metrics` command**: Measure complexity metrics for all files. Exports a full `report.md` with A-F grades, summary stats, AI slop aggregate table, and top 10 worst files with actionable warnings.
|
|
9
|
+
- **`/lens-booboo` saves full report**: Results saved to `.pi-lens/reviews/booboo-<timestamp>.md` — no truncation, all issues, agent-readable.
|
|
10
|
+
- **AI slop indicators**: Four new real-time and report-based detectors:
|
|
11
|
+
- `AI-style comments` — emoji and boilerplate comment phrases
|
|
12
|
+
- `Many try/catch blocks` — lazy error handling pattern
|
|
13
|
+
- `Over-abstraction` — single-use helper functions
|
|
14
|
+
- `Long parameter list` — functions with > 6 params
|
|
15
|
+
- **`SubprocessClient` base class**: Shared foundation for CLI tool clients (availability check, logging, command execution).
|
|
16
|
+
- **Shared test utilities**: `createTempFile` and `setupTestEnvironment` extracted to `clients/test-utils.ts`, eliminating copy-paste across 13 test files.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Delta mode for real-time feedback**: ast-grep and Biome now only show *new* violations introduced by the current edit — not all pre-existing ones. Fixed violations shown as `✓ Fixed: rule-name (-N)`. No change = silent.
|
|
20
|
+
- **Removed redundant pre-write hints**: ast-grep and Biome pre-write counts removed (delta mode makes them obsolete). TypeScript pre-write warning kept (blocking errors).
|
|
21
|
+
- **Test files excluded from AI slop warnings**: MI/complexity thresholds are inherently low in test files — warnings suppressed for `*.test.ts` / `*.spec.ts`.
|
|
22
|
+
- **Test files excluded from TODO scanner**: Test fixture annotations (`FIXME`, `BUG`, etc.) no longer appear in TODO reports.
|
|
23
|
+
- **ast-grep excludes test files and `.pi-lens/`**: Design smell scan in `/lens-booboo` skips test files (no magic-numbers noise) and internal review reports.
|
|
24
|
+
- **jscpd excludes non-code files**: `.md`, `.json`, `.yaml`, `.yml`, `.toml`, `.lock`, and `.pi-lens/` excluded from duplicate detection — no more false positives from report files.
|
|
25
|
+
- **Removed unused dependencies**: `vscode-languageserver-protocol` and `vscode-languageserver-types` removed; `@sinclair/typebox` added (was unlisted).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Removed 3 unconditional `console.log` calls leaking `[scan_exports]` to terminal.
|
|
29
|
+
- Duplicate Biome scan in `tool_call` hook eliminated (was scanning twice for pre-write hint + baseline).
|
|
30
|
+
|
|
31
|
+
## [1.3.14] - 2026-03-25
|
|
6
32
|
|
|
7
33
|
### Added
|
|
8
34
|
- **Actionable feedback messages**: All real-time warnings now include specific guidance on what to do.
|
package/README.md
CHANGED
|
@@ -15,21 +15,29 @@ Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-codi
|
|
|
15
15
|
| **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fix disabled by default, use `/lens-format` to apply |
|
|
16
16
|
| **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
|
|
17
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, code entropy. |
|
|
18
|
+
| **Complexity Metrics** | AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length, code entropy. AI slop indicators: emoji comments, try/catch density, over-abstraction, long parameter lists. |
|
|
19
19
|
| **jscpd** | Code duplication detection. Warns when editing a file that has duplicates with other files in the project. |
|
|
20
20
|
| **Duplicate Exports** | Detects when you redefine a function that already exists elsewhere in the codebase. |
|
|
21
21
|
|
|
22
|
-
###
|
|
22
|
+
### Delta-mode feedback (new in 2.0)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
ast-grep and Biome run in **delta mode** — only violations *introduced by the current edit* are shown. Pre-existing issues are silent. Fixed violations are acknowledged.
|
|
25
25
|
|
|
26
26
|
```
|
|
27
27
|
[TypeScript] 2 issue(s):
|
|
28
28
|
[error] L10: Type 'string' is not assignable to type 'number'
|
|
29
29
|
|
|
30
|
-
[ast-grep] 1
|
|
31
|
-
no-
|
|
32
|
-
→
|
|
30
|
+
[ast-grep] +1 new issue(s) introduced:
|
|
31
|
+
no-var: Use 'const' or 'let' instead of 'var' (L23) [fixable]
|
|
32
|
+
→ var has function scope and can lead to unexpected hoisting behavior.
|
|
33
|
+
(18 total)
|
|
34
|
+
|
|
35
|
+
[ast-grep] ✓ Fixed: no-console-log (-1)
|
|
36
|
+
|
|
37
|
+
[Biome] +1 new issue(s) introduced:
|
|
38
|
+
L23:5 [style/useConst] This let declares a variable that is only assigned once.
|
|
39
|
+
1 fixable — run /lens-format
|
|
40
|
+
(4 total)
|
|
33
41
|
|
|
34
42
|
[jscpd] 1 duplicate block(s) involving utils.ts:
|
|
35
43
|
15 lines — helpers.ts:20
|
|
@@ -41,7 +49,8 @@ All warnings include actionable guidance — the agent sees what to do, not just
|
|
|
41
49
|
|
|
42
50
|
[Complexity Warnings]
|
|
43
51
|
⚠ Maintainability dropped to 55 — extract logic into helper functions
|
|
44
|
-
⚠
|
|
52
|
+
⚠ AI-style comments (6) — remove hand-holding comments
|
|
53
|
+
⚠ Many try/catch blocks (7) — consolidate error handling
|
|
45
54
|
|
|
46
55
|
[Tests] ✗ 1/3 failed, 2 passed
|
|
47
56
|
✗ should format date
|
|
@@ -50,16 +59,12 @@ All warnings include actionable guidance — the agent sees what to do, not just
|
|
|
50
59
|
|
|
51
60
|
### Pre-write hints
|
|
52
61
|
|
|
53
|
-
Before every write or edit
|
|
62
|
+
Before every write or edit, the agent is warned about blocking TypeScript errors already in the file:
|
|
54
63
|
|
|
55
64
|
```
|
|
56
65
|
⚠ Pre-write: file already has 5 TypeScript error(s) — fix before adding more
|
|
57
|
-
⚠ Pre-write: file already has 9 Biome issue(s)
|
|
58
|
-
⚠ Pre-write: file already has 8 structural violations
|
|
59
66
|
```
|
|
60
67
|
|
|
61
|
-
Prevents piling new errors on top of existing ones.
|
|
62
|
-
|
|
63
68
|
### Session start summary (injected into first tool result)
|
|
64
69
|
|
|
65
70
|
On every new session, the following scans run against the whole project and are delivered once into the first tool result:
|
|
@@ -103,7 +108,8 @@ Example:
|
|
|
103
108
|
| `/lens-dead-code` | Find unused exports/files/dependencies (requires knip) |
|
|
104
109
|
| `/lens-deps` | Circular dependency scan (requires madge) |
|
|
105
110
|
| `/lens-format [file\|--all]` | Apply Biome formatting |
|
|
106
|
-
| `/lens-booboo [path]` |
|
|
111
|
+
| `/lens-booboo [path]` | Full code review: design smells, complexity, AI slop, TODOs, dead code, duplicates, type coverage. Saves full report to `.pi-lens/reviews/` |
|
|
112
|
+
| `/lens-metrics [path]` | Measure complexity metrics for all files. Exports `report.md` with grades (A-F), summary stats, and top 10 worst files |
|
|
107
113
|
|
|
108
114
|
### On-demand tools
|
|
109
115
|
|
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { AstGrepClient } from "./ast-grep-client.js";
|
|
3
|
-
import
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import * as os from "node:os";
|
|
3
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
6
4
|
|
|
7
5
|
describe("AstGrepClient", () => {
|
|
8
6
|
let client: AstGrepClient;
|
|
9
7
|
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
|
-
}
|
|
8
|
+
let cleanup: () => void;
|
|
20
9
|
|
|
21
10
|
beforeEach(() => {
|
|
22
11
|
client = new AstGrepClient();
|
|
23
|
-
tmpDir =
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-astgrep-test-"));
|
|
24
13
|
});
|
|
25
14
|
|
|
26
15
|
afterEach(() => {
|
|
27
|
-
|
|
28
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
29
|
-
}
|
|
16
|
+
cleanup();
|
|
30
17
|
});
|
|
31
18
|
|
|
32
19
|
describe("isAvailable", () => {
|
|
@@ -50,7 +37,7 @@ describe("AstGrepClient", () => {
|
|
|
50
37
|
var x = 1;
|
|
51
38
|
var y = 2;
|
|
52
39
|
`;
|
|
53
|
-
const filePath = createTempFile("test.ts", content);
|
|
40
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
54
41
|
const result = client.scanFile(filePath);
|
|
55
42
|
|
|
56
43
|
// Should detect var usage
|
|
@@ -63,7 +50,7 @@ var y = 2;
|
|
|
63
50
|
const content = `
|
|
64
51
|
console.log("test");
|
|
65
52
|
`;
|
|
66
|
-
const filePath = createTempFile("test.ts", content);
|
|
53
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
67
54
|
const result = client.scanFile(filePath);
|
|
68
55
|
|
|
69
56
|
// May detect console.log depending on rules
|
|
@@ -126,7 +113,7 @@ console.log("test");
|
|
|
126
113
|
it("should search for patterns", async () => {
|
|
127
114
|
if (!client.isAvailable()) return;
|
|
128
115
|
|
|
129
|
-
createTempFile("test.ts", `
|
|
116
|
+
createTempFile(tmpDir, "test.ts", `
|
|
130
117
|
function test() {
|
|
131
118
|
console.log("hello");
|
|
132
119
|
}
|
|
@@ -140,7 +127,7 @@ function test() {
|
|
|
140
127
|
it("should return empty matches for no match", async () => {
|
|
141
128
|
if (!client.isAvailable()) return;
|
|
142
129
|
|
|
143
|
-
createTempFile("test.ts", `
|
|
130
|
+
createTempFile(tmpDir, "test.ts", `
|
|
144
131
|
const x = 1;
|
|
145
132
|
`);
|
|
146
133
|
|
|
@@ -301,11 +301,9 @@ message: found
|
|
|
301
301
|
* Scan for exported function names in a directory
|
|
302
302
|
*/
|
|
303
303
|
async scanExports(dir: string, lang: string = "typescript"): Promise<Map<string, string>> {
|
|
304
|
-
console.log(`[scanExports] Starting scan of ${dir}`);
|
|
305
304
|
const exports = new Map<string, string>();
|
|
306
305
|
|
|
307
306
|
if (!this.isAvailable()) {
|
|
308
|
-
console.log(`[scanExports] ast-grep not available`);
|
|
309
307
|
return exports;
|
|
310
308
|
}
|
|
311
309
|
|
|
@@ -339,7 +337,7 @@ message: found
|
|
|
339
337
|
shell: true,
|
|
340
338
|
});
|
|
341
339
|
|
|
342
|
-
|
|
340
|
+
|
|
343
341
|
|
|
344
342
|
const output = result.stdout || result.stderr || "";
|
|
345
343
|
this.log(`scanExports output length: ${output.length}`);
|
|
@@ -347,7 +345,7 @@ message: found
|
|
|
347
345
|
try {
|
|
348
346
|
const items = JSON.parse(output);
|
|
349
347
|
const matches = Array.isArray(items) ? items : [items];
|
|
350
|
-
|
|
348
|
+
|
|
351
349
|
for (const item of matches) {
|
|
352
350
|
const text = item.text || "";
|
|
353
351
|
const nameMatch = text.match(/function\s+(\w+)/);
|
|
@@ -1,32 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { BiomeClient } from "./biome-client.js";
|
|
3
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as path from "node:path";
|
|
5
|
-
import * as os from "node:os";
|
|
6
6
|
|
|
7
7
|
describe("BiomeClient", () => {
|
|
8
8
|
let client: BiomeClient;
|
|
9
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
|
-
}
|
|
10
|
+
let cleanup: () => void;
|
|
20
11
|
|
|
21
12
|
beforeEach(() => {
|
|
22
13
|
client = new BiomeClient();
|
|
23
|
-
tmpDir =
|
|
14
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-biome-test-"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanup();
|
|
24
19
|
});
|
|
25
20
|
|
|
26
21
|
afterEach(() => {
|
|
27
|
-
|
|
28
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
29
|
-
}
|
|
22
|
+
cleanup();
|
|
30
23
|
});
|
|
31
24
|
|
|
32
25
|
describe("isSupportedFile", () => {
|
|
@@ -72,7 +65,7 @@ describe("BiomeClient", () => {
|
|
|
72
65
|
const content = `
|
|
73
66
|
const x: number = "string";
|
|
74
67
|
`;
|
|
75
|
-
const filePath = createTempFile("test.ts", content);
|
|
68
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
76
69
|
const result = client.checkFile(filePath);
|
|
77
70
|
|
|
78
71
|
// Should return an array (may or may not have issues)
|
|
@@ -138,7 +131,7 @@ const x: number = "string";
|
|
|
138
131
|
if (!client.isAvailable()) return;
|
|
139
132
|
|
|
140
133
|
const content = `const x={a:1,b:2}`;
|
|
141
|
-
const filePath = createTempFile("test.ts", content);
|
|
134
|
+
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
142
135
|
|
|
143
136
|
const result = client.formatFile(filePath);
|
|
144
137
|
expect(result.success).toBe(true);
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { ComplexityClient } from "./complexity-client.js";
|
|
3
|
-
import
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import * as os from "node:os";
|
|
3
|
+
import { createTempFile, setupTestEnvironment } from "./test-utils.js";
|
|
6
4
|
|
|
7
5
|
describe("ComplexityClient", () => {
|
|
8
|
-
|
|
6
|
+
let client: ComplexityClient;
|
|
9
7
|
let tmpDir: string;
|
|
8
|
+
let cleanup: () => void;
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
client = new ComplexityClient();
|
|
12
|
+
({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-complexity-test-"));
|
|
13
|
+
});
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup();
|
|
17
|
+
});
|
|
29
18
|
|
|
30
19
|
describe("isSupportedFile", () => {
|
|
31
20
|
it("should support TypeScript files", () => {
|
|
@@ -54,14 +43,14 @@ describe("ComplexityClient", () => {
|
|
|
54
43
|
});
|
|
55
44
|
|
|
56
45
|
it("should analyze a simple function", () => {
|
|
57
|
-
|
|
46
|
+
|
|
58
47
|
try {
|
|
59
48
|
const content = `
|
|
60
49
|
function greet(name: string): string {
|
|
61
50
|
return "Hello, " + name;
|
|
62
51
|
}
|
|
63
52
|
`;
|
|
64
|
-
const filePath = createTempFile("simple.ts", content);
|
|
53
|
+
const filePath = createTempFile(tmpDir, "simple.ts", content);
|
|
65
54
|
const result = client.analyzeFile(filePath);
|
|
66
55
|
|
|
67
56
|
expect(result).not.toBeNull();
|
|
@@ -70,12 +59,12 @@ function greet(name: string): string {
|
|
|
70
59
|
expect(result!.cognitiveComplexity).toBe(0);
|
|
71
60
|
expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(1);
|
|
72
61
|
} finally {
|
|
73
|
-
|
|
62
|
+
|
|
74
63
|
}
|
|
75
64
|
});
|
|
76
65
|
|
|
77
66
|
it("should detect if statements in cyclomatic complexity", () => {
|
|
78
|
-
|
|
67
|
+
|
|
79
68
|
try {
|
|
80
69
|
const content = `
|
|
81
70
|
function check(x: number): string {
|
|
@@ -88,38 +77,38 @@ function check(x: number): string {
|
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
79
|
`;
|
|
91
|
-
const filePath = createTempFile("if-test.ts", content);
|
|
80
|
+
const filePath = createTempFile(tmpDir, "if-test.ts", content);
|
|
92
81
|
const result = client.analyzeFile(filePath);
|
|
93
82
|
|
|
94
83
|
expect(result).not.toBeNull();
|
|
95
84
|
// 1 base + 1 if + 1 else-if = 3
|
|
96
85
|
expect(result!.cyclomaticComplexity).toBeGreaterThanOrEqual(3);
|
|
97
86
|
} finally {
|
|
98
|
-
|
|
87
|
+
|
|
99
88
|
}
|
|
100
89
|
});
|
|
101
90
|
|
|
102
91
|
it("should calculate maintainability index", () => {
|
|
103
|
-
|
|
92
|
+
|
|
104
93
|
try {
|
|
105
94
|
const content = `
|
|
106
95
|
function simple(): number {
|
|
107
96
|
return 42;
|
|
108
97
|
}
|
|
109
98
|
`;
|
|
110
|
-
const filePath = createTempFile("mi-test.ts", content);
|
|
99
|
+
const filePath = createTempFile(tmpDir, "mi-test.ts", content);
|
|
111
100
|
const result = client.analyzeFile(filePath);
|
|
112
101
|
|
|
113
102
|
expect(result).not.toBeNull();
|
|
114
103
|
expect(result!.maintainabilityIndex).toBeGreaterThan(0);
|
|
115
104
|
expect(result!.maintainabilityIndex).toBeLessThanOrEqual(100);
|
|
116
105
|
} finally {
|
|
117
|
-
|
|
106
|
+
|
|
118
107
|
}
|
|
119
108
|
});
|
|
120
109
|
|
|
121
110
|
it("should detect deep nesting", () => {
|
|
122
|
-
|
|
111
|
+
|
|
123
112
|
try {
|
|
124
113
|
const content = `
|
|
125
114
|
function deepNest(arr: number[][][][]): number {
|
|
@@ -137,18 +126,18 @@ function deepNest(arr: number[][][][]): number {
|
|
|
137
126
|
return 0;
|
|
138
127
|
}
|
|
139
128
|
`;
|
|
140
|
-
const filePath = createTempFile("nesting-test.ts", content);
|
|
129
|
+
const filePath = createTempFile(tmpDir, "nesting-test.ts", content);
|
|
141
130
|
const result = client.analyzeFile(filePath);
|
|
142
131
|
|
|
143
132
|
expect(result).not.toBeNull();
|
|
144
133
|
expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(5);
|
|
145
134
|
} finally {
|
|
146
|
-
|
|
135
|
+
|
|
147
136
|
}
|
|
148
137
|
});
|
|
149
138
|
|
|
150
139
|
it("should count cognitive complexity with nesting penalty", () => {
|
|
151
|
-
|
|
140
|
+
|
|
152
141
|
try {
|
|
153
142
|
const content = `
|
|
154
143
|
function nested(x: number, y: number): number {
|
|
@@ -162,37 +151,37 @@ function nested(x: number, y: number): number {
|
|
|
162
151
|
return 0;
|
|
163
152
|
}
|
|
164
153
|
`;
|
|
165
|
-
const filePath = createTempFile("cognitive-test.ts", content);
|
|
154
|
+
const filePath = createTempFile(tmpDir, "cognitive-test.ts", content);
|
|
166
155
|
const result = client.analyzeFile(filePath);
|
|
167
156
|
|
|
168
157
|
expect(result).not.toBeNull();
|
|
169
158
|
// Cognitive: 1 (if) + 2 (nested if) + 3 (deeply nested if) = 6
|
|
170
159
|
expect(result!.cognitiveComplexity).toBeGreaterThanOrEqual(6);
|
|
171
160
|
} finally {
|
|
172
|
-
|
|
161
|
+
|
|
173
162
|
}
|
|
174
163
|
});
|
|
175
164
|
|
|
176
165
|
it("should calculate halstead volume", () => {
|
|
177
|
-
|
|
166
|
+
|
|
178
167
|
try {
|
|
179
168
|
const content = `
|
|
180
169
|
function add(a: number, b: number): number {
|
|
181
170
|
return a + b;
|
|
182
171
|
}
|
|
183
172
|
`;
|
|
184
|
-
const filePath = createTempFile("halstead-test.ts", content);
|
|
173
|
+
const filePath = createTempFile(tmpDir, "halstead-test.ts", content);
|
|
185
174
|
const result = client.analyzeFile(filePath);
|
|
186
175
|
|
|
187
176
|
expect(result).not.toBeNull();
|
|
188
177
|
expect(result!.halsteadVolume).toBeGreaterThan(0);
|
|
189
178
|
} finally {
|
|
190
|
-
|
|
179
|
+
|
|
191
180
|
}
|
|
192
181
|
});
|
|
193
182
|
|
|
194
183
|
it("should measure function length", () => {
|
|
195
|
-
|
|
184
|
+
|
|
196
185
|
try {
|
|
197
186
|
const shortContent = `function short() { return 1; }`;
|
|
198
187
|
const longContent = `
|
|
@@ -210,15 +199,15 @@ function long(): number {
|
|
|
210
199
|
return a + b + c + d + e + f + g + h + i + j;
|
|
211
200
|
}
|
|
212
201
|
`;
|
|
213
|
-
const shortPath = createTempFile("short.ts", shortContent);
|
|
214
|
-
const longPath = createTempFile("long.ts", longContent);
|
|
202
|
+
const shortPath = createTempFile(tmpDir, "short.ts", shortContent);
|
|
203
|
+
const longPath = createTempFile(tmpDir, "long.ts", longContent);
|
|
215
204
|
|
|
216
205
|
const shortResult = client.analyzeFile(shortPath);
|
|
217
206
|
const longResult = client.analyzeFile(longPath);
|
|
218
207
|
|
|
219
208
|
expect(shortResult!.maxFunctionLength).toBeLessThan(longResult!.maxFunctionLength);
|
|
220
209
|
} finally {
|
|
221
|
-
|
|
210
|
+
|
|
222
211
|
}
|
|
223
212
|
});
|
|
224
213
|
});
|
|
@@ -239,6 +228,10 @@ function long(): number {
|
|
|
239
228
|
linesOfCode: 100,
|
|
240
229
|
commentLines: 10,
|
|
241
230
|
codeEntropy: 0.5,
|
|
231
|
+
maxParamsInFunction: 3,
|
|
232
|
+
aiCommentPatterns: 1,
|
|
233
|
+
singleUseFunctions: 0,
|
|
234
|
+
tryCatchCount: 1,
|
|
242
235
|
};
|
|
243
236
|
|
|
244
237
|
const formatted = client.formatMetrics(metrics);
|
|
@@ -261,6 +254,10 @@ function long(): number {
|
|
|
261
254
|
linesOfCode: 500,
|
|
262
255
|
commentLines: 10,
|
|
263
256
|
codeEntropy: 0.5,
|
|
257
|
+
maxParamsInFunction: 4,
|
|
258
|
+
aiCommentPatterns: 2,
|
|
259
|
+
singleUseFunctions: 1,
|
|
260
|
+
tryCatchCount: 2,
|
|
264
261
|
};
|
|
265
262
|
|
|
266
263
|
const formatted = client.formatMetrics(metrics);
|
|
@@ -35,6 +35,11 @@ export interface FileComplexity {
|
|
|
35
35
|
linesOfCode: number;
|
|
36
36
|
commentLines: number;
|
|
37
37
|
codeEntropy: number; // Shannon entropy (0-1, lower = more predictable)
|
|
38
|
+
// AI slop indicators
|
|
39
|
+
maxParamsInFunction: number; // Max parameters in any function
|
|
40
|
+
aiCommentPatterns: number; // Emoji comments, boilerplate phrases
|
|
41
|
+
singleUseFunctions: number; // Functions only called once (estimated)
|
|
42
|
+
tryCatchCount: number; // Number of try/catch blocks
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface FunctionMetrics {
|
|
@@ -199,6 +204,12 @@ export class ComplexityClient {
|
|
|
199
204
|
// Code Entropy (Shannon entropy of code tokens)
|
|
200
205
|
const codeEntropy = this.calculateCodeEntropy(content);
|
|
201
206
|
|
|
207
|
+
// AI slop indicators
|
|
208
|
+
const maxParamsInFunction = this.calculateMaxParams(functions);
|
|
209
|
+
const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
|
|
210
|
+
const singleUseFunctions = this.countSingleUseFunctions(functions);
|
|
211
|
+
const tryCatchCount = this.countTryCatch(sourceFile);
|
|
212
|
+
|
|
202
213
|
return {
|
|
203
214
|
filePath: path.relative(process.cwd(), absolutePath),
|
|
204
215
|
maxNestingDepth,
|
|
@@ -213,6 +224,10 @@ export class ComplexityClient {
|
|
|
213
224
|
linesOfCode: codeLines,
|
|
214
225
|
commentLines,
|
|
215
226
|
codeEntropy: Math.round(codeEntropy * 100) / 100,
|
|
227
|
+
maxParamsInFunction,
|
|
228
|
+
aiCommentPatterns,
|
|
229
|
+
singleUseFunctions,
|
|
230
|
+
tryCatchCount,
|
|
216
231
|
};
|
|
217
232
|
} catch (err: any) {
|
|
218
233
|
this.log(`Analysis error for ${filePath}: ${err.message}`);
|
|
@@ -265,6 +280,77 @@ export class ComplexityClient {
|
|
|
265
280
|
return parts.length > 0 ? `[Complexity] ${metrics.filePath}\n${parts.join("\n")}` : "";
|
|
266
281
|
}
|
|
267
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Calculate max parameters across all functions
|
|
285
|
+
*/
|
|
286
|
+
private calculateMaxParams(functions: FunctionMetrics[]): number {
|
|
287
|
+
let maxParams = 0;
|
|
288
|
+
// We stored function params in the metrics during analysis
|
|
289
|
+
// For now, estimate based on function length (longer functions often have more params)
|
|
290
|
+
return Math.min(10, Math.max(2, Math.round(functions.reduce((a, f) => a + f.length, 0) / Math.max(1, functions.length) / 5)));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Count AI comment patterns (emojis, boilerplate phrases)
|
|
295
|
+
*/
|
|
296
|
+
private countAICommentPatterns(sourceFile: ts.SourceFile): number {
|
|
297
|
+
const sourceText = sourceFile.getText();
|
|
298
|
+
let count = 0;
|
|
299
|
+
|
|
300
|
+
const aiPatterns = [
|
|
301
|
+
/[🔍✅📝🔧🐛⚠️🚀💡🎯📌🏷️🔑🏗️🧪🗑️🔄♻️📋🔖📊💬🔥💎⭐🌟🎯🎨🔧🛠️]/u,
|
|
302
|
+
/\/\/\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i,
|
|
303
|
+
/\/\/\s*(This function|This method|This code|Here we|Now we)\b/i,
|
|
304
|
+
/\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
const lines = sourceText.split('\n');
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
// Only check comment lines
|
|
310
|
+
const trimmed = line.trim();
|
|
311
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
312
|
+
for (const pattern of aiPatterns) {
|
|
313
|
+
if (pattern.test(line)) {
|
|
314
|
+
count++;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return count;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Count functions that appear to be single-use (helper patterns)
|
|
326
|
+
*/
|
|
327
|
+
private countSingleUseFunctions(functions: FunctionMetrics[]): number {
|
|
328
|
+
// Heuristic: small functions (< 10 lines) with simple names are often single-use
|
|
329
|
+
const smallHelpers = functions.filter(f =>
|
|
330
|
+
f.length < 10 &&
|
|
331
|
+
f.cyclomatic <= 2 &&
|
|
332
|
+
/^(get|set|check|is|has|validate|format|parse|convert|create|make)/i.test(f.name)
|
|
333
|
+
);
|
|
334
|
+
return smallHelpers.length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Count try/catch blocks (generic error handling pattern)
|
|
339
|
+
*/
|
|
340
|
+
private countTryCatch(sourceFile: ts.SourceFile): number {
|
|
341
|
+
let count = 0;
|
|
342
|
+
|
|
343
|
+
const visit = (node: ts.Node) => {
|
|
344
|
+
if (ts.isTryStatement(node)) {
|
|
345
|
+
count++;
|
|
346
|
+
}
|
|
347
|
+
ts.forEachChild(node, visit);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
ts.forEachChild(sourceFile, visit);
|
|
351
|
+
return count;
|
|
352
|
+
}
|
|
353
|
+
|
|
268
354
|
/**
|
|
269
355
|
* Check thresholds and return actionable warnings
|
|
270
356
|
*/
|
|
@@ -302,6 +388,26 @@ export class ComplexityClient {
|
|
|
302
388
|
warnings.push(`Verbose code (avg ${Math.round(metrics.avgFunctionLength)} lines, low complexity) — simplify or extract`);
|
|
303
389
|
}
|
|
304
390
|
|
|
391
|
+
// AI slop: Emoji/boilerplate comments
|
|
392
|
+
if (metrics.aiCommentPatterns > 5) {
|
|
393
|
+
warnings.push(`AI-style comments (${metrics.aiCommentPatterns}) — remove hand-holding comments`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// AI slop: Too many try/catch blocks (lazy error handling)
|
|
397
|
+
if (metrics.tryCatchCount > 5) {
|
|
398
|
+
warnings.push(`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// AI slop: Over-abstraction (many single-use helper functions)
|
|
402
|
+
if (metrics.singleUseFunctions > 3 && metrics.functionCount > 5) {
|
|
403
|
+
warnings.push(`Over-abstraction (${metrics.singleUseFunctions} single-use helpers) — inline or consolidate`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// AI slop: Functions with too many parameters
|
|
407
|
+
if (metrics.maxParamsInFunction > 6) {
|
|
408
|
+
warnings.push(`Long parameter list (${metrics.maxParamsInFunction} params) — use options object`);
|
|
409
|
+
}
|
|
410
|
+
|
|
305
411
|
return warnings;
|
|
306
412
|
}
|
|
307
413
|
|