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.
- package/clients/ast-grep-client.test.ts +146 -116
- package/clients/ast-grep-client.ts +645 -551
- package/clients/biome-client.test.ts +154 -137
- package/clients/biome-client.ts +397 -337
- package/clients/complexity-client.test.ts +188 -200
- package/clients/complexity-client.ts +815 -667
- package/clients/dependency-checker.test.ts +55 -55
- package/clients/dependency-checker.ts +358 -333
- package/clients/go-client.test.ts +121 -111
- package/clients/go-client.ts +218 -216
- package/clients/jscpd-client.test.ts +132 -132
- package/clients/jscpd-client.ts +155 -118
- package/clients/knip-client.test.ts +123 -133
- package/clients/knip-client.ts +231 -218
- package/clients/metrics-client.test.ts +171 -167
- package/clients/metrics-client.ts +283 -252
- package/clients/ruff-client.test.ts +128 -117
- package/clients/ruff-client.ts +300 -269
- package/clients/rust-client.test.ts +104 -85
- package/clients/rust-client.ts +241 -234
- package/clients/subprocess-client.ts +1 -1
- package/clients/test-runner-client.test.ts +248 -215
- package/clients/test-runner-client.ts +728 -608
- package/clients/test-utils.ts +10 -3
- package/clients/todo-scanner.test.ts +288 -202
- package/clients/todo-scanner.ts +225 -187
- package/clients/type-coverage-client.test.ts +119 -119
- package/clients/type-coverage-client.ts +142 -115
- package/clients/types.ts +28 -28
- package/clients/typescript-client.test.ts +99 -93
- package/clients/typescript-client.ts +527 -502
- package/index.ts +662 -212
- package/package.json +1 -1
- package/tsconfig.json +12 -12
package/clients/todo-scanner.ts
CHANGED
|
@@ -13,199 +13,237 @@ import * as path from "node:path";
|
|
|
13
13
|
// --- Types ---
|
|
14
14
|
|
|
15
15
|
export interface TodoItem {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
}
|