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 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-review [path]` | Code review: design smells + complexity metrics |
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-review", {
218
+ pi.registerCommand("lens-booboo", {
219
219
  description:
220
- "Code review: design smells + complexity metrics. Usage: /lens-review [path]",
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: Complexity metrics
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "Real-time code feedback for pi — TypeScript LSP, Biome, ast-grep, Ruff, TODO scanner, dead code, duplicate detection, type coverage",
5
5
  "repository": {
6
6
  "type": "git",