pi-lens 2.0.26 → 2.0.28

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [2.0.27] - 2026-03-26
6
+
7
+ ### Added
8
+ - **`switch-exhaustiveness` check**: New type safety rule detects missing cases in union type switches. Uses TypeScript compiler API for type-aware analysis. Reports as inline blocker: `šŸ”“ STOP — Switch on 'X' is not exhaustive. Missing cases: 'Y'`.
9
+ - **`clients/type-safety-client.ts`**: New client for type safety checks. Extensible for future checks (null safety, exhaustive type guards).
10
+
11
+ ### Changed
12
+ - **Type safety violations added to inline feedback**: Missing switch cases now block the agent mid-task, same as TypeScript errors.
13
+ - **Type safety violations in `/lens-booboo-fix`**: Marked as agent-fixable (add missing case or default clause).
14
+
15
+ ## [2.0.26] - 2026-03-26
16
+
17
+ ### Added
18
+ - **5 new ast-grep rules** for AI slop detection:
19
+ - `no-process-env`: Block direct `process.env` access (use DI or config module) — error level
20
+ - `no-param-reassign`: Detect function parameter reassignment — warning level
21
+ - `no-single-char-var`: Flag single-character variable names — info level
22
+ - `switch-without-default`: Ensure switch statements have default case — warning level
23
+ - `no-architecture-violation`: Block cross-layer imports (models/db) — error level
24
+
25
+ ### Changed
26
+ - **RULE_ACTIONS updated** for new rules:
27
+ - `agent` type (inline + booboo-fix): `no-param-reassign`, `switch-without-default`, `switch-exhaustiveness`
28
+ - `skip` type (booboo-refactor only): `no-process-env`, `no-single-char-var`, `no-architecture-violation`
29
+
5
30
  ## [2.0.24] - 2026-03-26
6
31
 
7
32
  ### Changed
package/README.md CHANGED
@@ -202,11 +202,17 @@ Each rule includes a `message` and `note` that are shown in diagnostics, so the
202
202
  `no-debugger`, `no-throw-string`, `no-return-await`, `no-await-in-loop`, `no-await-in-promise-all`, `require-await`, `empty-catch`, `strict-equality`, `strict-inequality`
203
203
 
204
204
  **Patterns**
205
- `no-console-log`, `no-alert`, `no-delete-operator`, `no-shadow`, `no-star-imports`, `switch-needs-default`
205
+ `no-console-log`, `no-alert`, `no-delete-operator`, `no-shadow`, `no-star-imports`, `switch-needs-default`, `switch-without-default`
206
+
207
+ **Type Safety** (type-aware checks via `type-safety-client.ts`)
208
+ `switch-exhaustiveness` — detects missing cases in union type switches (inline blocker)
206
209
 
207
210
  **Design Smells**
208
211
  `long-method`, `long-parameter-list`, `large-class`
209
212
 
213
+ **AI Slop Detection**
214
+ `no-param-reassign`, `no-single-char-var`, `no-process-env`, `no-architecture-violation`
215
+
210
216
  ---
211
217
 
212
218
  ## External dependencies summary
@@ -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
 
@@ -682,6 +687,10 @@ export default function (pi: ExtensionAPI) {
682
687
  type: "skip",
683
688
  note: "Layer boundary violations require architectural decisions.",
684
689
  },
690
+ "switch-exhaustiveness": {
691
+ type: "agent",
692
+ note: "Add the missing case(s) or a default clause to handle all union values.",
693
+ },
685
694
  };
686
695
 
687
696
  // Derived from RULE_ACTIONS — used to suppress architectural rules from inline hard stops.
@@ -2025,6 +2034,28 @@ export default function (pi: ExtensionAPI) {
2025
2034
  }
2026
2035
  }
2027
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
+
2028
2059
  // ast-grep structural analysis — delta mode (only show new violations)
2029
2060
  if (!pi.getFlag("no-ast-grep") && astGrepClient.isAvailable()) {
2030
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.26",
3
+ "version": "2.0.28",
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",