pi-lens 2.0.25 ā 2.0.27
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/clients/type-safety-client.js +151 -0
- package/clients/type-safety-client.ts +210 -0
- package/index.ts +51 -0
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +15 -0
- package/rules/ast-grep-rules/rules/no-param-reassign.yml +16 -0
- package/rules/ast-grep-rules/rules/no-process-env.yml +18 -0
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +12 -0
- package/rules/ast-grep-rules/rules/switch-without-default.yml +12 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Safety Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Detects type safety violations that can cause runtime bugs.
|
|
5
|
+
* Uses the TypeScript compiler API for type-aware analysis.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* - Switch Exhaustiveness: Missing cases in union type switches
|
|
9
|
+
* - Null Safety: Potential null/undefined dereferences (future)
|
|
10
|
+
* - Exhaustive Type Guards: Incomplete instanceof checks (future)
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as ts from "typescript";
|
|
15
|
+
// --- Client ---
|
|
16
|
+
export class TypeSafetyClient {
|
|
17
|
+
constructor(verbose = false) {
|
|
18
|
+
this.log = verbose
|
|
19
|
+
? (msg) => console.error(`[type-safety] ${msg}`)
|
|
20
|
+
: () => { };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if file is supported (TS/JS)
|
|
24
|
+
*/
|
|
25
|
+
isSupportedFile(filePath) {
|
|
26
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
27
|
+
return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Analyze type safety issues for a file
|
|
31
|
+
*/
|
|
32
|
+
analyzeFile(filePath) {
|
|
33
|
+
const absolutePath = path.resolve(filePath);
|
|
34
|
+
if (!fs.existsSync(absolutePath))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
38
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
39
|
+
const issues = [];
|
|
40
|
+
// Check switch exhaustiveness
|
|
41
|
+
this.checkSwitchExhaustiveness(sourceFile, issues);
|
|
42
|
+
return { filePath: absolutePath, issues };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.log(`Error analyzing ${filePath}: ${error}`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check for switch statements that don't exhaust all union cases
|
|
51
|
+
*/
|
|
52
|
+
checkSwitchExhaustiveness(sourceFile, issues) {
|
|
53
|
+
const checker = this.getTypeChecker(sourceFile);
|
|
54
|
+
if (!checker)
|
|
55
|
+
return;
|
|
56
|
+
const visit = (node) => {
|
|
57
|
+
if (ts.isSwitchStatement(node)) {
|
|
58
|
+
const exprType = checker.getTypeAtLocation(node.expression);
|
|
59
|
+
// Only check union types (literal unions and object unions)
|
|
60
|
+
if (exprType.isUnion()) {
|
|
61
|
+
const unionTypes = exprType.types;
|
|
62
|
+
// Get all literal values from the union
|
|
63
|
+
const literalValues = unionTypes
|
|
64
|
+
.filter((t) => t.isLiteral() || t.flags & ts.TypeFlags.BooleanLiteral)
|
|
65
|
+
.map((t) => {
|
|
66
|
+
if (t.isLiteral()) {
|
|
67
|
+
return String(t.value);
|
|
68
|
+
}
|
|
69
|
+
// Boolean literals
|
|
70
|
+
if (t.flags & ts.TypeFlags.BooleanLiteral) {
|
|
71
|
+
return checker.typeToString(t);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
})
|
|
75
|
+
.filter((v) => v !== null);
|
|
76
|
+
// Skip if no literal union (e.g., string | number)
|
|
77
|
+
if (literalValues.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
// Get all case clauses
|
|
80
|
+
const coveredCases = new Set();
|
|
81
|
+
for (const clause of node.caseBlock.clauses) {
|
|
82
|
+
if (ts.isCaseClause(clause)) {
|
|
83
|
+
const caseType = checker.getTypeAtLocation(clause.expression);
|
|
84
|
+
if (caseType.isLiteral()) {
|
|
85
|
+
coveredCases.add(String(caseType.value));
|
|
86
|
+
}
|
|
87
|
+
else if (caseType.flags & ts.TypeFlags.BooleanLiteral) {
|
|
88
|
+
coveredCases.add(checker.typeToString(caseType));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Check for hasDefault
|
|
93
|
+
const hasDefault = node.caseBlock.clauses.some((c) => ts.isDefaultClause(c));
|
|
94
|
+
// Find missing cases
|
|
95
|
+
const missingCases = literalValues.filter((v) => !coveredCases.has(v));
|
|
96
|
+
if (missingCases.length > 0 && !hasDefault) {
|
|
97
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
98
|
+
const exprText = node.expression.getText(sourceFile);
|
|
99
|
+
const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
|
|
100
|
+
issues.push({
|
|
101
|
+
filePath: sourceFile.fileName,
|
|
102
|
+
rule: "switch-exhaustiveness",
|
|
103
|
+
line,
|
|
104
|
+
message: `Switch on '${exprText}' is not exhaustive. Missing cases: ${typeStr}`,
|
|
105
|
+
severity: "error",
|
|
106
|
+
context: `Type has ${literalValues.length} cases, ${coveredCases.size} covered, ${missingCases.length} missing`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
ts.forEachChild(node, visit);
|
|
112
|
+
};
|
|
113
|
+
ts.forEachChild(sourceFile, visit);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get type checker for the source file
|
|
117
|
+
*/
|
|
118
|
+
getTypeChecker(sourceFile) {
|
|
119
|
+
try {
|
|
120
|
+
const compilerOptions = {
|
|
121
|
+
target: ts.ScriptTarget.Latest,
|
|
122
|
+
module: ts.ModuleKind.ESNext,
|
|
123
|
+
strict: true,
|
|
124
|
+
noEmit: true,
|
|
125
|
+
skipLibCheck: true,
|
|
126
|
+
};
|
|
127
|
+
// Create a host that uses our pre-parsed source file
|
|
128
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
129
|
+
const originalGetSourceFile = host.getSourceFile;
|
|
130
|
+
host.getSourceFile = (fileName, languageVersion) => {
|
|
131
|
+
if (fileName === sourceFile.fileName)
|
|
132
|
+
return sourceFile;
|
|
133
|
+
return originalGetSourceFile(fileName, languageVersion);
|
|
134
|
+
};
|
|
135
|
+
const program = ts.createProgram([sourceFile.fileName], compilerOptions, host);
|
|
136
|
+
return program.getTypeChecker();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
this.log("Could not create type checker, skipping exhaustiveness check");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// --- Singleton ---
|
|
145
|
+
let instance = null;
|
|
146
|
+
export function getTypeSafetyClient(verbose = false) {
|
|
147
|
+
if (!instance) {
|
|
148
|
+
instance = new TypeSafetyClient(verbose);
|
|
149
|
+
}
|
|
150
|
+
return instance;
|
|
151
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Safety Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Detects type safety violations that can cause runtime bugs.
|
|
5
|
+
* Uses the TypeScript compiler API for type-aware analysis.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* - Switch Exhaustiveness: Missing cases in union type switches
|
|
9
|
+
* - Null Safety: Potential null/undefined dereferences (future)
|
|
10
|
+
* - Exhaustive Type Guards: Incomplete instanceof checks (future)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as ts from "typescript";
|
|
16
|
+
|
|
17
|
+
// --- Types ---
|
|
18
|
+
|
|
19
|
+
export interface TypeSafetyIssue {
|
|
20
|
+
filePath: string;
|
|
21
|
+
rule: "switch-exhaustiveness" | "null-safety" | "exhaustive-type-guard";
|
|
22
|
+
line: number;
|
|
23
|
+
message: string;
|
|
24
|
+
severity: "error" | "warning";
|
|
25
|
+
context: string; // Code snippet or type info
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TypeSafetyReport {
|
|
29
|
+
filePath: string;
|
|
30
|
+
issues: TypeSafetyIssue[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Client ---
|
|
34
|
+
|
|
35
|
+
export class TypeSafetyClient {
|
|
36
|
+
private log: (msg: string) => void;
|
|
37
|
+
|
|
38
|
+
constructor(verbose = false) {
|
|
39
|
+
this.log = verbose
|
|
40
|
+
? (msg: string) => console.error(`[type-safety] ${msg}`)
|
|
41
|
+
: () => {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if file is supported (TS/JS)
|
|
46
|
+
*/
|
|
47
|
+
isSupportedFile(filePath: string): boolean {
|
|
48
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
49
|
+
return [".ts", ".tsx", ".js", ".jsx"].includes(ext);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Analyze type safety issues for a file
|
|
54
|
+
*/
|
|
55
|
+
analyzeFile(filePath: string): TypeSafetyReport | null {
|
|
56
|
+
const absolutePath = path.resolve(filePath);
|
|
57
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
61
|
+
const sourceFile = ts.createSourceFile(
|
|
62
|
+
filePath,
|
|
63
|
+
content,
|
|
64
|
+
ts.ScriptTarget.Latest,
|
|
65
|
+
true,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const issues: TypeSafetyIssue[] = [];
|
|
69
|
+
|
|
70
|
+
// Check switch exhaustiveness
|
|
71
|
+
this.checkSwitchExhaustiveness(sourceFile, issues);
|
|
72
|
+
|
|
73
|
+
return { filePath: absolutePath, issues };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.log(`Error analyzing ${filePath}: ${error}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check for switch statements that don't exhaust all union cases
|
|
82
|
+
*/
|
|
83
|
+
private checkSwitchExhaustiveness(
|
|
84
|
+
sourceFile: ts.SourceFile,
|
|
85
|
+
issues: TypeSafetyIssue[],
|
|
86
|
+
): void {
|
|
87
|
+
const checker = this.getTypeChecker(sourceFile);
|
|
88
|
+
if (!checker) return;
|
|
89
|
+
|
|
90
|
+
const visit = (node: ts.Node) => {
|
|
91
|
+
if (ts.isSwitchStatement(node)) {
|
|
92
|
+
const exprType = checker.getTypeAtLocation(node.expression);
|
|
93
|
+
|
|
94
|
+
// Only check union types (literal unions and object unions)
|
|
95
|
+
if (exprType.isUnion()) {
|
|
96
|
+
const unionTypes = exprType.types;
|
|
97
|
+
|
|
98
|
+
// Get all literal values from the union
|
|
99
|
+
const literalValues = unionTypes
|
|
100
|
+
.filter((t) => t.isLiteral() || t.flags & ts.TypeFlags.BooleanLiteral)
|
|
101
|
+
.map((t) => {
|
|
102
|
+
if (t.isLiteral()) {
|
|
103
|
+
return String(t.value);
|
|
104
|
+
}
|
|
105
|
+
// Boolean literals
|
|
106
|
+
if (t.flags & ts.TypeFlags.BooleanLiteral) {
|
|
107
|
+
return checker.typeToString(t);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
})
|
|
111
|
+
.filter((v): v is string => v !== null);
|
|
112
|
+
|
|
113
|
+
// Skip if no literal union (e.g., string | number)
|
|
114
|
+
if (literalValues.length === 0) return;
|
|
115
|
+
|
|
116
|
+
// Get all case clauses
|
|
117
|
+
const coveredCases = new Set<string>();
|
|
118
|
+
for (const clause of node.caseBlock.clauses) {
|
|
119
|
+
if (ts.isCaseClause(clause)) {
|
|
120
|
+
const caseType = checker.getTypeAtLocation(clause.expression);
|
|
121
|
+
if (caseType.isLiteral()) {
|
|
122
|
+
coveredCases.add(String(caseType.value));
|
|
123
|
+
} else if (caseType.flags & ts.TypeFlags.BooleanLiteral) {
|
|
124
|
+
coveredCases.add(checker.typeToString(caseType));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for hasDefault
|
|
130
|
+
const hasDefault = node.caseBlock.clauses.some((c) =>
|
|
131
|
+
ts.isDefaultClause(c),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Find missing cases
|
|
135
|
+
const missingCases = literalValues.filter(
|
|
136
|
+
(v) => !coveredCases.has(v),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (missingCases.length > 0 && !hasDefault) {
|
|
140
|
+
const line = sourceFile.getLineAndCharacterOfPosition(
|
|
141
|
+
node.getStart(),
|
|
142
|
+
).line + 1;
|
|
143
|
+
|
|
144
|
+
const exprText = node.expression.getText(sourceFile);
|
|
145
|
+
const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
|
|
146
|
+
|
|
147
|
+
issues.push({
|
|
148
|
+
filePath: sourceFile.fileName,
|
|
149
|
+
rule: "switch-exhaustiveness",
|
|
150
|
+
line,
|
|
151
|
+
message: `Switch on '${exprText}' is not exhaustive. Missing cases: ${typeStr}`,
|
|
152
|
+
severity: "error",
|
|
153
|
+
context: `Type has ${literalValues.length} cases, ${coveredCases.size} covered, ${missingCases.length} missing`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ts.forEachChild(node, visit);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ts.forEachChild(sourceFile, visit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get type checker for the source file
|
|
167
|
+
*/
|
|
168
|
+
private getTypeChecker(sourceFile: ts.SourceFile): ts.TypeChecker | null {
|
|
169
|
+
try {
|
|
170
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
171
|
+
target: ts.ScriptTarget.Latest,
|
|
172
|
+
module: ts.ModuleKind.ESNext,
|
|
173
|
+
strict: true,
|
|
174
|
+
noEmit: true,
|
|
175
|
+
skipLibCheck: true,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Create a host that uses our pre-parsed source file
|
|
179
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
180
|
+
const originalGetSourceFile = host.getSourceFile;
|
|
181
|
+
host.getSourceFile = (fileName, languageVersion) => {
|
|
182
|
+
if (fileName === sourceFile.fileName) return sourceFile;
|
|
183
|
+
return originalGetSourceFile(fileName, languageVersion);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const program = ts.createProgram(
|
|
187
|
+
[sourceFile.fileName],
|
|
188
|
+
compilerOptions,
|
|
189
|
+
host,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return program.getTypeChecker();
|
|
193
|
+
} catch {
|
|
194
|
+
this.log("Could not create type checker, skipping exhaustiveness check");
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Singleton ---
|
|
202
|
+
|
|
203
|
+
let instance: TypeSafetyClient | null = null;
|
|
204
|
+
|
|
205
|
+
export function getTypeSafetyClient(verbose = false): TypeSafetyClient {
|
|
206
|
+
if (!instance) {
|
|
207
|
+
instance = new TypeSafetyClient(verbose);
|
|
208
|
+
}
|
|
209
|
+
return instance;
|
|
210
|
+
}
|
package/index.ts
CHANGED
|
@@ -31,6 +31,10 @@ import { Type } from "@sinclair/typebox";
|
|
|
31
31
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
32
32
|
import { BiomeClient } from "./clients/biome-client.js";
|
|
33
33
|
import { ComplexityClient } from "./clients/complexity-client.js";
|
|
34
|
+
import {
|
|
35
|
+
TypeSafetyClient,
|
|
36
|
+
getTypeSafetyClient,
|
|
37
|
+
} from "./clients/type-safety-client.js";
|
|
34
38
|
import { DependencyChecker } from "./clients/dependency-checker.js";
|
|
35
39
|
import { GoClient } from "./clients/go-client.js";
|
|
36
40
|
import { JscpdClient } from "./clients/jscpd-client.js";
|
|
@@ -78,6 +82,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
78
82
|
const testRunnerClient = new TestRunnerClient();
|
|
79
83
|
const metricsClient = new MetricsClient();
|
|
80
84
|
const complexityClient = new ComplexityClient();
|
|
85
|
+
const typeSafetyClient = new TypeSafetyClient();
|
|
81
86
|
const goClient = new GoClient();
|
|
82
87
|
const rustClient = new RustClient();
|
|
83
88
|
|
|
@@ -662,6 +667,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
662
667
|
type: "skip",
|
|
663
668
|
note: "Renaming requires understanding all variable scopes.",
|
|
664
669
|
},
|
|
670
|
+
"no-process-env": {
|
|
671
|
+
type: "skip",
|
|
672
|
+
note: "Using process.env directly makes code untestable. Use DI or a config module.",
|
|
673
|
+
},
|
|
674
|
+
"no-param-reassign": {
|
|
675
|
+
type: "agent",
|
|
676
|
+
note: "Create a new variable instead of reassigning the parameter.",
|
|
677
|
+
},
|
|
678
|
+
"no-single-char-var": {
|
|
679
|
+
type: "skip",
|
|
680
|
+
note: "Renaming requires understanding the variable's purpose.",
|
|
681
|
+
},
|
|
682
|
+
"switch-without-default": {
|
|
683
|
+
type: "agent",
|
|
684
|
+
note: "Add a default case to handle unexpected values.",
|
|
685
|
+
},
|
|
686
|
+
"no-architecture-violation": {
|
|
687
|
+
type: "skip",
|
|
688
|
+
note: "Layer boundary violations require architectural decisions.",
|
|
689
|
+
},
|
|
690
|
+
"switch-exhaustiveness": {
|
|
691
|
+
type: "agent",
|
|
692
|
+
note: "Add the missing case(s) or a default clause to handle all union values.",
|
|
693
|
+
},
|
|
665
694
|
};
|
|
666
695
|
|
|
667
696
|
// Derived from RULE_ACTIONS ā used to suppress architectural rules from inline hard stops.
|
|
@@ -2005,6 +2034,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
2005
2034
|
}
|
|
2006
2035
|
}
|
|
2007
2036
|
|
|
2037
|
+
// Type safety checks (switch exhaustiveness, etc.)
|
|
2038
|
+
if (typeSafetyClient.isSupportedFile(filePath)) {
|
|
2039
|
+
const report = typeSafetyClient.analyzeFile(filePath);
|
|
2040
|
+
if (report && report.issues.length > 0) {
|
|
2041
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
2042
|
+
const warnings = report.issues.filter((i) => i.severity === "warning");
|
|
2043
|
+
if (errors.length > 0) {
|
|
2044
|
+
lspOutput += `\n\nš“ STOP ā ${errors.length} type safety violation(s). Fix before continuing:\n`;
|
|
2045
|
+
for (const issue of errors) {
|
|
2046
|
+
lspOutput += ` L${issue.line}: ${issue.message}\n`;
|
|
2047
|
+
lspOutput += ` ā Add missing cases or add a default clause\n`;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (warnings.length > 0) {
|
|
2051
|
+
lspOutput += `\n\nš” ${warnings.length} type safety warning(s):\n`;
|
|
2052
|
+
for (const issue of warnings) {
|
|
2053
|
+
lspOutput += ` L${issue.line}: ${issue.message}\n`;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2008
2059
|
// ast-grep structural analysis ā delta mode (only show new violations)
|
|
2009
2060
|
if (!pi.getFlag("no-ast-grep") && astGrepClient.isAvailable()) {
|
|
2010
2061
|
const after = astGrepClient.scanFile(filePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.27",
|
|
4
4
|
"description": "Real-time code quality feedback for pi ā TypeScript LSP, Biome, ast-grep, Ruff, complexity metrics, duplicate detection. Includes automated fix loop (/lens-booboo-fix) and interactive architectural refactoring (/lens-booboo-refactor) with browser-based interviews.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
id: no-architecture-violation
|
|
2
|
+
language: TypeScript
|
|
3
|
+
message: "Forbidden import ā layer boundary violation: importing from database/model layer"
|
|
4
|
+
severity: error
|
|
5
|
+
note: |
|
|
6
|
+
This import crosses an architectural layer boundary.
|
|
7
|
+
Use dependency injection or a shared types/interfaces module instead.
|
|
8
|
+
|
|
9
|
+
BAD: import { User } from '../models/user';
|
|
10
|
+
GOOD: import { User } from '../types/user';
|
|
11
|
+
rule:
|
|
12
|
+
pattern: import { $$$ } from "$PATH"
|
|
13
|
+
constraints:
|
|
14
|
+
PATH:
|
|
15
|
+
regex: ".*models.*|.*database.*|.*prisma.*"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: no-param-reassign
|
|
2
|
+
language: TypeScript
|
|
3
|
+
message: "Do not reassign function parameters ā create a new variable instead"
|
|
4
|
+
severity: warning
|
|
5
|
+
note: |
|
|
6
|
+
Reassigning parameters makes code harder to debug and reason about.
|
|
7
|
+
It can cause unexpected side effects when the caller's reference changes.
|
|
8
|
+
Create a new variable with a descriptive name instead.
|
|
9
|
+
|
|
10
|
+
BAD: function update(user) { user = { ...user, active: true }; }
|
|
11
|
+
GOOD: function update(user) { const updated = { ...user, active: true }; }
|
|
12
|
+
rule:
|
|
13
|
+
pattern: |
|
|
14
|
+
function $FUNC($PARAM, $$$) {
|
|
15
|
+
$PARAM = $VALUE
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id: no-process-env
|
|
2
|
+
language: TypeScript
|
|
3
|
+
message: "Do not access process.env directly ā use dependency injection or a config module"
|
|
4
|
+
severity: error
|
|
5
|
+
note: |
|
|
6
|
+
Direct process.env access makes code untestable and tightly coupled to the
|
|
7
|
+
runtime environment. Use a configuration service or DI container instead.
|
|
8
|
+
|
|
9
|
+
BAD: const dbUrl = process.env.DATABASE_URL;
|
|
10
|
+
GOOD: constructor(private config: ConfigService) {}
|
|
11
|
+
const dbUrl = this.config.get('DATABASE_URL');
|
|
12
|
+
|
|
13
|
+
Grade 2.0 ā critical architectural violation for testability.
|
|
14
|
+
rule:
|
|
15
|
+
any:
|
|
16
|
+
- pattern: process.env.$KEY
|
|
17
|
+
- pattern: process.env[$KEY]
|
|
18
|
+
- pattern: Deno.env.get($KEY)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
id: no-single-char-var
|
|
2
|
+
language: TypeScript
|
|
3
|
+
message: "Avoid single-character variable names ā use descriptive names"
|
|
4
|
+
severity: info
|
|
5
|
+
note: |
|
|
6
|
+
Single-character variable names reduce readability.
|
|
7
|
+
Exception: loop counters (i, j, k) in short loops are acceptable.
|
|
8
|
+
rule:
|
|
9
|
+
pattern: const $VAR = $VALUE
|
|
10
|
+
constraints:
|
|
11
|
+
VAR:
|
|
12
|
+
regex: "^[a-z]$"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
id: switch-without-default
|
|
2
|
+
language: TypeScript
|
|
3
|
+
message: "Switch statement is missing a default case"
|
|
4
|
+
severity: warning
|
|
5
|
+
note: |
|
|
6
|
+
Every switch should have a default case. Without it, new enum values won't be caught at runtime.
|
|
7
|
+
rule:
|
|
8
|
+
all:
|
|
9
|
+
- pattern: switch ($EXPR) { $$$CASES }
|
|
10
|
+
- not:
|
|
11
|
+
has:
|
|
12
|
+
pattern: "default: $$$"
|