pi-lens 2.0.37 → 2.0.39
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 +16 -0
- package/README.md +1 -1
- package/clients/architect-client.js +50 -20
- package/clients/architect-client.ts +61 -22
- package/clients/ast-grep-client.js +32 -127
- package/clients/ast-grep-client.test.js +2 -14
- package/clients/ast-grep-client.test.ts +2 -16
- package/clients/ast-grep-client.ts +34 -150
- package/clients/auto-loop.js +117 -0
- package/clients/auto-loop.ts +171 -0
- package/clients/biome-client.js +25 -22
- package/clients/biome-client.ts +28 -25
- package/clients/complexity-client.js +111 -74
- package/clients/complexity-client.ts +149 -105
- package/clients/dependency-checker.js +16 -30
- package/clients/dependency-checker.ts +15 -35
- package/clients/fix-scanners.js +195 -0
- package/clients/fix-scanners.ts +297 -0
- package/clients/interviewer-templates.js +75 -0
- package/clients/interviewer-templates.ts +90 -0
- package/clients/interviewer.js +73 -101
- package/clients/interviewer.ts +195 -140
- package/clients/knip-client.js +12 -4
- package/clients/knip-client.ts +21 -16
- package/clients/metrics-history.js +215 -0
- package/clients/metrics-history.ts +300 -0
- package/clients/scan-architectural-debt.js +62 -72
- package/clients/scan-architectural-debt.ts +79 -64
- package/clients/scan-utils.js +98 -0
- package/clients/scan-utils.ts +112 -0
- package/clients/sg-runner.js +138 -0
- package/clients/sg-runner.ts +168 -0
- package/clients/subprocess-client.js +0 -37
- package/clients/subprocess-client.ts +0 -60
- package/clients/ts-service.ts +1 -7
- package/clients/type-safety-client.js +3 -8
- package/clients/type-safety-client.ts +10 -14
- package/clients/typescript-client.js +55 -56
- package/clients/typescript-client.ts +94 -52
- package/default-architect.yaml +87 -0
- package/index.ts +143 -1165
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/large-class.yml +6 -2
- package/rules/ast-grep-rules/rules/long-method.yml +1 -1
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
- package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
|
@@ -95,46 +95,32 @@ export class DependencyChecker {
|
|
|
95
95
|
importsChanged(filePath) {
|
|
96
96
|
const normalized = path.resolve(filePath);
|
|
97
97
|
if (!fs.existsSync(normalized)) {
|
|
98
|
-
// File deleted, remove from cache
|
|
99
98
|
this.importCache.delete(normalized);
|
|
100
99
|
return true;
|
|
101
100
|
}
|
|
102
101
|
const stat = fs.statSync(normalized);
|
|
103
|
-
const mtime = stat.mtimeMs;
|
|
104
102
|
const cached = this.importCache.get(normalized);
|
|
105
|
-
//
|
|
106
|
-
if (cached && cached.timestamp >=
|
|
103
|
+
// Fast path: timestamp hasn't changed
|
|
104
|
+
if (cached && cached.timestamp >= stat.mtimeMs) {
|
|
107
105
|
return false;
|
|
108
106
|
}
|
|
109
|
-
//
|
|
107
|
+
// Compare actual imports
|
|
110
108
|
const newImports = this.extractImports(normalized);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for (const imp of newImports) {
|
|
122
|
-
if (!cached.imports.has(imp)) {
|
|
123
|
-
this.importCache.set(normalized, newEntry);
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
for (const imp of cached.imports) {
|
|
128
|
-
if (!newImports.has(imp)) {
|
|
129
|
-
this.importCache.set(normalized, newEntry);
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Imports are the same, just update timestamp
|
|
134
|
-
this.importCache.set(normalized, newEntry);
|
|
109
|
+
const hasChanged = !cached || !this.setsEqual(cached.imports, newImports);
|
|
110
|
+
// Update cache
|
|
111
|
+
this.importCache.set(normalized, { imports: newImports, timestamp: stat.mtimeMs });
|
|
112
|
+
return hasChanged;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if two sets have the same elements
|
|
116
|
+
*/
|
|
117
|
+
setsEqual(a, b) {
|
|
118
|
+
if (a.size !== b.size)
|
|
135
119
|
return false;
|
|
120
|
+
for (const item of a) {
|
|
121
|
+
if (!b.has(item))
|
|
122
|
+
return false;
|
|
136
123
|
}
|
|
137
|
-
this.importCache.set(normalized, newEntry);
|
|
138
124
|
return true;
|
|
139
125
|
}
|
|
140
126
|
/**
|
|
@@ -139,55 +139,35 @@ export class DependencyChecker {
|
|
|
139
139
|
const normalized = path.resolve(filePath);
|
|
140
140
|
|
|
141
141
|
if (!fs.existsSync(normalized)) {
|
|
142
|
-
// File deleted, remove from cache
|
|
143
142
|
this.importCache.delete(normalized);
|
|
144
143
|
return true;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
const stat = fs.statSync(normalized);
|
|
148
|
-
const mtime = stat.mtimeMs;
|
|
149
|
-
|
|
150
147
|
const cached = this.importCache.get(normalized);
|
|
151
148
|
|
|
152
|
-
//
|
|
153
|
-
if (cached && cached.timestamp >=
|
|
149
|
+
// Fast path: timestamp hasn't changed
|
|
150
|
+
if (cached && cached.timestamp >= stat.mtimeMs) {
|
|
154
151
|
return false;
|
|
155
152
|
}
|
|
156
153
|
|
|
157
|
-
//
|
|
154
|
+
// Compare actual imports
|
|
158
155
|
const newImports = this.extractImports(normalized);
|
|
159
|
-
const
|
|
160
|
-
imports: newImports,
|
|
161
|
-
timestamp: mtime,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// Check if imports actually changed
|
|
165
|
-
if (cached) {
|
|
166
|
-
if (cached.imports.size !== newImports.size) {
|
|
167
|
-
this.importCache.set(normalized, newEntry);
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
for (const imp of newImports) {
|
|
172
|
-
if (!cached.imports.has(imp)) {
|
|
173
|
-
this.importCache.set(normalized, newEntry);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
156
|
+
const hasChanged = !cached || !this.setsEqual(cached.imports, newImports);
|
|
177
157
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
158
|
+
// Update cache
|
|
159
|
+
this.importCache.set(normalized, { imports: newImports, timestamp: stat.mtimeMs });
|
|
160
|
+
return hasChanged;
|
|
161
|
+
}
|
|
184
162
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Check if two sets have the same elements
|
|
165
|
+
*/
|
|
166
|
+
private setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
|
167
|
+
if (a.size !== b.size) return false;
|
|
168
|
+
for (const item of a) {
|
|
169
|
+
if (!b.has(item)) return false;
|
|
188
170
|
}
|
|
189
|
-
|
|
190
|
-
this.importCache.set(normalized, newEntry);
|
|
191
171
|
return true;
|
|
192
172
|
}
|
|
193
173
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner functions for fix.ts
|
|
3
|
+
*
|
|
4
|
+
* Each scanner encapsulates one type of issue detection:
|
|
5
|
+
* - scanDuplicates: JSCPD duplicate code detection
|
|
6
|
+
* - scanDeadCode: Knip dead code detection
|
|
7
|
+
* - scanAstGrep: Structural linting via ast-grep
|
|
8
|
+
* - scanBiome: Remaining Biome lint issues
|
|
9
|
+
* - scanSlop: AI slop indicators (high complexity patterns)
|
|
10
|
+
*/
|
|
11
|
+
import * as childProcess from "node:child_process";
|
|
12
|
+
import * as nodeFs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { shouldIgnoreFile } from "./scan-utils.js";
|
|
15
|
+
const DEBUG_LOG = path.join(process.env.HOME || process.env.USERPROFILE || ".", "pi-lens-debug.log");
|
|
16
|
+
function dbg(msg) {
|
|
17
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
18
|
+
try {
|
|
19
|
+
nodeFs.appendFileSync(DEBUG_LOG, line);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Ignored
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Scan for duplicate code blocks using JSCPD
|
|
27
|
+
*/
|
|
28
|
+
export function scanDuplicates(jscpd, targetPath, isTsProject) {
|
|
29
|
+
if (!jscpd.isAvailable())
|
|
30
|
+
return [];
|
|
31
|
+
const jscpdResult = jscpd.scan(targetPath);
|
|
32
|
+
return jscpdResult.clones.filter((c) => {
|
|
33
|
+
if (isTsProject && (c.fileA.endsWith(".js") || c.fileB.endsWith(".js")))
|
|
34
|
+
return false;
|
|
35
|
+
return path.resolve(c.fileA) !== path.resolve(c.fileB);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Scan for dead code using Knip
|
|
40
|
+
*/
|
|
41
|
+
export function scanDeadCode(knip, targetPath, isTsProject) {
|
|
42
|
+
if (!knip.isAvailable())
|
|
43
|
+
return [];
|
|
44
|
+
const knipResult = knip.analyze(targetPath);
|
|
45
|
+
return knipResult.issues.filter((i) => {
|
|
46
|
+
if (!i.file)
|
|
47
|
+
return true;
|
|
48
|
+
return !shouldIgnoreFile(i.file, isTsProject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scan for structural issues using ast-grep
|
|
53
|
+
*/
|
|
54
|
+
export function scanAstGrep(targetPath, isTsProject, configPath) {
|
|
55
|
+
const hasSg = nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
|
|
56
|
+
childProcess.spawnSync("npx", ["sg", "--version"], { encoding: "utf-8", timeout: 5000, shell: true }).status === 0;
|
|
57
|
+
if (!hasSg)
|
|
58
|
+
return [];
|
|
59
|
+
const result = childProcess.spawnSync("npx", [
|
|
60
|
+
"sg",
|
|
61
|
+
"scan",
|
|
62
|
+
"--config",
|
|
63
|
+
configPath,
|
|
64
|
+
"--json",
|
|
65
|
+
"--globs",
|
|
66
|
+
"!**/*.test.ts",
|
|
67
|
+
"--globs",
|
|
68
|
+
"!**/*.spec.ts",
|
|
69
|
+
"--globs",
|
|
70
|
+
"!**/test-utils.ts",
|
|
71
|
+
"--globs",
|
|
72
|
+
"!**/.pi-lens/**",
|
|
73
|
+
...(isTsProject ? ["--globs", "!**/*.js"] : []),
|
|
74
|
+
targetPath,
|
|
75
|
+
], {
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
timeout: 30000,
|
|
78
|
+
shell: true,
|
|
79
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
80
|
+
});
|
|
81
|
+
const raw = result.stdout?.trim() ?? "";
|
|
82
|
+
const items = raw.startsWith("[")
|
|
83
|
+
? (() => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
})()
|
|
91
|
+
: raw.split("\n").flatMap((l) => {
|
|
92
|
+
try {
|
|
93
|
+
return [JSON.parse(l)];
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const astIssues = [];
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const rule = item.ruleId || item.rule?.title || item.name || "unknown";
|
|
102
|
+
const line = (item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
|
|
103
|
+
1;
|
|
104
|
+
const relFile = path.relative(targetPath, item.file ?? "").replace(/\\/g, "/");
|
|
105
|
+
if (shouldIgnoreFile(relFile, isTsProject))
|
|
106
|
+
continue;
|
|
107
|
+
astIssues.push({
|
|
108
|
+
rule,
|
|
109
|
+
file: relFile,
|
|
110
|
+
line,
|
|
111
|
+
message: item.message ?? rule,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return astIssues;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Scan for remaining Biome lint issues (couldn't be auto-fixed)
|
|
118
|
+
*/
|
|
119
|
+
export function scanBiomeIssues(biome, targetPath) {
|
|
120
|
+
if (!biome.isAvailable())
|
|
121
|
+
return [];
|
|
122
|
+
const checkResult = childProcess.spawnSync("npx", ["@biomejs/biome", "check", "--reporter=json", "--max-diagnostics=50", targetPath], { encoding: "utf-8", timeout: 20000, shell: true });
|
|
123
|
+
const remainingBiome = [];
|
|
124
|
+
try {
|
|
125
|
+
const data = JSON.parse(checkResult.stdout ?? "{}");
|
|
126
|
+
for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
|
|
127
|
+
if (!diag.category?.startsWith("lint/"))
|
|
128
|
+
continue;
|
|
129
|
+
const filePath = diag.location?.path?.file ?? "";
|
|
130
|
+
const line = diag.location?.span?.start?.line ?? 0;
|
|
131
|
+
const rule = diag.category ?? "lint";
|
|
132
|
+
remainingBiome.push({
|
|
133
|
+
file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
|
|
134
|
+
line: line + 1,
|
|
135
|
+
rule,
|
|
136
|
+
message: diag.message ?? rule,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
dbg(`biome lint parse failed: ${e}`);
|
|
142
|
+
}
|
|
143
|
+
return remainingBiome;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Scan for AI slop indicators (high complexity patterns)
|
|
147
|
+
*/
|
|
148
|
+
export function scanSlop(complexity, targetPath, isTsProject) {
|
|
149
|
+
const slopFiles = [];
|
|
150
|
+
const scanDir = (dir) => {
|
|
151
|
+
if (!nodeFs.existsSync(dir))
|
|
152
|
+
return;
|
|
153
|
+
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
|
|
154
|
+
const fullPath = path.join(dir, entry.name);
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
if (["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(entry.name))
|
|
157
|
+
continue;
|
|
158
|
+
scanDir(fullPath);
|
|
159
|
+
}
|
|
160
|
+
else if (complexity.isSupportedFile(fullPath)) {
|
|
161
|
+
const metrics = complexity.analyzeFile(fullPath);
|
|
162
|
+
if (metrics) {
|
|
163
|
+
const warnings = complexity
|
|
164
|
+
.checkThresholds(metrics)
|
|
165
|
+
.filter((w) => w.includes("AI-style") ||
|
|
166
|
+
w.includes("try/catch") ||
|
|
167
|
+
w.includes("single-use") ||
|
|
168
|
+
w.includes("Excessive comments"));
|
|
169
|
+
const relFile = path
|
|
170
|
+
.relative(targetPath, fullPath)
|
|
171
|
+
.replace(/\\/g, "/");
|
|
172
|
+
if (shouldIgnoreFile(relFile, isTsProject))
|
|
173
|
+
continue;
|
|
174
|
+
if (warnings.length >= 2) {
|
|
175
|
+
slopFiles.push({ file: relFile, warnings });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
scanDir(targetPath);
|
|
182
|
+
return slopFiles;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Run all scanners and return combined results
|
|
186
|
+
*/
|
|
187
|
+
export function scanAll(clients, targetPath, isTsProject, configPath) {
|
|
188
|
+
return {
|
|
189
|
+
duplicates: scanDuplicates(clients.jscpd, targetPath, isTsProject),
|
|
190
|
+
deadCode: scanDeadCode(clients.knip, targetPath, isTsProject),
|
|
191
|
+
astIssues: scanAstGrep(targetPath, isTsProject, configPath),
|
|
192
|
+
biomeIssues: scanBiomeIssues(clients.biome, targetPath),
|
|
193
|
+
slopFiles: scanSlop(clients.complexity, targetPath, isTsProject),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner functions for fix.ts
|
|
3
|
+
*
|
|
4
|
+
* Each scanner encapsulates one type of issue detection:
|
|
5
|
+
* - scanDuplicates: JSCPD duplicate code detection
|
|
6
|
+
* - scanDeadCode: Knip dead code detection
|
|
7
|
+
* - scanAstGrep: Structural linting via ast-grep
|
|
8
|
+
* - scanBiome: Remaining Biome lint issues
|
|
9
|
+
* - scanSlop: AI slop indicators (high complexity patterns)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as childProcess from "node:child_process";
|
|
13
|
+
import * as nodeFs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { BiomeClient } from "./biome-client.js";
|
|
16
|
+
import type { ComplexityClient } from "./complexity-client.js";
|
|
17
|
+
import type { JscpdClient } from "./jscpd-client.js";
|
|
18
|
+
import type { KnipClient } from "./knip-client.js";
|
|
19
|
+
import { shouldIgnoreFile } from "./scan-utils.js";
|
|
20
|
+
|
|
21
|
+
export interface DuplicateClone {
|
|
22
|
+
fileA: string;
|
|
23
|
+
fileB: string;
|
|
24
|
+
startA: number;
|
|
25
|
+
startB: number;
|
|
26
|
+
lines: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DeadCodeIssue {
|
|
30
|
+
type: string;
|
|
31
|
+
name: string;
|
|
32
|
+
file?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AstIssue {
|
|
36
|
+
rule: string;
|
|
37
|
+
file: string;
|
|
38
|
+
line: number;
|
|
39
|
+
message: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BiomeIssue {
|
|
43
|
+
file: string;
|
|
44
|
+
line: number;
|
|
45
|
+
rule: string;
|
|
46
|
+
message: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SlopFile {
|
|
50
|
+
file: string;
|
|
51
|
+
warnings: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FixScanResults {
|
|
55
|
+
duplicates: DuplicateClone[];
|
|
56
|
+
deadCode: DeadCodeIssue[];
|
|
57
|
+
astIssues: AstIssue[];
|
|
58
|
+
biomeIssues: BiomeIssue[];
|
|
59
|
+
slopFiles: SlopFile[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEBUG_LOG = path.join(
|
|
63
|
+
process.env.HOME || process.env.USERPROFILE || ".",
|
|
64
|
+
"pi-lens-debug.log",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
function dbg(msg: string) {
|
|
68
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
69
|
+
try {
|
|
70
|
+
nodeFs.appendFileSync(DEBUG_LOG, line);
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignored
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Scan for duplicate code blocks using JSCPD
|
|
78
|
+
*/
|
|
79
|
+
export function scanDuplicates(
|
|
80
|
+
jscpd: JscpdClient,
|
|
81
|
+
targetPath: string,
|
|
82
|
+
isTsProject: boolean,
|
|
83
|
+
): DuplicateClone[] {
|
|
84
|
+
if (!jscpd.isAvailable()) return [];
|
|
85
|
+
|
|
86
|
+
const jscpdResult = jscpd.scan(targetPath);
|
|
87
|
+
return jscpdResult.clones.filter((c) => {
|
|
88
|
+
if (isTsProject && (c.fileA.endsWith(".js") || c.fileB.endsWith(".js")))
|
|
89
|
+
return false;
|
|
90
|
+
return path.resolve(c.fileA) !== path.resolve(c.fileB);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scan for dead code using Knip
|
|
96
|
+
*/
|
|
97
|
+
export function scanDeadCode(
|
|
98
|
+
knip: KnipClient,
|
|
99
|
+
targetPath: string,
|
|
100
|
+
isTsProject: boolean,
|
|
101
|
+
): DeadCodeIssue[] {
|
|
102
|
+
if (!knip.isAvailable()) return [];
|
|
103
|
+
|
|
104
|
+
const knipResult = knip.analyze(targetPath);
|
|
105
|
+
return knipResult.issues.filter((i) => {
|
|
106
|
+
if (!i.file) return true;
|
|
107
|
+
return !shouldIgnoreFile(i.file, isTsProject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Scan for structural issues using ast-grep
|
|
113
|
+
*/
|
|
114
|
+
export function scanAstGrep(
|
|
115
|
+
targetPath: string,
|
|
116
|
+
isTsProject: boolean,
|
|
117
|
+
configPath: string,
|
|
118
|
+
): AstIssue[] {
|
|
119
|
+
const hasSg = nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
|
|
120
|
+
childProcess.spawnSync("npx", ["sg", "--version"], { encoding: "utf-8", timeout: 5000, shell: true }).status === 0;
|
|
121
|
+
|
|
122
|
+
if (!hasSg) return [];
|
|
123
|
+
|
|
124
|
+
const result = childProcess.spawnSync(
|
|
125
|
+
"npx",
|
|
126
|
+
[
|
|
127
|
+
"sg",
|
|
128
|
+
"scan",
|
|
129
|
+
"--config",
|
|
130
|
+
configPath,
|
|
131
|
+
"--json",
|
|
132
|
+
"--globs",
|
|
133
|
+
"!**/*.test.ts",
|
|
134
|
+
"--globs",
|
|
135
|
+
"!**/*.spec.ts",
|
|
136
|
+
"--globs",
|
|
137
|
+
"!**/test-utils.ts",
|
|
138
|
+
"--globs",
|
|
139
|
+
"!**/.pi-lens/**",
|
|
140
|
+
...(isTsProject ? ["--globs", "!**/*.js"] : []),
|
|
141
|
+
targetPath,
|
|
142
|
+
],
|
|
143
|
+
{
|
|
144
|
+
encoding: "utf-8",
|
|
145
|
+
timeout: 30000,
|
|
146
|
+
shell: true,
|
|
147
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const raw = result.stdout?.trim() ?? "";
|
|
152
|
+
const items: any[] = raw.startsWith("[")
|
|
153
|
+
? (() => {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
})()
|
|
160
|
+
: raw.split("\n").flatMap((l: string) => {
|
|
161
|
+
try {
|
|
162
|
+
return [JSON.parse(l)];
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const astIssues: AstIssue[] = [];
|
|
169
|
+
for (const item of items) {
|
|
170
|
+
const rule = item.ruleId || item.rule?.title || item.name || "unknown";
|
|
171
|
+
const line =
|
|
172
|
+
(item.labels?.[0]?.range?.start?.line ?? item.range?.start?.line ?? 0) +
|
|
173
|
+
1;
|
|
174
|
+
const relFile = path.relative(targetPath, item.file ?? "").replace(/\\/g, "/");
|
|
175
|
+
|
|
176
|
+
if (shouldIgnoreFile(relFile, isTsProject)) continue;
|
|
177
|
+
|
|
178
|
+
astIssues.push({
|
|
179
|
+
rule,
|
|
180
|
+
file: relFile,
|
|
181
|
+
line,
|
|
182
|
+
message: item.message ?? rule,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return astIssues;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scan for remaining Biome lint issues (couldn't be auto-fixed)
|
|
191
|
+
*/
|
|
192
|
+
export function scanBiomeIssues(
|
|
193
|
+
biome: BiomeClient,
|
|
194
|
+
targetPath: string,
|
|
195
|
+
): BiomeIssue[] {
|
|
196
|
+
if (!biome.isAvailable()) return [];
|
|
197
|
+
|
|
198
|
+
const checkResult = childProcess.spawnSync(
|
|
199
|
+
"npx",
|
|
200
|
+
["@biomejs/biome", "check", "--reporter=json", "--max-diagnostics=50", targetPath],
|
|
201
|
+
{ encoding: "utf-8", timeout: 20000, shell: true },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const remainingBiome: BiomeIssue[] = [];
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(checkResult.stdout ?? "{}");
|
|
207
|
+
for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
|
|
208
|
+
if (!diag.category?.startsWith("lint/")) continue;
|
|
209
|
+
const filePath = diag.location?.path?.file ?? "";
|
|
210
|
+
const line = diag.location?.span?.start?.line ?? 0;
|
|
211
|
+
const rule = diag.category ?? "lint";
|
|
212
|
+
remainingBiome.push({
|
|
213
|
+
file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
|
|
214
|
+
line: line + 1,
|
|
215
|
+
rule,
|
|
216
|
+
message: diag.message ?? rule,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
dbg(`biome lint parse failed: ${e}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return remainingBiome;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Scan for AI slop indicators (high complexity patterns)
|
|
228
|
+
*/
|
|
229
|
+
export function scanSlop(
|
|
230
|
+
complexity: ComplexityClient,
|
|
231
|
+
targetPath: string,
|
|
232
|
+
isTsProject: boolean,
|
|
233
|
+
): SlopFile[] {
|
|
234
|
+
const slopFiles: SlopFile[] = [];
|
|
235
|
+
|
|
236
|
+
const scanDir = (dir: string) => {
|
|
237
|
+
if (!nodeFs.existsSync(dir)) return;
|
|
238
|
+
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
|
|
239
|
+
const fullPath = path.join(dir, entry.name);
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
if (
|
|
242
|
+
["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(
|
|
243
|
+
entry.name,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
continue;
|
|
247
|
+
scanDir(fullPath);
|
|
248
|
+
} else if (complexity.isSupportedFile(fullPath)) {
|
|
249
|
+
const metrics = complexity.analyzeFile(fullPath);
|
|
250
|
+
if (metrics) {
|
|
251
|
+
const warnings = complexity
|
|
252
|
+
.checkThresholds(metrics)
|
|
253
|
+
.filter(
|
|
254
|
+
(w) =>
|
|
255
|
+
w.includes("AI-style") ||
|
|
256
|
+
w.includes("try/catch") ||
|
|
257
|
+
w.includes("single-use") ||
|
|
258
|
+
w.includes("Excessive comments"),
|
|
259
|
+
);
|
|
260
|
+
const relFile = path
|
|
261
|
+
.relative(targetPath, fullPath)
|
|
262
|
+
.replace(/\\/g, "/");
|
|
263
|
+
if (shouldIgnoreFile(relFile, isTsProject)) continue;
|
|
264
|
+
if (warnings.length >= 2) {
|
|
265
|
+
slopFiles.push({ file: relFile, warnings });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
scanDir(targetPath);
|
|
273
|
+
return slopFiles;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Run all scanners and return combined results
|
|
278
|
+
*/
|
|
279
|
+
export function scanAll(
|
|
280
|
+
clients: {
|
|
281
|
+
jscpd: JscpdClient;
|
|
282
|
+
knip: KnipClient;
|
|
283
|
+
biome: BiomeClient;
|
|
284
|
+
complexity: ComplexityClient;
|
|
285
|
+
},
|
|
286
|
+
targetPath: string,
|
|
287
|
+
isTsProject: boolean,
|
|
288
|
+
configPath: string,
|
|
289
|
+
): FixScanResults {
|
|
290
|
+
return {
|
|
291
|
+
duplicates: scanDuplicates(clients.jscpd, targetPath, isTsProject),
|
|
292
|
+
deadCode: scanDeadCode(clients.knip, targetPath, isTsProject),
|
|
293
|
+
astIssues: scanAstGrep(targetPath, isTsProject, configPath),
|
|
294
|
+
biomeIssues: scanBiomeIssues(clients.biome, targetPath),
|
|
295
|
+
slopFiles: scanSlop(clients.complexity, targetPath, isTsProject),
|
|
296
|
+
};
|
|
297
|
+
}
|