pi-lens 3.1.2 → 3.2.0

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 (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -1,157 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { createTempFile, setupTestEnvironment } from "./test-utils.js";
3
- import { TypeScriptClient } from "./typescript-client.js";
4
- describe("TypeScriptClient - Code Fixes", () => {
5
- let client;
6
- let tmpDir;
7
- let cleanup;
8
- beforeEach(() => {
9
- client = new TypeScriptClient();
10
- ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-codefix-test-"));
11
- });
12
- afterEach(() => {
13
- cleanup();
14
- });
15
- describe("getCodeFixes", () => {
16
- it("should provide fix for missing property on object literal", () => {
17
- // Real-world case: Missing required property in object literal
18
- const content = `
19
- interface Config {
20
- name: string;
21
- port: number;
22
- debug: boolean;
23
- }
24
-
25
- const config: Config = {
26
- name: "my-app",
27
- port: 3000
28
- // Missing 'debug' property - TS2345
29
- };
30
- `;
31
- const filePath = createTempFile(tmpDir, "missing-property.ts", content);
32
- client.addFile(filePath, content);
33
- const diags = client.getDiagnostics(filePath);
34
- const missingPropError = diags.find((d) => d.code === 2345 || d.message.includes("missing"));
35
- if (missingPropError) {
36
- const line = missingPropError.range.start.line;
37
- const char = missingPropError.range.start.character;
38
- const fixes = client.getCodeFixes(filePath, line, char, [
39
- missingPropError.code,
40
- ]);
41
- // TypeScript should suggest adding the missing property
42
- expect(fixes.length).toBeGreaterThan(0);
43
- const hasAddPropertyFix = fixes.some((f) => f.description.toLowerCase().includes("add") ||
44
- f.description.toLowerCase().includes("property") ||
45
- f.description.toLowerCase().includes("declare"));
46
- expect(hasAddPropertyFix).toBe(true);
47
- }
48
- });
49
- it("should provide fix for missing await in async function", () => {
50
- // Real-world case: Forgetting await on a Promise-returning function
51
- const content = `
52
- async function fetchUser(id: string): Promise<{ name: string }> {
53
- return { name: "John" };
54
- }
55
-
56
- async function getUserName(id: string): Promise<string> {
57
- const user = fetchUser(id); // Missing await
58
- return user.name; // Type 'Promise<{ name: string; }>' has no property 'name'
59
- }
60
- `;
61
- const filePath = createTempFile(tmpDir, "missing-await.ts", content);
62
- client.addFile(filePath, content);
63
- const diags = client.getDiagnostics(filePath);
64
- // TS2739: Type 'Promise<{ name: string; }>' is missing 'name'
65
- const propertyError = diags.find((d) => d.code === 2739 || d.message.includes("is missing"));
66
- if (propertyError) {
67
- const fixes = client.getAllCodeFixes(filePath);
68
- // If there's an error, check if we have fixes for it
69
- const lineFixes = fixes.get(propertyError.range.start.line);
70
- if (lineFixes) {
71
- expect(lineFixes.length).toBeGreaterThan(0);
72
- }
73
- }
74
- // Test passes if we get here - not all TS versions provide fixes for this
75
- expect(true).toBe(true);
76
- });
77
- it("should provide fix for incorrect type assignment", () => {
78
- // Real-world case: String instead of number
79
- const content = `
80
- function calculateTotal(price: number, tax: number): number {
81
- return price + tax;
82
- }
83
-
84
- const result = calculateTotal("100", 10); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'
85
- `;
86
- const filePath = createTempFile(tmpDir, "type-mismatch.ts", content);
87
- client.addFile(filePath, content);
88
- const diags = client.getDiagnostics(filePath);
89
- const typeError = diags.find((d) => d.code === 2345);
90
- if (typeError) {
91
- const line = typeError.range.start.line;
92
- const char = typeError.range.start.character;
93
- const fixes = client.getCodeFixes(filePath, line, char, [2345]);
94
- // TypeScript often suggests fixes for type mismatches
95
- expect(fixes).toBeDefined();
96
- }
97
- });
98
- it("should collect all fixes via getAllCodeFixes", () => {
99
- // Multiple errors in one file
100
- const content = `
101
- interface Person {
102
- name: string;
103
- age: number;
104
- }
105
-
106
- const person: Person = {
107
- name: "Alice"
108
- // Missing age
109
- };
110
-
111
- function greet(p: Person): string {
112
- return "Hello " + p.name;
113
- }
114
-
115
- greet({ name: "Bob" }); // Missing age in argument
116
- `;
117
- const filePath = createTempFile(tmpDir, "multiple-errors.ts", content);
118
- client.addFile(filePath, content);
119
- const allFixes = client.getAllCodeFixes(filePath);
120
- // Should have fixes mapped by line number
121
- expect(allFixes).toBeInstanceOf(Map);
122
- // Each fix entry should have a description and changes
123
- for (const [line, fixes] of allFixes.entries()) {
124
- expect(typeof line).toBe("number");
125
- expect(fixes.length).toBeGreaterThan(0);
126
- for (const fix of fixes) {
127
- expect(fix.description).toBeTruthy();
128
- expect(fix.changes).toBeDefined();
129
- }
130
- }
131
- });
132
- });
133
- describe("Integration with diagnostic messages", () => {
134
- it("should include fix suggestions in getAllCodeFixes output", () => {
135
- const content = `
136
- class User {
137
- constructor(public name: string) {}
138
- }
139
-
140
- const user = new User(); // TS2554: Expected 1 arguments, but got 0
141
- `;
142
- const filePath = createTempFile(tmpDir, "constructor-args.ts", content);
143
- client.addFile(filePath, content);
144
- const diags = client.getDiagnostics(filePath);
145
- const argError = diags.find((d) => d.code === 2554);
146
- if (argError) {
147
- const fixes = client.getAllCodeFixes(filePath);
148
- const lineFixes = fixes.get(argError.range.start.line);
149
- if (lineFixes && lineFixes.length > 0) {
150
- // The runner would append this to the message
151
- const suggestion = lineFixes[0].description;
152
- expect(suggestion).toBeTruthy();
153
- }
154
- }
155
- });
156
- });
157
- });
@@ -1,105 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { createTempFile, setupTestEnvironment } from "./test-utils.js";
3
- import { TypeScriptClient } from "./typescript-client.js";
4
- describe("TypeScriptClient", () => {
5
- let client;
6
- let tmpDir;
7
- let cleanup;
8
- beforeEach(() => {
9
- client = new TypeScriptClient();
10
- ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-ts-test-"));
11
- });
12
- afterEach(() => {
13
- cleanup();
14
- });
15
- afterEach(() => {
16
- cleanup();
17
- });
18
- describe("isTypeScriptFile", () => {
19
- it("should recognize TypeScript files", () => {
20
- expect(client.isTypeScriptFile("test.ts")).toBe(true);
21
- expect(client.isTypeScriptFile("test.tsx")).toBe(true);
22
- });
23
- it("should also recognize JavaScript files (for type checking)", () => {
24
- expect(client.isTypeScriptFile("test.js")).toBe(true);
25
- expect(client.isTypeScriptFile("test.jsx")).toBe(true);
26
- });
27
- it("should not recognize non-JS/TS files", () => {
28
- expect(client.isTypeScriptFile("test.py")).toBe(false);
29
- expect(client.isTypeScriptFile("test.md")).toBe(false);
30
- });
31
- });
32
- describe("updateFile and getDiagnostics", () => {
33
- it("should detect type errors", () => {
34
- const content = `
35
- const x: number = "string"; // Type error
36
- `;
37
- const filePath = createTempFile(tmpDir, "test.ts", content);
38
- client.updateFile(filePath, content);
39
- const diags = client.getDiagnostics(filePath);
40
- expect(diags.length).toBeGreaterThan(0);
41
- expect(diags.some((d) => d.message.includes("string"))).toBe(true);
42
- });
43
- it("should not report errors for valid code", () => {
44
- const content = `
45
- const x: number = 42;
46
- const y: string = "hello";
47
- `;
48
- const filePath = createTempFile(tmpDir, "test.ts", content);
49
- client.updateFile(filePath, content);
50
- const diags = client.getDiagnostics(filePath);
51
- // May have warnings but no errors for valid code
52
- const errors = diags.filter((d) => d.severity === 1); // Error severity
53
- expect(errors.length).toBe(0);
54
- });
55
- it("should detect undefined variables", () => {
56
- const content = `
57
- function test() {
58
- return undefinedVariable;
59
- }
60
- `;
61
- const filePath = createTempFile(tmpDir, "test.ts", content);
62
- client.updateFile(filePath, content);
63
- const diags = client.getDiagnostics(filePath);
64
- expect(diags.some((d) => d.message.includes("undefined"))).toBe(true);
65
- });
66
- it("should detect missing function arguments", () => {
67
- const content = `
68
- function add(a: number, b: number): number {
69
- return a + b;
70
- }
71
- const result = add(1);
72
- `;
73
- const filePath = createTempFile(tmpDir, "test.ts", content);
74
- client.updateFile(filePath, content);
75
- const diags = client.getDiagnostics(filePath);
76
- expect(diags.some((d) => d.message.includes("Expected"))).toBe(true);
77
- });
78
- });
79
- describe("diagnostic severity", () => {
80
- it("should have correct severity levels", () => {
81
- const diags = [
82
- {
83
- range: {
84
- start: { line: 0, character: 0 },
85
- end: { line: 0, character: 10 },
86
- },
87
- severity: 1, // Error
88
- message: "Error message",
89
- },
90
- {
91
- range: {
92
- start: { line: 1, character: 0 },
93
- end: { line: 1, character: 10 },
94
- },
95
- severity: 2, // Warning
96
- message: "Warning message",
97
- },
98
- ];
99
- // Test that diagnostics have expected structure
100
- expect(diags[0].severity).toBe(1); // Error
101
- expect(diags[1].severity).toBe(2); // Warning
102
- expect(diags[0].message).toContain("Error");
103
- });
104
- });
105
- });
@@ -1,250 +0,0 @@
1
- /**
2
- * AstGrep Client for pi-lens
3
- *
4
- * Structural code analysis using ast-grep CLI.
5
- * Scans files against YAML rule definitions.
6
- *
7
- * Requires: npm install -D @ast-grep/cli
8
- * Rules: ./rules/ directory
9
- */
10
- import { spawnSync } from "node:child_process";
11
- import * as fs from "node:fs";
12
- import * as path from "node:path";
13
- import { AstGrepParser } from "./ast-grep-parser.js";
14
- import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
- import { SgRunner } from "./sg-runner.js";
16
- const _getExtensionDir = () => {
17
- if (typeof __dirname !== "undefined") {
18
- return __dirname;
19
- }
20
- return ".";
21
- };
22
- // --- Client ---
23
- export class AstGrepClient {
24
- available = null;
25
- ruleDir;
26
- log;
27
- ruleManager;
28
- runner;
29
- constructor(ruleDir, verbose = false) {
30
- this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
31
- this.log = verbose
32
- ? (msg) => console.error(`[ast-grep] ${msg}`)
33
- : () => { };
34
- this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
35
- this.runner = new SgRunner(verbose);
36
- }
37
- /**
38
- * Check if ast-grep CLI is available
39
- */
40
- isAvailable() {
41
- if (this.available !== null)
42
- return this.available;
43
- this.available = this.runner.isAvailable();
44
- if (this.available) {
45
- this.log("ast-grep available");
46
- }
47
- return this.available;
48
- }
49
- /**
50
- * Search for AST patterns in files
51
- */
52
- async search(pattern, lang, paths, options) {
53
- const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
54
- if (options?.selector) {
55
- args.push("--selector", options.selector);
56
- }
57
- if (options?.context !== undefined) {
58
- args.push("--context", String(options.context));
59
- }
60
- args.push(...paths);
61
- return this.runner.exec(args);
62
- }
63
- /**
64
- * Search and replace AST patterns
65
- */
66
- async replace(pattern, rewrite, lang, paths, apply = false) {
67
- const args = [
68
- "run",
69
- "-p",
70
- pattern,
71
- "-r",
72
- rewrite,
73
- "--lang",
74
- lang,
75
- "--json=compact",
76
- ];
77
- if (apply)
78
- args.push("--update-all");
79
- args.push(...paths);
80
- const result = await this.runner.exec(args);
81
- return { matches: result.matches, applied: apply, error: result.error };
82
- }
83
- /**
84
- * Run a one-off scan with a temporary rule and configuration
85
- */
86
- runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
87
- if (!this.isAvailable())
88
- return [];
89
- return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
90
- }
91
- /**
92
- * Find similar functions by comparing normalized AST structure
93
- */
94
- async findSimilarFunctions(dir, lang = "typescript") {
95
- const ruleYaml = `id: find-functions
96
- language: ${lang}
97
- rule:
98
- kind: function_declaration
99
- severity: info
100
- message: found
101
- `;
102
- const matches = this.runTempScan(dir, "find-functions", ruleYaml);
103
- if (matches.length === 0)
104
- return [];
105
- return this.groupSimilarFunctions(matches);
106
- }
107
- groupSimilarFunctions(matches) {
108
- const grouped = new Map();
109
- for (const item of matches) {
110
- const name = this.extractFunctionName(item.text);
111
- if (!name)
112
- continue;
113
- const signature = this.normalizeFunction(item.text);
114
- const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
115
- 1;
116
- const group = grouped.get(signature) ?? [];
117
- group.push({ name, file: item.file, line });
118
- grouped.set(signature, group);
119
- }
120
- return Array.from(grouped.entries())
121
- .filter(([_, functions]) => functions.length > 1)
122
- .map(([pattern, functions]) => ({ pattern, functions }));
123
- }
124
- /**
125
- * Extract function name from match text
126
- */
127
- extractFunctionName(text) {
128
- return text.match(/function\s+(\w+)/)?.[1] ?? null;
129
- }
130
- normalizeFunction(text) {
131
- const normalizedText = text
132
- .replace(/function\s+\w+/, "function FN")
133
- .replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
134
- .replace(/["'].*?["']/g, "STR")
135
- .replace(/`[^`]*`/g, "TMPL")
136
- .replace(/\b\d+\b/g, "NUM")
137
- .replace(/\btrue\b|\bfalse\b/g, "BOOL")
138
- .replace(/\/\/.*/g, "")
139
- .replace(/\/\*[\s\S]*?\*\//g, "")
140
- .replace(/\s+/g, " ")
141
- .trim();
142
- // Extract just the body structure
143
- const bodyMatch = normalizedText.match(/\{(.*)\}/);
144
- const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
145
- // Use first 200 chars as signature
146
- return body.slice(0, 200);
147
- }
148
- /**
149
- * Scan for exported function names in a directory
150
- */
151
- async scanExports(dir, lang = "typescript") {
152
- const exports = new Map();
153
- const ruleYaml = `id: find-functions
154
- language: ${lang}
155
- rule:
156
- kind: function_declaration
157
- severity: info
158
- message: found
159
- `;
160
- const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
161
- this.log(`scanExports output length: ${matches.length}`);
162
- for (const item of matches) {
163
- const text = item.text || "";
164
- const nameMatch = text.match(/function\s+(\w+)/);
165
- if (nameMatch?.[1]) {
166
- this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
167
- exports.set(nameMatch[1], item.file);
168
- }
169
- }
170
- return exports;
171
- }
172
- formatMatches(matches, isDryRun = false, showModeIndicator = false) {
173
- return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
174
- }
175
- /**
176
- * Scan a file against all rules
177
- */
178
- scanFile(filePath) {
179
- if (!this.isAvailable())
180
- return [];
181
- const absolutePath = path.resolve(filePath);
182
- if (!fs.existsSync(absolutePath))
183
- return [];
184
- const configPath = path.join(this.ruleDir, ".sgconfig.yml");
185
- try {
186
- const result = spawnSync("npx", ["sg", "scan", "--config", configPath, "--json", absolutePath], {
187
- encoding: "utf-8",
188
- timeout: 15000,
189
- shell: process.platform === "win32",
190
- });
191
- // ast-grep exits 1 when it finds issues
192
- const output = result.stdout || result.stderr || "";
193
- if (!output.trim())
194
- return [];
195
- const parser = new AstGrepParser((id) => this.getRuleDescription(id), (sev) => this.mapSeverity(sev));
196
- return parser.parseOutput(output, absolutePath);
197
- }
198
- catch (err) {
199
- this.log(`Scan error: ${err instanceof Error ? err.message : String(err)}`);
200
- return [];
201
- }
202
- }
203
- /**
204
- * Format diagnostics for LLM consumption
205
- */
206
- formatDiagnostics(diags) {
207
- if (diags.length === 0)
208
- return "";
209
- const errors = diags.filter((d) => d.severity === "error");
210
- const warnings = diags.filter((d) => d.severity === "warning");
211
- const hints = diags.filter((d) => d.severity === "hint");
212
- let output = `[ast-grep] ${diags.length} structural issue(s)`;
213
- if (errors.length)
214
- output += ` — ${errors.length} error(s)`;
215
- if (warnings.length)
216
- output += ` — ${warnings.length} warning(s)`;
217
- if (hints.length)
218
- output += ` — ${hints.length} hint(s)`;
219
- output += ":\n";
220
- for (const d of diags.slice(0, 10)) {
221
- const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
222
- const ruleInfo = d.ruleDescription
223
- ? `${d.rule}: ${d.ruleDescription.message}`
224
- : d.rule;
225
- const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
226
- output += ` ${ruleInfo} (${loc})${fix}\n`;
227
- if (d.ruleDescription?.note) {
228
- const shortNote = d.ruleDescription.note.split("\n")[0];
229
- output += ` → ${shortNote}\n`;
230
- }
231
- }
232
- if (diags.length > 10) {
233
- output += ` ... and ${diags.length - 10} more\n`;
234
- }
235
- return output;
236
- }
237
- getRuleDescription(ruleId) {
238
- return this.ruleManager.loadRuleDescriptions().get(ruleId);
239
- }
240
- mapSeverity(severity) {
241
- const lower = severity.toLowerCase();
242
- if (lower === "error")
243
- return "error";
244
- if (lower === "warning")
245
- return "warning";
246
- if (lower === "info")
247
- return "info";
248
- return "hint";
249
- }
250
- }
@@ -1,86 +0,0 @@
1
- import * as path from "node:path";
2
- export class AstGrepParser {
3
- getRuleDescription;
4
- mapSeverity;
5
- constructor(getRuleDescription, mapSeverity) {
6
- this.getRuleDescription = getRuleDescription;
7
- this.mapSeverity = mapSeverity;
8
- }
9
- parseOutput(output, filterFile) {
10
- const resolvedFilterFile = path.resolve(filterFile);
11
- try {
12
- const items = JSON.parse(output);
13
- if (Array.isArray(items)) {
14
- return items
15
- .map((item) => this.parseDiagnostic(item, resolvedFilterFile))
16
- .filter((d) => d !== null);
17
- }
18
- }
19
- catch (err) {
20
- void err;
21
- }
22
- return output
23
- .split("\n")
24
- .filter((l) => l.trim())
25
- .map((line) => {
26
- try {
27
- return this.parseDiagnostic(JSON.parse(line), resolvedFilterFile);
28
- }
29
- catch (err) {
30
- void err;
31
- return null;
32
- }
33
- })
34
- .filter((d) => d !== null);
35
- }
36
- parseDiagnostic(item, filterFile) {
37
- if (item.labels?.length) {
38
- return this.parseNewFormat(item, filterFile);
39
- }
40
- if (item.spans?.length) {
41
- return this.parseLegacyFormat(item, filterFile);
42
- }
43
- return null;
44
- }
45
- parseNewFormat(item, filterFile) {
46
- const label = item.labels.find((l) => l.style === "primary") || item.labels[0];
47
- const filePath = path.resolve(label.file || filterFile);
48
- if (filePath !== filterFile)
49
- return null;
50
- const start = label.range?.start || { line: 0, column: 0 };
51
- const end = label.range?.end || start;
52
- return {
53
- line: start.line + 1,
54
- column: start.column,
55
- endLine: end.line + 1,
56
- endColumn: end.column,
57
- severity: this.mapSeverity(item.severity),
58
- message: item.message || "Unknown issue",
59
- rule: item.ruleId || "unknown",
60
- ruleDescription: this.getRuleDescription(item.ruleId || "unknown"),
61
- file: filePath,
62
- };
63
- }
64
- parseLegacyFormat(item, filterFile) {
65
- const span = item.spans?.[0];
66
- if (!span)
67
- return null;
68
- const filePath = path.resolve(span.file || filterFile);
69
- if (filePath !== filterFile)
70
- return null;
71
- const start = span.range?.start || { line: 0, column: 0 };
72
- const end = span.range?.end || start;
73
- const ruleId = item.name || item.ruleId || "unknown";
74
- return {
75
- line: start.line + 1,
76
- column: start.column,
77
- endLine: end.line + 1,
78
- endColumn: end.column,
79
- severity: this.mapSeverity(item.severity || item.Severity || "warning"),
80
- message: item.Message?.text || item.message || "Unknown issue",
81
- rule: ruleId,
82
- ruleDescription: this.getRuleDescription(ruleId),
83
- file: filePath,
84
- };
85
- }
86
- }
@@ -1,91 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- export class AstGrepRuleManager {
4
- ruleDir;
5
- log;
6
- ruleDescriptions = null;
7
- constructor(ruleDir, log) {
8
- this.ruleDir = ruleDir;
9
- this.log = log;
10
- }
11
- loadRuleDescriptions() {
12
- if (this.ruleDescriptions !== null)
13
- return this.ruleDescriptions;
14
- const descriptions = new Map();
15
- const possiblePaths = [
16
- path.join(this.ruleDir, "ast-grep-rules", "rules"),
17
- path.join(this.ruleDir, "rules"),
18
- this.ruleDir,
19
- ];
20
- const rulesPath = possiblePaths.find((p) => fs.existsSync(p));
21
- if (!rulesPath) {
22
- this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
23
- this.ruleDescriptions = descriptions;
24
- return descriptions;
25
- }
26
- try {
27
- const files = fs.readdirSync(rulesPath).filter((f) => f.endsWith(".yml"));
28
- this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
29
- for (const file of files) {
30
- const filePath = path.join(rulesPath, file);
31
- const content = fs.readFileSync(filePath, "utf-8");
32
- const rule = this.parseRuleYaml(content);
33
- if (rule) {
34
- descriptions.set(rule.id, rule);
35
- }
36
- }
37
- }
38
- catch (err) {
39
- this.log(`Failed to load rule descriptions: ${err.message}`);
40
- }
41
- this.ruleDescriptions = descriptions;
42
- return descriptions;
43
- }
44
- parseRuleYaml(content) {
45
- const result = {};
46
- const idMatch = content.match(/^id:\s*(.+)$/m);
47
- if (idMatch)
48
- result.id = idMatch[1].trim();
49
- const msgMatch = content.match(/^message:\s*"([^"]+)"/m) ||
50
- content.match(/^message:\s*'([^']+)'/m) ||
51
- content.match(/^message:\s*(.+)$/m);
52
- if (msgMatch)
53
- result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
54
- const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
55
- if (noteMatch) {
56
- result.note = noteMatch[1]
57
- .split("\n")
58
- .map((line) => line.trim())
59
- .filter((line) => line.length > 0)
60
- .join(" ");
61
- }
62
- const sevMatch = content.match(/^severity:\s*(.+)$/m);
63
- if (sevMatch)
64
- result.severity = this.mapSeverity(sevMatch[1].trim());
65
- const gradeMatch = content.match(/Grade\s+(\d+\.\d+)/i);
66
- if (gradeMatch)
67
- result.grade = parseFloat(gradeMatch[1]);
68
- const fixMatch = content.match(/^fix:\s*\|?([\s\S]*?)(?=^\w|^rule:|Z)/m);
69
- if (fixMatch) {
70
- result.fix = fixMatch[1]
71
- .split("\n")
72
- .map((line) => line.replace(/^\s*\|?\s*/, ""))
73
- .filter((line) => line.length > 0)
74
- .join("\n");
75
- }
76
- if (result.id && result.message) {
77
- return result;
78
- }
79
- return null;
80
- }
81
- mapSeverity(severity) {
82
- const lower = severity.toLowerCase();
83
- if (lower === "error")
84
- return "error";
85
- if (lower === "warning")
86
- return "warning";
87
- if (lower === "info")
88
- return "info";
89
- return "hint";
90
- }
91
- }