pi-lens 2.0.28 → 2.0.29

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,18 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [2.0.29] - 2026-03-26
6
+
7
+ ### Added
8
+ - **`clients/ts-service.ts`**: Shared TypeScript service that creates one `ts.Program` per session. Both `complexity-client` and `type-safety-client` now share the same program instead of creating a new one per file. Significant performance improvement on large codebases.
9
+
10
+ ### Removed
11
+ - **3 redundant ast-grep rules** that overlap with Biome: `no-var`, `prefer-template`, `no-useless-concat`. Biome handles these natively with auto-fix. ast-grep no longer duplicates this coverage.
12
+ - **`prefer-const` from RULE_ACTIONS** — no longer needed (Biome handles directly).
13
+
14
+ ### Changed
15
+ - **Consolidated rule overlap**: Biome is now the single source of truth for style/format rules. ast-grep focuses on structural patterns Biome doesn't cover (security, design smells, AI slop).
16
+
5
17
  ## [2.0.27] - 2026-03-26
6
18
 
7
19
  ### Added
package/README.md CHANGED
@@ -195,8 +195,8 @@ Each rule includes a `message` and `note` that are shown in diagnostics, so the
195
195
  **TypeScript**
196
196
  `no-any-type`, `no-as-any`, `no-non-null-assertion`
197
197
 
198
- **Style**
199
- `no-var`, `prefer-const`, `prefer-template`, `no-useless-concat`, `prefer-nullish-coalescing`, `prefer-optional-chain`, `nested-ternary`, `no-lonely-if`
198
+ **Style** (Biome handles `no-var`, `prefer-const`, `prefer-template`, `no-useless-concat` natively)
199
+ `prefer-nullish-coalescing`, `prefer-optional-chain`, `nested-ternary`, `no-lonely-if`
200
200
 
201
201
  **Correctness**
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`
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared TypeScript Service for pi-lens
3
+ *
4
+ * Creates a single ts.Program per session that is shared across all clients
5
+ * (complexity-client, type-safety-client). Avoids creating a new program per file.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as ts from "typescript";
9
+ export class TypeScriptService {
10
+ constructor(verbose = false) {
11
+ this.program = null;
12
+ this.checker = null;
13
+ this.files = new Map();
14
+ this.log = verbose
15
+ ? (msg) => console.error(`[ts-service] ${msg}`)
16
+ : () => { };
17
+ }
18
+ /**
19
+ * Update a file's content in the service
20
+ */
21
+ updateFile(filePath, content) {
22
+ this.files.set(filePath, content);
23
+ this.invalidate();
24
+ }
25
+ /**
26
+ * Invalidate the program (rebuild on next access)
27
+ */
28
+ invalidate() {
29
+ this.program = null;
30
+ this.checker = null;
31
+ }
32
+ /**
33
+ * Get the shared type checker (rebuilds program if needed)
34
+ */
35
+ getChecker() {
36
+ if (this.checker)
37
+ return this.checker;
38
+ if (this.files.size === 0)
39
+ return null;
40
+ try {
41
+ const compilerOptions = {
42
+ target: ts.ScriptTarget.Latest,
43
+ module: ts.ModuleKind.ESNext,
44
+ strict: true,
45
+ noEmit: true,
46
+ skipLibCheck: true,
47
+ lib: ["es2020"],
48
+ };
49
+ const host = ts.createCompilerHost(compilerOptions);
50
+ // Override getSourceFile to return our cached files
51
+ const originalGetSourceFile = host.getSourceFile;
52
+ host.getSourceFile = (fileName, languageVersion) => {
53
+ // Check if we have this file cached
54
+ const cachedContent = this.files.get(fileName);
55
+ if (cachedContent !== undefined) {
56
+ return ts.createSourceFile(fileName, cachedContent, languageVersion, true);
57
+ }
58
+ // Fall back to default (for lib files, etc.)
59
+ return originalGetSourceFile(fileName, languageVersion);
60
+ };
61
+ // Override fileExists and readFile
62
+ const originalFileExists = host.fileExists;
63
+ host.fileExists = (fileName) => {
64
+ if (this.files.has(fileName))
65
+ return true;
66
+ return originalFileExists(fileName);
67
+ };
68
+ const originalReadFile = host.readFile;
69
+ host.readFile = (fileName) => {
70
+ const cached = this.files.get(fileName);
71
+ if (cached !== undefined)
72
+ return cached;
73
+ return originalReadFile(fileName);
74
+ };
75
+ const fileNames = [...this.files.keys()];
76
+ this.program = ts.createProgram(fileNames, compilerOptions, host);
77
+ this.checker = this.program.getTypeChecker();
78
+ this.log(`Program created with ${fileNames.length} files`);
79
+ return this.checker;
80
+ }
81
+ catch (error) {
82
+ this.log(`Error creating program: ${error}`);
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Get source file for a path
88
+ */
89
+ getSourceFile(filePath) {
90
+ const content = this.files.get(filePath);
91
+ if (!content) {
92
+ // Try to read from disk
93
+ try {
94
+ const diskContent = fs.readFileSync(filePath, "utf-8");
95
+ this.files.set(filePath, diskContent);
96
+ return ts.createSourceFile(filePath, diskContent, ts.ScriptTarget.Latest, true);
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
103
+ }
104
+ /**
105
+ * Get source file from program (for type checking)
106
+ */
107
+ getSourceFileFromProgram(filePath) {
108
+ // Ensure program is built
109
+ const checker = this.getChecker();
110
+ if (!checker || !this.program)
111
+ return null;
112
+ return this.program.getSourceFile(filePath) ?? null;
113
+ }
114
+ /**
115
+ * Clear all cached files
116
+ */
117
+ clear() {
118
+ this.files.clear();
119
+ this.invalidate();
120
+ }
121
+ }
122
+ // --- Singleton ---
123
+ let instance = null;
124
+ export function getTypeScriptService(verbose = false) {
125
+ if (!instance) {
126
+ instance = new TypeScriptService(verbose);
127
+ }
128
+ return instance;
129
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Shared TypeScript Service for pi-lens
3
+ *
4
+ * Creates a single ts.Program per session that is shared across all clients
5
+ * (complexity-client, type-safety-client). Avoids creating a new program per file.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as ts from "typescript";
11
+
12
+ export class TypeScriptService {
13
+ private program: ts.Program | null = null;
14
+ private checker: ts.TypeChecker | null = null;
15
+ private files = new Map<string, string>();
16
+ private log: (msg: string) => void;
17
+
18
+ constructor(verbose = false) {
19
+ this.log = verbose
20
+ ? (msg: string) => console.error(`[ts-service] ${msg}`)
21
+ : () => {};
22
+ }
23
+
24
+ /**
25
+ * Update a file's content in the service
26
+ */
27
+ updateFile(filePath: string, content: string): void {
28
+ this.files.set(filePath, content);
29
+ this.invalidate();
30
+ }
31
+
32
+ /**
33
+ * Invalidate the program (rebuild on next access)
34
+ */
35
+ invalidate(): void {
36
+ this.program = null;
37
+ this.checker = null;
38
+ }
39
+
40
+ /**
41
+ * Get the shared type checker (rebuilds program if needed)
42
+ */
43
+ getChecker(): ts.TypeChecker | null {
44
+ if (this.checker) return this.checker;
45
+
46
+ if (this.files.size === 0) return null;
47
+
48
+ try {
49
+ const compilerOptions: ts.CompilerOptions = {
50
+ target: ts.ScriptTarget.Latest,
51
+ module: ts.ModuleKind.ESNext,
52
+ strict: true,
53
+ noEmit: true,
54
+ skipLibCheck: true,
55
+ lib: ["es2020"],
56
+ };
57
+
58
+ const host = ts.createCompilerHost(compilerOptions);
59
+
60
+ // Override getSourceFile to return our cached files
61
+ const originalGetSourceFile = host.getSourceFile;
62
+ host.getSourceFile = (fileName, languageVersion) => {
63
+ // Check if we have this file cached
64
+ const cachedContent = this.files.get(fileName);
65
+ if (cachedContent !== undefined) {
66
+ return ts.createSourceFile(
67
+ fileName,
68
+ cachedContent,
69
+ languageVersion,
70
+ true,
71
+ );
72
+ }
73
+ // Fall back to default (for lib files, etc.)
74
+ return originalGetSourceFile(fileName, languageVersion);
75
+ };
76
+
77
+ // Override fileExists and readFile
78
+ const originalFileExists = host.fileExists;
79
+ host.fileExists = (fileName) => {
80
+ if (this.files.has(fileName)) return true;
81
+ return originalFileExists(fileName);
82
+ };
83
+
84
+ const originalReadFile = host.readFile;
85
+ host.readFile = (fileName) => {
86
+ const cached = this.files.get(fileName);
87
+ if (cached !== undefined) return cached;
88
+ return originalReadFile(fileName);
89
+ };
90
+
91
+ const fileNames = [...this.files.keys()];
92
+ this.program = ts.createProgram(fileNames, compilerOptions, host);
93
+ this.checker = this.program.getTypeChecker();
94
+
95
+ this.log(`Program created with ${fileNames.length} files`);
96
+ return this.checker;
97
+ } catch (error) {
98
+ this.log(`Error creating program: ${error}`);
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get source file for a path
105
+ */
106
+ getSourceFile(filePath: string): ts.SourceFile | null {
107
+ const content = this.files.get(filePath);
108
+ if (!content) {
109
+ // Try to read from disk
110
+ try {
111
+ const diskContent = fs.readFileSync(filePath, "utf-8");
112
+ this.files.set(filePath, diskContent);
113
+ return ts.createSourceFile(
114
+ filePath,
115
+ diskContent,
116
+ ts.ScriptTarget.Latest,
117
+ true,
118
+ );
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ return ts.createSourceFile(
124
+ filePath,
125
+ content,
126
+ ts.ScriptTarget.Latest,
127
+ true,
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Get source file from program (for type checking)
133
+ */
134
+ getSourceFileFromProgram(filePath: string): ts.SourceFile | null {
135
+ // Ensure program is built
136
+ const checker = this.getChecker();
137
+ if (!checker || !this.program) return null;
138
+
139
+ return this.program.getSourceFile(filePath) ?? null;
140
+ }
141
+
142
+ /**
143
+ * Clear all cached files
144
+ */
145
+ clear(): void {
146
+ this.files.clear();
147
+ this.invalidate();
148
+ }
149
+ }
150
+
151
+ // --- Singleton ---
152
+
153
+ let instance: TypeScriptService | null = null;
154
+
155
+ export function getTypeScriptService(verbose = false): TypeScriptService {
156
+ if (!instance) {
157
+ instance = new TypeScriptService(verbose);
158
+ }
159
+ return instance;
160
+ }
@@ -2,7 +2,7 @@
2
2
  * Type Safety Client for pi-lens
3
3
  *
4
4
  * Detects type safety violations that can cause runtime bugs.
5
- * Uses the TypeScript compiler API for type-aware analysis.
5
+ * Uses the shared TypeScriptService for efficient type checking.
6
6
  *
7
7
  * Checks:
8
8
  * - Switch Exhaustiveness: Missing cases in union type switches
@@ -12,6 +12,7 @@
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import * as ts from "typescript";
15
+ import { getTypeScriptService } from "./ts-service.js";
15
16
  // --- Client ---
16
17
  export class TypeSafetyClient {
17
18
  constructor(verbose = false) {
@@ -53,6 +54,11 @@ export class TypeSafetyClient {
53
54
  const checker = this.getTypeChecker(sourceFile);
54
55
  if (!checker)
55
56
  return;
57
+ // Use the source file from the program (has proper type information)
58
+ const tsService = getTypeScriptService();
59
+ const programSourceFile = tsService.getSourceFileFromProgram(sourceFile.fileName);
60
+ if (!programSourceFile)
61
+ return;
56
62
  const visit = (node) => {
57
63
  if (ts.isSwitchStatement(node)) {
58
64
  const exprType = checker.getTypeAtLocation(node.expression);
@@ -94,11 +100,11 @@ export class TypeSafetyClient {
94
100
  // Find missing cases
95
101
  const missingCases = literalValues.filter((v) => !coveredCases.has(v));
96
102
  if (missingCases.length > 0 && !hasDefault) {
97
- const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
98
- const exprText = node.expression.getText(sourceFile);
103
+ const line = programSourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
104
+ const exprText = node.expression.getText(programSourceFile);
99
105
  const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
100
106
  issues.push({
101
- filePath: sourceFile.fileName,
107
+ filePath: programSourceFile.fileName,
102
108
  rule: "switch-exhaustiveness",
103
109
  line,
104
110
  message: `Switch on '${exprText}' is not exhaustive. Missing cases: ${typeStr}`,
@@ -110,35 +116,21 @@ export class TypeSafetyClient {
110
116
  }
111
117
  ts.forEachChild(node, visit);
112
118
  };
113
- ts.forEachChild(sourceFile, visit);
119
+ ts.forEachChild(programSourceFile, visit);
114
120
  }
115
121
  /**
116
- * Get type checker for the source file
122
+ * Get type checker from shared service
117
123
  */
118
124
  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;
125
+ const tsService = getTypeScriptService();
126
+ // Update the file in the shared service
127
+ const content = fs.readFileSync(sourceFile.fileName, "utf-8");
128
+ tsService.updateFile(sourceFile.fileName, content);
129
+ const checker = tsService.getChecker();
130
+ if (!checker) {
131
+ this.log("Could not get type checker, skipping exhaustiveness check");
141
132
  }
133
+ return checker;
142
134
  }
143
135
  }
144
136
  // --- Singleton ---
@@ -2,7 +2,7 @@
2
2
  * Type Safety Client for pi-lens
3
3
  *
4
4
  * Detects type safety violations that can cause runtime bugs.
5
- * Uses the TypeScript compiler API for type-aware analysis.
5
+ * Uses the shared TypeScriptService for efficient type checking.
6
6
  *
7
7
  * Checks:
8
8
  * - Switch Exhaustiveness: Missing cases in union type switches
@@ -13,6 +13,7 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
  import * as ts from "typescript";
16
+ import { getTypeScriptService } from "./ts-service.js";
16
17
 
17
18
  // --- Types ---
18
19
 
@@ -87,6 +88,11 @@ export class TypeSafetyClient {
87
88
  const checker = this.getTypeChecker(sourceFile);
88
89
  if (!checker) return;
89
90
 
91
+ // Use the source file from the program (has proper type information)
92
+ const tsService = getTypeScriptService();
93
+ const programSourceFile = tsService.getSourceFileFromProgram(sourceFile.fileName);
94
+ if (!programSourceFile) return;
95
+
90
96
  const visit = (node: ts.Node) => {
91
97
  if (ts.isSwitchStatement(node)) {
92
98
  const exprType = checker.getTypeAtLocation(node.expression);
@@ -137,15 +143,15 @@ export class TypeSafetyClient {
137
143
  );
138
144
 
139
145
  if (missingCases.length > 0 && !hasDefault) {
140
- const line = sourceFile.getLineAndCharacterOfPosition(
146
+ const line = programSourceFile.getLineAndCharacterOfPosition(
141
147
  node.getStart(),
142
148
  ).line + 1;
143
149
 
144
- const exprText = node.expression.getText(sourceFile);
150
+ const exprText = node.expression.getText(programSourceFile);
145
151
  const typeStr = missingCases.map((c) => `'${c}'`).join(", ");
146
152
 
147
153
  issues.push({
148
- filePath: sourceFile.fileName,
154
+ filePath: programSourceFile.fileName,
149
155
  rule: "switch-exhaustiveness",
150
156
  line,
151
157
  message: `Switch on '${exprText}' is not exhaustive. Missing cases: ${typeStr}`,
@@ -159,41 +165,24 @@ export class TypeSafetyClient {
159
165
  ts.forEachChild(node, visit);
160
166
  };
161
167
 
162
- ts.forEachChild(sourceFile, visit);
168
+ ts.forEachChild(programSourceFile, visit);
163
169
  }
164
170
 
165
171
  /**
166
- * Get type checker for the source file
172
+ * Get type checker from shared service
167
173
  */
168
174
  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
- );
175
+ const tsService = getTypeScriptService();
191
176
 
192
- return program.getTypeChecker();
193
- } catch {
194
- this.log("Could not create type checker, skipping exhaustiveness check");
195
- return null;
177
+ // Update the file in the shared service
178
+ const content = fs.readFileSync(sourceFile.fileName, "utf-8");
179
+ tsService.updateFile(sourceFile.fileName, content);
180
+
181
+ const checker = tsService.getChecker();
182
+ if (!checker) {
183
+ this.log("Could not get type checker, skipping exhaustiveness check");
196
184
  }
185
+ return checker;
197
186
  }
198
187
 
199
188
  }
package/index.ts CHANGED
@@ -613,11 +613,7 @@ export default function (pi: ExtensionAPI) {
613
613
  string,
614
614
  { type: "biome" | "agent" | "skip"; note: string }
615
615
  > = {
616
- "no-var": { type: "biome", note: "auto-fixed by Biome --write" },
617
- "prefer-template": { type: "biome", note: "auto-fixed by Biome --write" },
618
- "no-useless-concat": { type: "biome", note: "auto-fixed by Biome --write" },
619
616
  "no-lonely-if": { type: "biome", note: "auto-fixed by Biome --write" },
620
- "prefer-const": { type: "biome", note: "auto-fixed by Biome --write" },
621
617
  "empty-catch": {
622
618
  type: "agent",
623
619
  note: "Add this.log('Error: ' + err.message) to the catch block",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "2.0.28",
3
+ "version": "2.0.29",
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",
@@ -1,17 +0,0 @@
1
- id: no-useless-concat
2
- language: TypeScript
3
- message: "Unnecessary string concatenation with adjacent string literals"
4
- severity: warning
5
- note: |
6
- Adjacent string literals should be combined into a single string literal
7
- rather than concatenated with the + operator.
8
- rule:
9
- all:
10
- - pattern: $A + $B
11
- - any:
12
- - has:
13
- kind: string
14
- pattern: "$A"
15
- - has:
16
- kind: string
17
- pattern: "$B"
@@ -1,10 +0,0 @@
1
- id: no-var
2
- language: TypeScript
3
- message: "Use 'const' or 'let' instead of 'var'"
4
- severity: warning
5
- note: |
6
- var has function scope and can lead to unexpected hoisting behavior.
7
- Use const for values that don't change, let for values that do.
8
- rule:
9
- pattern: var $X = $Y
10
- fix: let $X = $Y
@@ -1,20 +0,0 @@
1
- id: prefer-template
2
- language: TypeScript
3
- message: "Use template literals instead of string concatenation"
4
- severity: warning
5
- note: |
6
- Template literals are more readable and less error-prone than string
7
- concatenation with the + operator.
8
- rule:
9
- all:
10
- - pattern: $A + $B
11
- - any:
12
- - inside:
13
- kind: binary_expression
14
- has:
15
- pattern: $A + $B
16
- any:
17
- - has:
18
- kind: string
19
- - has:
20
- kind: template_string