pi-lens 3.2.0 → 3.3.1
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 +20 -0
- package/README.md +4 -10
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/agent-behavior-client.test.js +94 -0
- package/clients/biome-client.test.js +144 -0
- package/clients/cache-manager.test.js +197 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.test.js +116 -0
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
- package/clients/dispatch/runners/lsp.js +42 -5
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/pyright.test.js +98 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/utils/format-utils.js +1 -6
- package/clients/dispatch/utils/format-utils.ts +1 -6
- package/clients/dogfood.test.js +201 -0
- package/clients/file-kinds.test.js +169 -0
- package/clients/formatters.js +1 -1
- package/clients/go-client.test.js +127 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/lsp/__tests__/client.test.js +310 -0
- package/clients/lsp/__tests__/client.test.ts +1 -46
- package/clients/lsp/__tests__/config.test.js +167 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/launch.test.js +313 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/service.test.js +435 -0
- package/clients/lsp/client.js +32 -44
- package/clients/lsp/client.ts +36 -45
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/lsp/server.js +27 -2
- package/clients/metrics-client.test.js +141 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/sanitize.test.js +177 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.test.js +105 -0
- package/commands/rate.test.js +119 -0
- package/index.ts +66 -72
- package/package.json +1 -1
- package/clients/bus/bus.js +0 -191
- package/clients/bus/bus.ts +0 -251
- package/clients/bus/events.js +0 -214
- package/clients/bus/events.ts +0 -279
- package/clients/bus/index.js +0 -8
- package/clients/bus/index.ts +0 -9
- package/clients/bus/integration.js +0 -158
- package/clients/bus/integration.ts +0 -227
- package/clients/dispatch/bus-dispatcher.js +0 -178
- package/clients/dispatch/bus-dispatcher.ts +0 -258
- package/clients/services/__tests__/effect-integration.test.ts +0 -111
- package/clients/services/effect-integration.js +0 -198
- package/clients/services/effect-integration.ts +0 -276
- package/clients/services/index.js +0 -7
- package/clients/services/index.ts +0 -8
- package/clients/services/runner-service.js +0 -134
- package/clients/services/runner-service.ts +0 -225
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatDiagnostic } from "./utils/format-utils.js";
|
|
3
|
+
describe("formatDiagnostic inline output verification", () => {
|
|
4
|
+
it("should display complete architect messages (NOT truncated to 'No ')", () => {
|
|
5
|
+
// Simulate actual architect diagnostic
|
|
6
|
+
const diagnostic = {
|
|
7
|
+
id: "architect-1",
|
|
8
|
+
message: "No absolute Windows paths — breaks CI and cross-platform builds.",
|
|
9
|
+
filePath: "/test.ts",
|
|
10
|
+
line: 5,
|
|
11
|
+
severity: "warning",
|
|
12
|
+
semantic: "warning",
|
|
13
|
+
tool: "architect",
|
|
14
|
+
rule: "no-absolute-windows-paths",
|
|
15
|
+
};
|
|
16
|
+
const output = formatDiagnostic(diagnostic);
|
|
17
|
+
console.log("\n=== Architect Message Output ===");
|
|
18
|
+
console.log(output);
|
|
19
|
+
console.log("=================================\n");
|
|
20
|
+
// Verify complete message is shown
|
|
21
|
+
expect(output).toBe(" L5: No absolute Windows paths — breaks CI and cross-platform builds.");
|
|
22
|
+
});
|
|
23
|
+
it("should display code fix messages inline correctly", () => {
|
|
24
|
+
// This is what I actually see from ts-lsp runner
|
|
25
|
+
const diagnostic = {
|
|
26
|
+
id: "ts-12-2345",
|
|
27
|
+
message: "Property 'debug' is missing in type 'Config'\n💡 Quick fix: Add missing property 'debug'",
|
|
28
|
+
filePath: "/src/config.ts",
|
|
29
|
+
line: 12,
|
|
30
|
+
severity: "error",
|
|
31
|
+
semantic: "blocking",
|
|
32
|
+
tool: "ts-lsp",
|
|
33
|
+
rule: "TS2345",
|
|
34
|
+
fixable: true,
|
|
35
|
+
fixSuggestion: "Add missing property 'debug'",
|
|
36
|
+
};
|
|
37
|
+
const output = formatDiagnostic(diagnostic);
|
|
38
|
+
console.log("\n=== Code Fix Message Output ===");
|
|
39
|
+
console.log(output);
|
|
40
|
+
console.log("================================\n");
|
|
41
|
+
// Both lines should be properly indented
|
|
42
|
+
expect(output).toBe(" L12: Property 'debug' is missing in type 'Config'\n 💡 Quick fix: Add missing property 'debug'");
|
|
43
|
+
});
|
|
44
|
+
it("should prove architect 'No ' messages are complete (not noise)", () => {
|
|
45
|
+
// All the "No " messages from default-architect.yaml
|
|
46
|
+
const testMessages = [
|
|
47
|
+
"No absolute Windows paths — breaks CI and cross-platform builds.",
|
|
48
|
+
"No hardcoded localhost URLs — use environment variables or a config service.",
|
|
49
|
+
"No empty catch/except blocks. Swallowing errors makes debugging impossible — at least log the error.",
|
|
50
|
+
"No hardcoded secrets — use environment variables or a secrets manager.",
|
|
51
|
+
"No 'any' types — use 'unknown' or define a proper interface to maintain type safety.",
|
|
52
|
+
];
|
|
53
|
+
for (let i = 0; i < testMessages.length; i++) {
|
|
54
|
+
const diagnostic = {
|
|
55
|
+
id: `architect-${i}`,
|
|
56
|
+
message: testMessages[i],
|
|
57
|
+
filePath: "/test.ts",
|
|
58
|
+
line: i + 1,
|
|
59
|
+
severity: "warning",
|
|
60
|
+
semantic: "warning",
|
|
61
|
+
tool: "architect",
|
|
62
|
+
rule: "test",
|
|
63
|
+
};
|
|
64
|
+
const output = formatDiagnostic(diagnostic);
|
|
65
|
+
// Each message should be complete, NOT truncated to just "No "
|
|
66
|
+
expect(output.length).toBeGreaterThan(15); // More than " L1: No "
|
|
67
|
+
expect(output).toContain("No ");
|
|
68
|
+
expect(output).toContain("—"); // Contains the em-dash explanation
|
|
69
|
+
// Verify it's not truncated
|
|
70
|
+
const messageAfterNo = output.split("No ")[1];
|
|
71
|
+
expect(messageAfterNo.length).toBeGreaterThan(5);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for declarative dispatch system
|
|
3
|
+
*/
|
|
4
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createDispatchContext, getRunner, getRunnersForKind, listRunners, registerRunner, } from "./dispatcher.js";
|
|
6
|
+
// --- Test Runners ---
|
|
7
|
+
const testRunner1 = {
|
|
8
|
+
id: "test-runner-1",
|
|
9
|
+
appliesTo: ["jsts", "python"],
|
|
10
|
+
priority: 10,
|
|
11
|
+
enabledByDefault: true,
|
|
12
|
+
async run() {
|
|
13
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const testRunner2 = {
|
|
17
|
+
id: "test-runner-2",
|
|
18
|
+
appliesTo: ["python"],
|
|
19
|
+
priority: 20,
|
|
20
|
+
enabledByDefault: true,
|
|
21
|
+
async run() {
|
|
22
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const testRunnerWithCondition = {
|
|
26
|
+
id: "test-runner-conditional",
|
|
27
|
+
appliesTo: ["jsts"],
|
|
28
|
+
priority: 5,
|
|
29
|
+
enabledByDefault: false,
|
|
30
|
+
when: async (ctx) => ctx.autofix,
|
|
31
|
+
async run() {
|
|
32
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
// --- Tests ---
|
|
36
|
+
describe("Runner Registry", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
// Note: In a real test suite, we'd reset the registry between tests
|
|
39
|
+
registerRunner(testRunner1);
|
|
40
|
+
registerRunner(testRunner2);
|
|
41
|
+
registerRunner(testRunnerWithCondition);
|
|
42
|
+
});
|
|
43
|
+
it("should register a runner", () => {
|
|
44
|
+
const runner = getRunner("test-runner-1");
|
|
45
|
+
expect(runner).toBeDefined();
|
|
46
|
+
expect(runner?.id).toBe("test-runner-1");
|
|
47
|
+
});
|
|
48
|
+
it("should return undefined for unknown runner", () => {
|
|
49
|
+
const runner = getRunner("unknown-runner");
|
|
50
|
+
expect(runner).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it("should get runners for a specific kind", () => {
|
|
53
|
+
const jstsRunners = getRunnersForKind("jsts");
|
|
54
|
+
expect(jstsRunners.length).toBeGreaterThan(0);
|
|
55
|
+
expect(jstsRunners.some((r) => r.id === "test-runner-1")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("should return runners sorted by priority", () => {
|
|
58
|
+
const jstsRunners = getRunnersForKind("jsts");
|
|
59
|
+
const priorities = jstsRunners.map((r) => r.priority ?? 100);
|
|
60
|
+
for (let i = 1; i < priorities.length; i++) {
|
|
61
|
+
expect(priorities[i - 1]).toBeLessThanOrEqual(priorities[i]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
it("should list all registered runners", () => {
|
|
65
|
+
const all = listRunners();
|
|
66
|
+
expect(all.length).toBeGreaterThanOrEqual(3);
|
|
67
|
+
});
|
|
68
|
+
it("should reject duplicate registrations", () => {
|
|
69
|
+
// This should log an error but not throw
|
|
70
|
+
expect(() => registerRunner(testRunner1)).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("Dispatch Context", () => {
|
|
74
|
+
it("should create a dispatch context", () => {
|
|
75
|
+
const mockPi = { getFlag: (flag) => flag === "autofix" };
|
|
76
|
+
const ctx = createDispatchContext("test.ts", "/project", mockPi);
|
|
77
|
+
// Path is normalized to absolute path (Windows compatibility)
|
|
78
|
+
expect(ctx.filePath).toContain("test.ts");
|
|
79
|
+
expect(ctx.cwd).toBe("/project");
|
|
80
|
+
expect(ctx.autofix).toBe(false);
|
|
81
|
+
expect(ctx.deltaMode).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it("should detect file kind", () => {
|
|
84
|
+
const mockPi = { getFlag: () => false };
|
|
85
|
+
const ctxTs = createDispatchContext("test.ts", "/project", mockPi);
|
|
86
|
+
expect(ctxTs.kind).toBe("jsts");
|
|
87
|
+
const ctxPy = createDispatchContext("test.py", "/project", mockPi);
|
|
88
|
+
expect(ctxPy.kind).toBe("python");
|
|
89
|
+
const ctxGo = createDispatchContext("test.go", "/project", mockPi);
|
|
90
|
+
expect(ctxGo.kind).toBe("go");
|
|
91
|
+
});
|
|
92
|
+
it("should respect autofix flag", () => {
|
|
93
|
+
const mockPiNoFix = { getFlag: (_f) => false };
|
|
94
|
+
const ctx1 = createDispatchContext("test.ts", "/project", mockPiNoFix);
|
|
95
|
+
expect(ctx1.autofix).toBe(false);
|
|
96
|
+
const mockPiWithFix = { getFlag: (f) => f === "autofix-biome" };
|
|
97
|
+
const ctx2 = createDispatchContext("test.ts", "/project", mockPiWithFix);
|
|
98
|
+
expect(ctx2.autofix).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("Conditional Runners", () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
registerRunner(testRunnerWithCondition);
|
|
104
|
+
});
|
|
105
|
+
it("should respect when condition", async () => {
|
|
106
|
+
const runner = getRunner("test-runner-conditional");
|
|
107
|
+
expect(runner).toBeDefined();
|
|
108
|
+
const mockPiNoFix = { getFlag: () => false };
|
|
109
|
+
const ctxNoFix = createDispatchContext("test.ts", "/project", mockPiNoFix);
|
|
110
|
+
// When autofix is false, the condition should return false
|
|
111
|
+
if (runner?.when) {
|
|
112
|
+
const shouldRun = await runner.when(ctxNoFix);
|
|
113
|
+
expect(shouldRun).toBe(false);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
4
|
+
function createMockContext(filePath, kind = "jsts", cwd) {
|
|
5
|
+
return {
|
|
6
|
+
filePath,
|
|
7
|
+
cwd: cwd || process.cwd(),
|
|
8
|
+
kind,
|
|
9
|
+
autofix: false,
|
|
10
|
+
deltaMode: false,
|
|
11
|
+
baselines: { get: () => undefined, set: () => { }, clear: () => { } },
|
|
12
|
+
pi: { getFlag: () => false },
|
|
13
|
+
hasTool: async () => false,
|
|
14
|
+
log: () => { },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("architect runner", () => {
|
|
18
|
+
const testDir = path.join(process.env.TEMP || "/tmp", `architect_test_${Date.now()}`);
|
|
19
|
+
const configPath = path.join(testDir, ".pi-lens", "architect.yaml");
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
// Create test config
|
|
22
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(configPath, `version: "1.0"
|
|
24
|
+
rules:
|
|
25
|
+
- pattern: "**/*.ts"
|
|
26
|
+
max_lines: 50
|
|
27
|
+
must_not:
|
|
28
|
+
- pattern: 'hardcoded_secret_12345'
|
|
29
|
+
message: "No hardcoded secrets"
|
|
30
|
+
fix: "Use process.env.SECRET"
|
|
31
|
+
- pattern: 'console\.log'
|
|
32
|
+
message: "No console.log in production"
|
|
33
|
+
`);
|
|
34
|
+
});
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(testDir)) {
|
|
38
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore cleanup errors
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it("should load default config when no user config exists", async () => {
|
|
46
|
+
const module = await import("./architect.js");
|
|
47
|
+
const runner = module.default;
|
|
48
|
+
// Use a unique temp dir with no user config (will fall back to default)
|
|
49
|
+
const noUserConfigDir = path.join(process.env.TEMP || "/tmp", `no_arch_user_config_${Date.now()}`);
|
|
50
|
+
fs.mkdirSync(noUserConfigDir, { recursive: true });
|
|
51
|
+
// Create a very large file that should trigger default max_lines rule
|
|
52
|
+
const tmpFile = path.join(noUserConfigDir, `large_${Date.now()}.ts`);
|
|
53
|
+
fs.writeFileSync(tmpFile, Array(5000).fill("// line").join("\n"));
|
|
54
|
+
try {
|
|
55
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", noUserConfigDir));
|
|
56
|
+
// Should use default config and find violations
|
|
57
|
+
expect(result.status).toBe("succeeded");
|
|
58
|
+
// Should have size violation from default config
|
|
59
|
+
expect(result.diagnostics.some((d) => d.message.includes("line limit"))).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(tmpFile))
|
|
64
|
+
fs.unlinkSync(tmpFile);
|
|
65
|
+
if (fs.existsSync(noUserConfigDir))
|
|
66
|
+
fs.rmdirSync(noUserConfigDir);
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
it("should detect file size violations", async () => {
|
|
72
|
+
const module = await import("./architect.js");
|
|
73
|
+
const runner = module.default;
|
|
74
|
+
const tmpFile = path.join(testDir, `large_file_${Date.now()}.ts`);
|
|
75
|
+
// Create file with 100 lines (exceeds 50 line limit)
|
|
76
|
+
fs.writeFileSync(tmpFile, Array(100).fill("// line").join("\n"));
|
|
77
|
+
try {
|
|
78
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
79
|
+
expect(result.status).toBe("succeeded");
|
|
80
|
+
expect(result.diagnostics.length).toBeGreaterThan(0);
|
|
81
|
+
expect(result.diagnostics.some((d) => d.message.includes("50 line limit"))).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(tmpFile))
|
|
86
|
+
fs.unlinkSync(tmpFile);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it("should detect pattern violations", async () => {
|
|
92
|
+
const module = await import("./architect.js");
|
|
93
|
+
const runner = module.default;
|
|
94
|
+
const tmpFile = path.join(testDir, `bad_patterns_${Date.now()}.ts`);
|
|
95
|
+
fs.writeFileSync(tmpFile, `const x = hardcoded_secret_12345;
|
|
96
|
+
console.log(x);
|
|
97
|
+
`);
|
|
98
|
+
try {
|
|
99
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
100
|
+
expect(result.status).toBe("succeeded");
|
|
101
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
102
|
+
expect(result.diagnostics.some((d) => d.message.includes("hardcoded"))).toBe(true);
|
|
103
|
+
expect(result.diagnostics.some((d) => d.message.includes("console.log"))).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(tmpFile))
|
|
108
|
+
fs.unlinkSync(tmpFile);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it("should return no diagnostics for clean files", async () => {
|
|
114
|
+
const module = await import("./architect.js");
|
|
115
|
+
const runner = module.default;
|
|
116
|
+
const tmpFile = path.join(testDir, `clean_${Date.now()}.ts`);
|
|
117
|
+
// Small file (20 lines) with no violations
|
|
118
|
+
fs.writeFileSync(tmpFile, Array(20).fill("// clean code").join("\n"));
|
|
119
|
+
try {
|
|
120
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
121
|
+
expect(result.status).toBe("succeeded");
|
|
122
|
+
expect(result.diagnostics.length).toBe(0);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
try {
|
|
126
|
+
if (fs.existsSync(tmpFile))
|
|
127
|
+
fs.unlinkSync(tmpFile);
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it("should skip test files", async () => {
|
|
133
|
+
const module = await import("./architect.js");
|
|
134
|
+
const runner = module.default;
|
|
135
|
+
// The runner should have skipTestFiles: true
|
|
136
|
+
expect(runner.skipTestFiles).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
function createMockContext(filePath, kind = "jsts") {
|
|
5
|
+
return {
|
|
6
|
+
filePath,
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
kind,
|
|
9
|
+
autofix: false,
|
|
10
|
+
deltaMode: false,
|
|
11
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
12
|
+
pi: {},
|
|
13
|
+
hasTool: async () => false,
|
|
14
|
+
log: () => { },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("ast-grep-napi vs CLI comparison", () => {
|
|
18
|
+
it("should load the napi module", async () => {
|
|
19
|
+
const napiModule = await import("./ast-grep-napi.js");
|
|
20
|
+
expect(napiModule.default.id).toBe("ast-grep-napi");
|
|
21
|
+
expect(napiModule.default.appliesTo).toEqual(["jsts"]);
|
|
22
|
+
});
|
|
23
|
+
it("should scan TypeScript file and return succeeded status", async () => {
|
|
24
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `napi_test_${Date.now()}.ts`);
|
|
25
|
+
fs.writeFileSync(tmpFile, `// Test file with various patterns
|
|
26
|
+
function test(items: string[]) {
|
|
27
|
+
for (let i = 0; i < items.length; i++) {
|
|
28
|
+
console.log(items[i]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
riskyOperation();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// empty catch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return await fetchData();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchData() {
|
|
41
|
+
return await Promise.resolve(42);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function riskyOperation() {
|
|
45
|
+
debugger;
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
try {
|
|
49
|
+
// Test NAPI version
|
|
50
|
+
const napiModule = await import("./ast-grep-napi.js");
|
|
51
|
+
const napiRunner = napiModule.default;
|
|
52
|
+
console.time("napi");
|
|
53
|
+
let napiResult;
|
|
54
|
+
try {
|
|
55
|
+
napiResult = await napiRunner.run(createMockContext(tmpFile));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error("NAPI runner threw error:", error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
console.timeEnd("napi");
|
|
62
|
+
console.log("NAPI result status:", napiResult.status);
|
|
63
|
+
console.log("NAPI result semantic:", napiResult.semantic);
|
|
64
|
+
console.log("NAPI result diagnostics count:", napiResult.diagnostics?.length);
|
|
65
|
+
// Should complete successfully (not skipped, not failed)
|
|
66
|
+
expect(napiResult.status).toBe("succeeded");
|
|
67
|
+
expect(napiResult.semantic).toBe("warning"); // Has findings, so marked as warning
|
|
68
|
+
// Log findings
|
|
69
|
+
console.log("NAPI found:", napiResult.diagnostics.length, "issues");
|
|
70
|
+
console.log("\n=== NAPI FINDINGS ===");
|
|
71
|
+
napiResult.diagnostics.forEach((d, i) => {
|
|
72
|
+
console.log(`${i + 1}. Line ${d.line}: ${d.rule}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(tmpFile)) {
|
|
78
|
+
fs.unlinkSync(tmpFile);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Ignore cleanup errors
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("should skip non-TS/JS files", async () => {
|
|
87
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `napi_test_py_${Date.now()}.py`);
|
|
88
|
+
fs.writeFileSync(tmpFile, "# Python file\nprint('hello')");
|
|
89
|
+
try {
|
|
90
|
+
const napiModule = await import("./ast-grep-napi.js");
|
|
91
|
+
const napiRunner = napiModule.default;
|
|
92
|
+
const result = await napiRunner.run(createMockContext(tmpFile, "python"));
|
|
93
|
+
expect(result.status).toBe("skipped");
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(tmpFile)) {
|
|
98
|
+
fs.unlinkSync(tmpFile);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Ignore cleanup errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -34,11 +34,48 @@ const lspRunner = {
|
|
|
34
34
|
if (!content) {
|
|
35
35
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
36
36
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Try to open file in LSP and get diagnostics
|
|
38
|
+
// If the server fails to spawn or crashes, this will be caught
|
|
39
|
+
let lspDiags = [];
|
|
40
|
+
let serverFailed = false;
|
|
41
|
+
let failureReason = "";
|
|
42
|
+
try {
|
|
43
|
+
await lspService.openFile(ctx.filePath, content);
|
|
44
|
+
// getDiagnostics() internally calls waitForDiagnostics() with bus
|
|
45
|
+
// subscription + 150ms debounce + 3s timeout
|
|
46
|
+
lspDiags = await lspService.getDiagnostics(ctx.filePath);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
serverFailed = true;
|
|
50
|
+
failureReason = err instanceof Error ? err.message : String(err);
|
|
51
|
+
// Check if this is a server spawn/connection error
|
|
52
|
+
if (failureReason.includes("spawn") ||
|
|
53
|
+
failureReason.includes("exited") ||
|
|
54
|
+
failureReason.includes("connection") ||
|
|
55
|
+
failureReason.includes("JSON RPC")) {
|
|
56
|
+
// Mark this server as broken so we don't keep trying
|
|
57
|
+
console.error(`[lsp-runner] LSP server failed for ${ctx.filePath}: ${failureReason}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// If server failed to provide diagnostics, report as failed status
|
|
61
|
+
if (serverFailed) {
|
|
62
|
+
return {
|
|
63
|
+
status: "failed",
|
|
64
|
+
diagnostics: [
|
|
65
|
+
{
|
|
66
|
+
id: `lsp:server-error:0`,
|
|
67
|
+
message: `LSP server failed: ${failureReason}`,
|
|
68
|
+
filePath: ctx.filePath,
|
|
69
|
+
line: 1,
|
|
70
|
+
column: 1,
|
|
71
|
+
severity: "error",
|
|
72
|
+
semantic: "warning", // Don't block - fallback to other runners
|
|
73
|
+
tool: "lsp",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
semantic: "warning",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
42
79
|
// Convert LSP diagnostics to our format
|
|
43
80
|
// Defensive: filter out malformed diagnostics that may lack range
|
|
44
81
|
const diagnostics = lspDiags
|