pi-lens 1.3.6 → 1.3.7
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/README.md +1 -1
- package/clients/ast-grep-client.ts +97 -0
- package/index.ts +21 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Example:
|
|
|
73
73
|
| `/lens-dead-code` | Find unused exports/files/dependencies (requires knip) |
|
|
74
74
|
| `/lens-deps` | Circular dependency scan (requires madge) |
|
|
75
75
|
| `/lens-format [file\|--all]` | Apply Biome formatting |
|
|
76
|
-
| `/lens-
|
|
76
|
+
| `/lens-booboo [path]` | Code review: design smells + complexity metrics |
|
|
77
77
|
|
|
78
78
|
### On-demand tools
|
|
79
79
|
|
|
@@ -200,6 +200,103 @@ export class AstGrepClient {
|
|
|
200
200
|
return { matches: result.matches, applied: apply, error: result.error };
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Find similar functions by comparing normalized AST structure
|
|
205
|
+
*/
|
|
206
|
+
async findSimilarFunctions(dir: string, lang: string = "typescript"): Promise<Array<{ pattern: string; functions: Array<{ name: string; file: string; line: number }> }>> {
|
|
207
|
+
if (!this.isAvailable()) return [];
|
|
208
|
+
|
|
209
|
+
const tmpDir = require("node:os").tmpdir();
|
|
210
|
+
const ts = Date.now();
|
|
211
|
+
const ruleDir = require("node:path").join(tmpDir, `pi-lens-similar-${ts}`);
|
|
212
|
+
const rulesSubdir = require("node:path").join(ruleDir, "rules");
|
|
213
|
+
const ruleFile = require("node:path").join(rulesSubdir, "find-functions.yml");
|
|
214
|
+
const configFile = require("node:path").join(ruleDir, ".sgconfig.yml");
|
|
215
|
+
|
|
216
|
+
require("node:fs").mkdirSync(rulesSubdir, { recursive: true });
|
|
217
|
+
require("node:fs").writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
|
|
218
|
+
require("node:fs").writeFileSync(ruleFile, `id: find-functions
|
|
219
|
+
language: ${lang}
|
|
220
|
+
rule:
|
|
221
|
+
kind: function_declaration
|
|
222
|
+
severity: info
|
|
223
|
+
message: found
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const result = spawnSync("npx", [
|
|
228
|
+
"sg", "scan",
|
|
229
|
+
"--config", configFile,
|
|
230
|
+
"--json",
|
|
231
|
+
dir,
|
|
232
|
+
], {
|
|
233
|
+
encoding: "utf-8",
|
|
234
|
+
timeout: 30000,
|
|
235
|
+
shell: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const output = result.stdout || result.stderr || "";
|
|
239
|
+
if (!output.trim()) return [];
|
|
240
|
+
|
|
241
|
+
const items = JSON.parse(output);
|
|
242
|
+
const matches = Array.isArray(items) ? items : [items];
|
|
243
|
+
|
|
244
|
+
// Normalize each function by removing identifiers
|
|
245
|
+
const normalized = new Map<string, Array<{ name: string; file: string; line: number }>>();
|
|
246
|
+
|
|
247
|
+
for (const item of matches) {
|
|
248
|
+
const text = item.text || "";
|
|
249
|
+
const nameMatch = text.match(/function\s+(\w+)/);
|
|
250
|
+
if (!nameMatch || !nameMatch[1]) continue;
|
|
251
|
+
|
|
252
|
+
// Normalize by replacing function name with FN, parameters with P1..Pn, and removing specific values
|
|
253
|
+
let normalizedText = text
|
|
254
|
+
.replace(/function\s+\w+/, "function FN")
|
|
255
|
+
.replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
|
|
256
|
+
.replace(/["'].*?["']/g, "STR")
|
|
257
|
+
.replace(/`[^`]*`/g, "TMPL")
|
|
258
|
+
.replace(/\b\d+\b/g, "NUM")
|
|
259
|
+
.replace(/\btrue\b|\bfalse\b/g, "BOOL")
|
|
260
|
+
.replace(/\/\/.*/g, "")
|
|
261
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
262
|
+
.replace(/\s+/g, " ")
|
|
263
|
+
.trim();
|
|
264
|
+
|
|
265
|
+
// Extract just the body structure
|
|
266
|
+
const bodyMatch = normalizedText.match(/\{(.*)\}/);
|
|
267
|
+
const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
|
|
268
|
+
|
|
269
|
+
// Use first 200 chars as signature
|
|
270
|
+
const signature = body.slice(0, 200);
|
|
271
|
+
|
|
272
|
+
if (!normalized.has(signature)) {
|
|
273
|
+
normalized.set(signature, []);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const line = item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0;
|
|
277
|
+
normalized.get(signature)!.push({
|
|
278
|
+
name: nameMatch[1],
|
|
279
|
+
file: item.file,
|
|
280
|
+
line: line + 1,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Return groups with more than one function
|
|
285
|
+
const result_groups: Array<{ pattern: string; functions: Array<{ name: string; file: string; line: number }> }> = [];
|
|
286
|
+
for (const [pattern, functions] of normalized) {
|
|
287
|
+
if (functions.length > 1) {
|
|
288
|
+
result_groups.push({ pattern, functions });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result_groups;
|
|
293
|
+
} catch {
|
|
294
|
+
return [];
|
|
295
|
+
} finally {
|
|
296
|
+
try { require("node:fs").rmSync(ruleDir, { recursive: true, force: true }); } catch {}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
203
300
|
/**
|
|
204
301
|
* Scan for exported function names in a directory
|
|
205
302
|
*/
|
package/index.ts
CHANGED
|
@@ -215,9 +215,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
215
215
|
},
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
pi.registerCommand("lens-
|
|
218
|
+
pi.registerCommand("lens-booboo", {
|
|
219
219
|
description:
|
|
220
|
-
"Code review: design smells + complexity metrics. Usage: /lens-
|
|
220
|
+
"Code review: design smells + complexity metrics. Usage: /lens-booboo [path]",
|
|
221
221
|
handler: async (args, ctx) => {
|
|
222
222
|
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
223
223
|
ctx.ui.notify("🔍 Running code review...", "info");
|
|
@@ -286,7 +286,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// Part 2:
|
|
289
|
+
// Part 2: Similar functions (advanced duplicate detection)
|
|
290
|
+
if (astGrepClient.isAvailable()) {
|
|
291
|
+
const similarGroups = await astGrepClient.findSimilarFunctions(targetPath, "typescript");
|
|
292
|
+
if (similarGroups.length > 0) {
|
|
293
|
+
let report = `[Similar Functions] ${similarGroups.length} group(s) of structurally similar functions:\n`;
|
|
294
|
+
for (const group of similarGroups.slice(0, 5)) {
|
|
295
|
+
report += ` Pattern: ${group.functions.map(f => f.name).join(", ")}\n`;
|
|
296
|
+
for (const fn of group.functions) {
|
|
297
|
+
report += ` ${fn.name} (${path.basename(fn.file)}:${fn.line})\n`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (similarGroups.length > 5) {
|
|
301
|
+
report += ` ... and ${similarGroups.length - 5} more groups\n`;
|
|
302
|
+
}
|
|
303
|
+
parts.push(report);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Part 3: Complexity metrics
|
|
290
308
|
const results: import("./clients/complexity-client.js").FileComplexity[] = [];
|
|
291
309
|
|
|
292
310
|
const scanDir = (dir: string) => {
|
package/package.json
CHANGED