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.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- 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
|
-
}
|