pi-lens 2.0.7 → 2.0.8

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.
Files changed (34) hide show
  1. package/clients/ast-grep-client.test.ts +146 -116
  2. package/clients/ast-grep-client.ts +645 -551
  3. package/clients/biome-client.test.ts +154 -137
  4. package/clients/biome-client.ts +397 -337
  5. package/clients/complexity-client.test.ts +188 -200
  6. package/clients/complexity-client.ts +815 -667
  7. package/clients/dependency-checker.test.ts +55 -55
  8. package/clients/dependency-checker.ts +358 -333
  9. package/clients/go-client.test.ts +121 -111
  10. package/clients/go-client.ts +218 -216
  11. package/clients/jscpd-client.test.ts +132 -132
  12. package/clients/jscpd-client.ts +155 -118
  13. package/clients/knip-client.test.ts +123 -133
  14. package/clients/knip-client.ts +231 -218
  15. package/clients/metrics-client.test.ts +171 -167
  16. package/clients/metrics-client.ts +283 -252
  17. package/clients/ruff-client.test.ts +128 -117
  18. package/clients/ruff-client.ts +300 -269
  19. package/clients/rust-client.test.ts +104 -85
  20. package/clients/rust-client.ts +241 -234
  21. package/clients/subprocess-client.ts +1 -1
  22. package/clients/test-runner-client.test.ts +248 -215
  23. package/clients/test-runner-client.ts +728 -608
  24. package/clients/test-utils.ts +10 -3
  25. package/clients/todo-scanner.test.ts +288 -202
  26. package/clients/todo-scanner.ts +225 -187
  27. package/clients/type-coverage-client.test.ts +119 -119
  28. package/clients/type-coverage-client.ts +142 -115
  29. package/clients/types.ts +28 -28
  30. package/clients/typescript-client.test.ts +99 -93
  31. package/clients/typescript-client.ts +527 -502
  32. package/index.ts +662 -212
  33. package/package.json +1 -1
  34. package/tsconfig.json +12 -12
@@ -13,199 +13,237 @@ import * as path from "node:path";
13
13
  // --- Types ---
14
14
 
15
15
  export interface TodoItem {
16
- type: "TODO" | "FIXME" | "HACK" | "XXX" | "NOTE" | "DEPRECATED" | "BUG";
17
- message: string;
18
- file: string;
19
- line: number;
20
- column: number;
16
+ type: "TODO" | "FIXME" | "HACK" | "XXX" | "NOTE" | "DEPRECATED" | "BUG";
17
+ message: string;
18
+ file: string;
19
+ line: number;
20
+ column: number;
21
21
  }
22
22
 
23
23
  export interface TodoScanResult {
24
- items: TodoItem[];
25
- byType: Map<string, TodoItem[]>;
26
- byFile: Map<string, TodoItem[]>;
24
+ items: TodoItem[];
25
+ byType: Map<string, TodoItem[]>;
26
+ byFile: Map<string, TodoItem[]>;
27
27
  }
28
28
 
29
29
  // --- Scanner ---
30
30
 
31
31
  export class TodoScanner {
32
- private readonly pattern = /\b(TODO|FIXME|HACK|XXX|NOTE|DEPRECATED|BUG)\b\s*[\(:]?\s*(.+)/gi;
33
-
34
- /**
35
- * Check if a match position is inside a comment context.
36
- * Handles: // line comments, star-slash block comments, * JSDoc lines, # Python comments
37
- */
38
- private isInComment(line: string, matchIndex: number): boolean {
39
- const trimmed = line.trimStart();
40
-
41
- // Line starts with comment markers — entire line is a comment
42
- if (/^\/\/|^\/\*|^\*|^#/.test(trimmed)) return true;
43
-
44
- // Check if there's a // before the match position (not inside a string)
45
- const beforeMatch = line.slice(0, matchIndex);
46
- const lineCommentPos = beforeMatch.lastIndexOf("//");
47
- if (lineCommentPos !== -1) {
48
- // Count quotes before // to see if it's inside a string
49
- const beforeComment = beforeMatch.slice(0, lineCommentPos);
50
- const singleQuotes = (beforeComment.match(/'/g) || []).length;
51
- const doubleQuotes = (beforeComment.match(/"/g) || []).length;
52
- const backticks = (beforeComment.match(/`/g) || []).length;
53
- if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
54
- return true;
55
- }
56
- }
57
-
58
- // Check for /* ... */ block comment before match
59
- const blockOpen = beforeMatch.lastIndexOf("/*");
60
- const blockClose = beforeMatch.lastIndexOf("*/");
61
- if (blockOpen !== -1 && blockClose < blockOpen) return true;
62
-
63
- // Check for # comment (Python)
64
- const hashPos = beforeMatch.lastIndexOf("#");
65
- if (hashPos !== -1) {
66
- const beforeHash = beforeMatch.slice(0, hashPos);
67
- const singleQuotes = (beforeHash.match(/'/g) || []).length;
68
- const doubleQuotes = (beforeHash.match(/"/g) || []).length;
69
- if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
70
- return true;
71
- }
72
- }
73
-
74
- return false;
75
- }
76
-
77
- /**
78
- * Scan a single file for TODOs
79
- */
80
- scanFile(filePath: string): TodoItem[] {
81
- const absolutePath = path.resolve(filePath);
82
- if (!fs.existsSync(absolutePath)) return [];
83
-
84
- const content = fs.readFileSync(absolutePath, "utf-8");
85
- const lines = content.split("\n");
86
- const items: TodoItem[] = [];
87
-
88
- for (let i = 0; i < lines.length; i++) {
89
- const line = lines[i];
90
- const matches = line.matchAll(this.pattern);
91
-
92
- for (const match of matches) {
93
- // Skip matches that aren't inside comments
94
- if (!this.isInComment(line, match.index ?? 0)) continue;
95
-
96
- const type = match[1].toUpperCase() as TodoItem["type"];
97
- const message = (match[2] || "").trim().replace(/\s*\*\/\s*$/, ""); // Strip closing comment
98
-
99
- items.push({
100
- type,
101
- message: message.slice(0, 200), // Limit message length
102
- file: path.relative(process.cwd(), absolutePath),
103
- line: i + 1,
104
- column: match.index || 0,
105
- });
106
- }
107
- }
108
-
109
- return items;
110
- }
111
-
112
- /**
113
- * Scan a directory recursively
114
- */
115
- scanDirectory(dirPath: string, extensions = [".ts", ".tsx", ".js", ".jsx", ".py"]): TodoScanResult {
116
- const items: TodoItem[] = [];
117
-
118
- const scan = (dir: string) => {
119
- if (!fs.existsSync(dir)) return;
120
-
121
- const entries = fs.readdirSync(dir, { withFileTypes: true });
122
-
123
- for (const entry of entries) {
124
- const fullPath = path.join(dir, entry.name);
125
-
126
- if (entry.isDirectory()) {
127
- // Skip common non-source directories
128
- if (["node_modules", ".git", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
129
- scan(fullPath);
130
- } else if (extensions.some(ext => entry.name.endsWith(ext))) {
131
- // Skip this scanner file — its own type literals and regex cause false positives
132
- if (entry.name === "todo-scanner.ts" || entry.name === "todo-scanner.js") continue;
133
- // Skip test files — intentional annotations are test fixtures, not work items
134
- if (/\.(test|spec)\.[jt]sx?$/.test(entry.name)) continue;
135
- items.push(...this.scanFile(fullPath));
136
- }
137
- }
138
- };
139
-
140
- scan(path.resolve(dirPath));
141
-
142
- // Group by type
143
- const byType = new Map<string, TodoItem[]>();
144
- for (const item of items) {
145
- const existing = byType.get(item.type) || [];
146
- existing.push(item);
147
- byType.set(item.type, existing);
148
- }
149
-
150
- // Group by file
151
- const byFile = new Map<string, TodoItem[]>();
152
- for (const item of items) {
153
- const existing = byFile.get(item.file) || [];
154
- existing.push(item);
155
- byFile.set(item.file, existing);
156
- }
157
-
158
- return { items, byType, byFile };
159
- }
160
-
161
- /**
162
- * Format scan results for LLM consumption
163
- */
164
- formatResult(result: TodoScanResult, maxItems = 30): string {
165
- if (result.items.length === 0) return "";
166
-
167
- let output = `[TODOs] ${result.items.length} annotation(s) found`;
168
-
169
- // Summary by type
170
- const typeCounts: string[] = [];
171
- for (const [type, items] of result.byType) {
172
- typeCounts.push(`${items.length} ${type}`);
173
- }
174
- if (typeCounts.length > 0) {
175
- output += ` (${typeCounts.join(", ")})`;
176
- }
177
- output += ":\n";
178
-
179
- // Show by priority: FIXME/HACK first, then TODO
180
- const priorityOrder: TodoItem["type"][] = ["FIXME", "HACK", "BUG", "DEPRECATED", "TODO", "XXX", "NOTE"];
181
- const sorted = [...result.items].sort((a, b) => {
182
- const aIdx = priorityOrder.indexOf(a.type);
183
- const bIdx = priorityOrder.indexOf(b.type);
184
- return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
185
- });
186
-
187
- for (const item of sorted.slice(0, maxItems)) {
188
- const icon = this.getIcon(item.type);
189
- output += ` ${icon} ${item.file}:${item.line} ${item.type}: ${item.message}\n`;
190
- }
191
-
192
- if (result.items.length > maxItems) {
193
- output += ` ... and ${result.items.length - maxItems} more\n`;
194
- }
195
-
196
- return output;
197
- }
198
-
199
- private getIcon(type: TodoItem["type"]): string {
200
- switch (type) {
201
- case "FIXME": return "🔴";
202
- case "HACK": return "🟠";
203
- case "BUG": return "🐛";
204
- case "DEPRECATED": return "⚠️";
205
- case "TODO": return "📝";
206
- case "XXX": return "❌";
207
- case "NOTE": return "ℹ️";
208
- default: return "";
209
- }
210
- }
32
+ private readonly pattern =
33
+ /\b(TODO|FIXME|HACK|XXX|NOTE|DEPRECATED|BUG)\b\s*[(:]?\s*(.+)/gi;
34
+
35
+ /**
36
+ * Check if a match position is inside a comment context.
37
+ * Handles: // line comments, star-slash block comments, * JSDoc lines, # Python comments
38
+ */
39
+ private isInComment(line: string, matchIndex: number): boolean {
40
+ const trimmed = line.trimStart();
41
+
42
+ // Line starts with comment markers — entire line is a comment
43
+ if (/^\/\/|^\/\*|^\*|^#/.test(trimmed)) return true;
44
+
45
+ // Check if there's a // before the match position (not inside a string)
46
+ const beforeMatch = line.slice(0, matchIndex);
47
+ const lineCommentPos = beforeMatch.lastIndexOf("//");
48
+ if (lineCommentPos !== -1) {
49
+ // Count quotes before // to see if it's inside a string
50
+ const beforeComment = beforeMatch.slice(0, lineCommentPos);
51
+ const singleQuotes = (beforeComment.match(/'/g) || []).length;
52
+ const doubleQuotes = (beforeComment.match(/"/g) || []).length;
53
+ const backticks = (beforeComment.match(/`/g) || []).length;
54
+ if (
55
+ singleQuotes % 2 === 0 &&
56
+ doubleQuotes % 2 === 0 &&
57
+ backticks % 2 === 0
58
+ ) {
59
+ return true;
60
+ }
61
+ }
62
+
63
+ // Check for /* ... */ block comment before match
64
+ const blockOpen = beforeMatch.lastIndexOf("/*");
65
+ const blockClose = beforeMatch.lastIndexOf("*/");
66
+ if (blockOpen !== -1 && blockClose < blockOpen) return true;
67
+
68
+ // Check for # comment (Python)
69
+ const hashPos = beforeMatch.lastIndexOf("#");
70
+ if (hashPos !== -1) {
71
+ const beforeHash = beforeMatch.slice(0, hashPos);
72
+ const singleQuotes = (beforeHash.match(/'/g) || []).length;
73
+ const doubleQuotes = (beforeHash.match(/"/g) || []).length;
74
+ if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
75
+ return true;
76
+ }
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Scan a single file for TODOs
84
+ */
85
+ scanFile(filePath: string): TodoItem[] {
86
+ const absolutePath = path.resolve(filePath);
87
+ if (!fs.existsSync(absolutePath)) return [];
88
+
89
+ const content = fs.readFileSync(absolutePath, "utf-8");
90
+ const lines = content.split("\n");
91
+ const items: TodoItem[] = [];
92
+
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const line = lines[i];
95
+ const matches = line.matchAll(this.pattern);
96
+
97
+ for (const match of matches) {
98
+ // Skip matches that aren't inside comments
99
+ if (!this.isInComment(line, match.index ?? 0)) continue;
100
+
101
+ const type = match[1].toUpperCase() as TodoItem["type"];
102
+ const message = (match[2] || "").trim().replace(/\s*\*\/\s*$/, ""); // Strip closing comment
103
+
104
+ items.push({
105
+ type,
106
+ message: message.slice(0, 200), // Limit message length
107
+ file: path.relative(process.cwd(), absolutePath),
108
+ line: i + 1,
109
+ column: match.index || 0,
110
+ });
111
+ }
112
+ }
113
+
114
+ return items;
115
+ }
116
+
117
+ /**
118
+ * Scan a directory recursively
119
+ */
120
+ scanDirectory(
121
+ dirPath: string,
122
+ extensions = [".ts", ".tsx", ".js", ".jsx", ".py"],
123
+ ): TodoScanResult {
124
+ const items: TodoItem[] = [];
125
+
126
+ const scan = (dir: string) => {
127
+ if (!fs.existsSync(dir)) return;
128
+
129
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
130
+
131
+ for (const entry of entries) {
132
+ const fullPath = path.join(dir, entry.name);
133
+
134
+ if (entry.isDirectory()) {
135
+ // Skip common non-source directories
136
+ if (
137
+ [
138
+ "node_modules",
139
+ ".git",
140
+ "dist",
141
+ "build",
142
+ ".next",
143
+ "coverage",
144
+ ].includes(entry.name)
145
+ )
146
+ continue;
147
+ scan(fullPath);
148
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
149
+ // Skip this scanner file — its own type literals and regex cause false positives
150
+ if (
151
+ entry.name === "todo-scanner.ts" ||
152
+ entry.name === "todo-scanner.js"
153
+ )
154
+ continue;
155
+ // Skip test files — intentional annotations are test fixtures, not work items
156
+ if (/\.(test|spec)\.[jt]sx?$/.test(entry.name)) continue;
157
+ items.push(...this.scanFile(fullPath));
158
+ }
159
+ }
160
+ };
161
+
162
+ scan(path.resolve(dirPath));
163
+
164
+ // Group by type
165
+ const byType = new Map<string, TodoItem[]>();
166
+ for (const item of items) {
167
+ const existing = byType.get(item.type) || [];
168
+ existing.push(item);
169
+ byType.set(item.type, existing);
170
+ }
171
+
172
+ // Group by file
173
+ const byFile = new Map<string, TodoItem[]>();
174
+ for (const item of items) {
175
+ const existing = byFile.get(item.file) || [];
176
+ existing.push(item);
177
+ byFile.set(item.file, existing);
178
+ }
179
+
180
+ return { items, byType, byFile };
181
+ }
182
+
183
+ /**
184
+ * Format scan results for LLM consumption
185
+ */
186
+ formatResult(result: TodoScanResult, maxItems = 30): string {
187
+ if (result.items.length === 0) return "";
188
+
189
+ let output = `[TODOs] ${result.items.length} annotation(s) found`;
190
+
191
+ // Summary by type
192
+ const typeCounts: string[] = [];
193
+ for (const [type, items] of result.byType) {
194
+ typeCounts.push(`${items.length} ${type}`);
195
+ }
196
+ if (typeCounts.length > 0) {
197
+ output += ` (${typeCounts.join(", ")})`;
198
+ }
199
+ output += ":\n";
200
+
201
+ // Show by priority: FIXME/HACK first, then TODO
202
+ const priorityOrder: TodoItem["type"][] = [
203
+ "FIXME",
204
+ "HACK",
205
+ "BUG",
206
+ "DEPRECATED",
207
+ "TODO",
208
+ "XXX",
209
+ "NOTE",
210
+ ];
211
+ const sorted = [...result.items].sort((a, b) => {
212
+ const aIdx = priorityOrder.indexOf(a.type);
213
+ const bIdx = priorityOrder.indexOf(b.type);
214
+ return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
215
+ });
216
+
217
+ for (const item of sorted.slice(0, maxItems)) {
218
+ const icon = this.getIcon(item.type);
219
+ output += ` ${icon} ${item.file}:${item.line} — ${item.type}: ${item.message}\n`;
220
+ }
221
+
222
+ if (result.items.length > maxItems) {
223
+ output += ` ... and ${result.items.length - maxItems} more\n`;
224
+ }
225
+
226
+ return output;
227
+ }
228
+
229
+ private getIcon(type: TodoItem["type"]): string {
230
+ switch (type) {
231
+ case "FIXME":
232
+ return "🔴";
233
+ case "HACK":
234
+ return "🟠";
235
+ case "BUG":
236
+ return "🐛";
237
+ case "DEPRECATED":
238
+ return "⚠️";
239
+ case "TODO":
240
+ return "📝";
241
+ case "XXX":
242
+ return "❌";
243
+ case "NOTE":
244
+ return "ℹ️";
245
+ default:
246
+ return "•";
247
+ }
248
+ }
211
249
  }