pi-lens 1.3.6 → 1.3.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/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
  */
@@ -238,6 +238,7 @@ function long(): number {
238
238
  maintainabilityIndex: 75,
239
239
  linesOfCode: 100,
240
240
  commentLines: 10,
241
+ codeEntropy: 0.5,
241
242
  };
242
243
 
243
244
  const formatted = client.formatMetrics(metrics);
@@ -259,6 +260,7 @@ function long(): number {
259
260
  maintainabilityIndex: 25,
260
261
  linesOfCode: 500,
261
262
  commentLines: 10,
263
+ codeEntropy: 0.5,
262
264
  };
263
265
 
264
266
  const formatted = client.formatMetrics(metrics);
@@ -34,6 +34,7 @@ export interface FileComplexity {
34
34
  maintainabilityIndex: number; // 0-100
35
35
  linesOfCode: number;
36
36
  commentLines: number;
37
+ codeEntropy: number; // Shannon entropy (0-1, lower = more predictable)
37
38
  }
38
39
 
39
40
  export interface FunctionMetrics {
@@ -195,6 +196,9 @@ export class ComplexityClient {
195
196
  commentLines
196
197
  );
197
198
 
199
+ // Code Entropy (Shannon entropy of code tokens)
200
+ const codeEntropy = this.calculateCodeEntropy(content);
201
+
198
202
  return {
199
203
  filePath: path.relative(process.cwd(), absolutePath),
200
204
  maxNestingDepth,
@@ -208,6 +212,7 @@ export class ComplexityClient {
208
212
  maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
209
213
  linesOfCode: codeLines,
210
214
  commentLines,
215
+ codeEntropy: Math.round(codeEntropy * 1000) / 1000,
211
216
  };
212
217
  } catch (err: any) {
213
218
  this.log(`Analysis error for ${filePath}: ${err.message}`);
@@ -242,6 +247,13 @@ export class ComplexityClient {
242
247
  parts.push(` Max nesting: ${metrics.maxNestingDepth} levels (consider extracting)`);
243
248
  }
244
249
 
250
+ // Code entropy (0-1, lower = more predictable)
251
+ if (metrics.codeEntropy > 0.8) {
252
+ parts.push(` Entropy: ${metrics.codeEntropy} (high — code may be unpredictable/AI-generated)`);
253
+ } else if (metrics.codeEntropy < 0.3) {
254
+ parts.push(` Entropy: ${metrics.codeEntropy} (low — repetitive patterns detected)`);
255
+ }
256
+
245
257
  // Function length
246
258
  if (metrics.maxFunctionLength > 50) {
247
259
  parts.push(` Longest function: ${metrics.maxFunctionLength} lines (avg: ${metrics.avgFunctionLength})`);
@@ -491,6 +503,45 @@ export class ComplexityClient {
491
503
  return totalOps * Math.log2(uniqueOps);
492
504
  }
493
505
 
506
+ /**
507
+ * Calculate Shannon entropy of code tokens
508
+ * Measures predictability/uniformity of code
509
+ * 0 = completely uniform, 1 = maximum entropy
510
+ */
511
+ private calculateCodeEntropy(sourceText: string): number {
512
+ // Tokenize by splitting on whitespace and common delimiters
513
+ const tokens = sourceText
514
+ .replace(/\/\/.*/g, "") // Remove single-line comments
515
+ .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
516
+ .replace(/["'`][^"'`]*["'`]/g, "STR") // Normalize strings
517
+ .replace(/\b\d+(\.\d+)?\b/g, "NUM") // Normalize numbers
518
+ .split(/[\s\n\r\t,;:()[\]{}=<>!&|+\-*/%^~?]+/)
519
+ .filter(t => t.length > 0);
520
+
521
+ if (tokens.length === 0) return 0;
522
+
523
+ // Count token frequencies
524
+ const freq = new Map<string, number>();
525
+ for (const token of tokens) {
526
+ freq.set(token, (freq.get(token) || 0) + 1);
527
+ }
528
+
529
+ // Calculate Shannon entropy: H = -sum(p * log2(p))
530
+ let entropy = 0;
531
+ for (const count of freq.values()) {
532
+ const p = count / tokens.length;
533
+ if (p > 0) {
534
+ entropy -= p * Math.log2(p);
535
+ }
536
+ }
537
+
538
+ // Normalize to 0-1 range
539
+ const maxEntropy = Math.log2(freq.size);
540
+ if (maxEntropy === 0) return 0;
541
+
542
+ return Math.min(1, entropy / maxEntropy);
543
+ }
544
+
494
545
  private isKeyword(text: string): boolean {
495
546
  const keywords = new Set([
496
547
  "if", "else", "for", "while", "do", "switch", "case", "break", "continue",
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.8",
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",