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,115 +0,0 @@
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
- expect(ctx.filePath).toBe("test.ts");
78
- expect(ctx.cwd).toBe("/project");
79
- expect(ctx.autofix).toBe(false);
80
- expect(ctx.deltaMode).toBe(true);
81
- });
82
- it("should detect file kind", () => {
83
- const mockPi = { getFlag: () => false };
84
- const ctxTs = createDispatchContext("test.ts", "/project", mockPi);
85
- expect(ctxTs.kind).toBe("jsts");
86
- const ctxPy = createDispatchContext("test.py", "/project", mockPi);
87
- expect(ctxPy.kind).toBe("python");
88
- const ctxGo = createDispatchContext("test.go", "/project", mockPi);
89
- expect(ctxGo.kind).toBe("go");
90
- });
91
- it("should respect autofix flag", () => {
92
- const mockPiNoFix = { getFlag: (_f) => false };
93
- const ctx1 = createDispatchContext("test.ts", "/project", mockPiNoFix);
94
- expect(ctx1.autofix).toBe(false);
95
- const mockPiWithFix = { getFlag: (f) => f === "autofix-biome" };
96
- const ctx2 = createDispatchContext("test.ts", "/project", mockPiWithFix);
97
- expect(ctx2.autofix).toBe(true);
98
- });
99
- });
100
- describe("Conditional Runners", () => {
101
- beforeEach(() => {
102
- registerRunner(testRunnerWithCondition);
103
- });
104
- it("should respect when condition", async () => {
105
- const runner = getRunner("test-runner-conditional");
106
- expect(runner).toBeDefined();
107
- const mockPiNoFix = { getFlag: () => false };
108
- const ctxNoFix = createDispatchContext("test.ts", "/project", mockPiNoFix);
109
- // When autofix is false, the condition should return false
110
- if (runner?.when) {
111
- const shouldRun = await runner.when(ctxNoFix);
112
- expect(shouldRun).toBe(false);
113
- }
114
- });
115
- });
@@ -1,138 +0,0 @@
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
- });
@@ -1,106 +0,0 @@
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
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * Tests for oxlint runner
3
- */
4
- import * as fs from "node:fs";
5
- import { createRequire } from "node:module";
6
- import * as path from "node:path";
7
- import { describe, expect, it } from "vitest";
8
- /**
9
- * Delay helper for Windows file cleanup
10
- * Windows may hold file handles briefly after process exit
11
- */
12
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
- function createMockContext(filePath, overrides = {}) {
14
- return {
15
- filePath,
16
- cwd: process.cwd(),
17
- kind: "jsts",
18
- autofix: false,
19
- deltaMode: false,
20
- baselines: { get: () => [], add: () => { }, save: () => { } },
21
- pi: { getFlag: () => false, ...overrides.pi },
22
- hasTool: async () => false,
23
- log: () => { },
24
- ...overrides,
25
- };
26
- }
27
- describe("oxlint runner", () => {
28
- const require = createRequire(import.meta.url);
29
- it("should have correct runner definition", async () => {
30
- const oxlintModule = await import("./oxlint.js");
31
- const runner = oxlintModule.default;
32
- expect(runner.id).toBe("oxlint");
33
- expect(runner.appliesTo).toEqual(["jsts"]);
34
- expect(runner.priority).toBe(12);
35
- expect(runner.enabledByDefault).toBe(false); // Opt-in initially
36
- expect(runner.skipTestFiles).toBe(true);
37
- });
38
- it("should detect oxlint availability", () => {
39
- const { spawnSync } = require("node:child_process");
40
- const result = spawnSync("oxlint", ["--version"], {
41
- encoding: "utf-8",
42
- timeout: 10000,
43
- shell: true,
44
- });
45
- expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy(); // May or may not be installed
46
- });
47
- it("should detect common lint issues", async () => {
48
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_test_${Date.now()}.ts`);
49
- fs.writeFileSync(tmpFile, `// Test file with issues
50
- function test() {
51
- // Double negation
52
- const flag = !!value;
53
-
54
- // Unused variable
55
- const unused = 42;
56
-
57
- // Console statement
58
- console.log("test");
59
- }
60
- `);
61
- try {
62
- const oxlintModule = await import("./oxlint.js");
63
- const runner = oxlintModule.default;
64
- const result = await runner.run(createMockContext(tmpFile));
65
- // If oxlint is installed, should detect issues
66
- // If not installed, will be skipped
67
- if (result.status !== "skipped") {
68
- // Should detect at least some issues (console, unused vars, etc.)
69
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
70
- expect(result.diagnostics.some((d) => d.tool === "oxlint" &&
71
- (d.message.includes("console") ||
72
- d.message.includes("unused") ||
73
- d.message.includes("!!")))).toBe(true);
74
- }
75
- }
76
- finally {
77
- // Windows may hold file handles briefly - add small delay
78
- await delay(100);
79
- if (fs.existsSync(tmpFile)) {
80
- fs.unlinkSync(tmpFile);
81
- }
82
- }
83
- });
84
- it("should respect no-oxlint flag", async () => {
85
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_flag_${Date.now()}.ts`);
86
- fs.writeFileSync(tmpFile, `function test() { console.log("test"); }`);
87
- try {
88
- const oxlintModule = await import("./oxlint.js");
89
- const runner = oxlintModule.default;
90
- // Create context with no-oxlint flag set to true
91
- const ctxWithFlag = createMockContext(tmpFile, {
92
- pi: { getFlag: (name) => name === "no-oxlint" },
93
- });
94
- const result = await runner.run(ctxWithFlag);
95
- expect(result.status).toBe("skipped");
96
- }
97
- finally {
98
- // Windows may hold file handles briefly - add small delay
99
- await delay(100);
100
- if (fs.existsSync(tmpFile)) {
101
- fs.unlinkSync(tmpFile);
102
- }
103
- }
104
- });
105
- it("should provide fix suggestions when available", async () => {
106
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_fix_${Date.now()}.ts`);
107
- fs.writeFileSync(tmpFile, `// File with auto-fixable issues
108
- const x = !!value;
109
- `);
110
- try {
111
- const oxlintModule = await import("./oxlint.js");
112
- const runner = oxlintModule.default;
113
- const result = await runner.run(createMockContext(tmpFile));
114
- if (result.status !== "skipped" && result.diagnostics.length > 0) {
115
- // Some issues should be fixable
116
- const fixableDiags = result.diagnostics.filter((d) => d.fixable);
117
- // At least some diagnostics should have fixes
118
- expect(fixableDiags.length).toBeGreaterThanOrEqual(0);
119
- }
120
- }
121
- finally {
122
- // Windows may hold file handles briefly - add small delay
123
- await delay(100);
124
- if (fs.existsSync(tmpFile)) {
125
- fs.unlinkSync(tmpFile);
126
- }
127
- }
128
- });
129
- it("should pass clean TypeScript files", async () => {
130
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_ok_${Date.now()}.ts`);
131
- fs.writeFileSync(tmpFile, `// Clean TypeScript file
132
- function greet(name: string): string {
133
- return \`Hello, \${name}!\`;
134
- }
135
-
136
- const result = greet("world");
137
- export { greet };
138
- `);
139
- try {
140
- const oxlintModule = await import("./oxlint.js");
141
- const runner = oxlintModule.default;
142
- const result = await runner.run(createMockContext(tmpFile));
143
- if (result.status !== "skipped") {
144
- // Clean files should have no issues
145
- expect(result.diagnostics.length).toBe(0);
146
- expect(result.status).toBe("succeeded");
147
- }
148
- }
149
- finally {
150
- // Windows may hold file handles briefly - add small delay
151
- await delay(100);
152
- if (fs.existsSync(tmpFile)) {
153
- fs.unlinkSync(tmpFile);
154
- }
155
- }
156
- });
157
- it("should handle JSON output correctly", async () => {
158
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_json_${Date.now()}.ts`);
159
- fs.writeFileSync(tmpFile, `const unused = 1;`);
160
- try {
161
- const oxlintModule = await import("./oxlint.js");
162
- const runner = oxlintModule.default;
163
- const result = await runner.run(createMockContext(tmpFile));
164
- if (result.status !== "skipped") {
165
- // All diagnostics should have required fields
166
- for (const diag of result.diagnostics) {
167
- expect(diag.id).toBeDefined();
168
- expect(diag.message).toBeDefined();
169
- expect(diag.tool).toBe("oxlint");
170
- expect(diag.line).toBeGreaterThanOrEqual(1);
171
- expect(diag.severity).toMatch(/^(error|warning|info)$/);
172
- }
173
- }
174
- }
175
- finally {
176
- // Windows may hold file handles briefly - add small delay
177
- await delay(100);
178
- if (fs.existsSync(tmpFile)) {
179
- fs.unlinkSync(tmpFile);
180
- }
181
- }
182
- });
183
- it("should skip when oxlint is not available", async () => {
184
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_skip_${Date.now()}.ts`);
185
- fs.writeFileSync(tmpFile, `const x = 1;`);
186
- try {
187
- const oxlintModule = await import("./oxlint.js");
188
- const runner = oxlintModule.default;
189
- // Check if oxlint is available
190
- const { spawnSync } = require("node:child_process");
191
- const checkResult = spawnSync("oxlint", ["--version"], {
192
- encoding: "utf-8",
193
- timeout: 5000,
194
- shell: true,
195
- });
196
- const isAvailable = !checkResult.error && checkResult.status === 0;
197
- const result = await runner.run(createMockContext(tmpFile));
198
- if (!isAvailable) {
199
- expect(result.status).toBe("skipped");
200
- expect(result.diagnostics).toHaveLength(0);
201
- }
202
- }
203
- finally {
204
- // Windows may hold file handles briefly - add small delay
205
- await delay(100);
206
- if (fs.existsSync(tmpFile)) {
207
- fs.unlinkSync(tmpFile);
208
- }
209
- }
210
- });
211
- it("should handle parsing errors gracefully", async () => {
212
- const tmpFile = path.join(process.env.TEMP || "/tmp", `oxlint_parse_${Date.now()}.ts`);
213
- // Intentionally malformed file
214
- fs.writeFileSync(tmpFile, `const x = `);
215
- try {
216
- const oxlintModule = await import("./oxlint.js");
217
- const runner = oxlintModule.default;
218
- const result = await runner.run(createMockContext(tmpFile));
219
- // Should handle parse errors without crashing
220
- expect(["succeeded", "failed", "skipped"]).toContain(result.status);
221
- }
222
- finally {
223
- // Windows may hold file handles briefly - add small delay
224
- await delay(100);
225
- if (fs.existsSync(tmpFile)) {
226
- fs.unlinkSync(tmpFile);
227
- }
228
- }
229
- });
230
- });
@@ -1,98 +0,0 @@
1
- import * as fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import * as path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- function createMockContext(filePath) {
6
- return {
7
- filePath,
8
- cwd: process.cwd(),
9
- kind: "python",
10
- autofix: false,
11
- deltaMode: false,
12
- baselines: { get: () => [], add: () => { }, save: () => { } },
13
- pi: {},
14
- hasTool: async () => false,
15
- log: () => { },
16
- };
17
- }
18
- describe("pyright runner", () => {
19
- const require = createRequire(import.meta.url);
20
- it("should have correct runner definition", async () => {
21
- const pyrightModule = await import("./pyright.js");
22
- const runner = pyrightModule.default;
23
- expect(runner.id).toBe("pyright");
24
- expect(runner.appliesTo).toEqual(["python"]);
25
- expect(runner.priority).toBe(5); // Higher priority than ruff
26
- expect(runner.enabledByDefault).toBe(true);
27
- });
28
- it("should detect pyright availability", () => {
29
- const { spawnSync } = require("node:child_process");
30
- const result = spawnSync("npx", ["pyright", "--version"], {
31
- encoding: "utf-8",
32
- timeout: 10000,
33
- shell: true,
34
- });
35
- expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
36
- });
37
- it("should type-check Python files and find errors", async () => {
38
- const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_${Date.now()}.py`);
39
- fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
40
- return x + y
41
-
42
- result: str = add(1, 2)
43
-
44
- def greet(name: str) -> str:
45
- return "Hello " + name
46
-
47
- greet(123)
48
- `);
49
- try {
50
- const pyrightModule = await import("./pyright.js");
51
- const runner = pyrightModule.default;
52
- const result = await runner.run(createMockContext(tmpFile));
53
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
54
- expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
55
- expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
56
- }
57
- finally {
58
- try {
59
- if (fs.existsSync(tmpFile)) {
60
- fs.unlinkSync(tmpFile);
61
- }
62
- }
63
- catch {
64
- // Ignore cleanup errors
65
- }
66
- }
67
- });
68
- it("should pass valid Python files", async () => {
69
- const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_ok_${Date.now()}.py`);
70
- fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
71
- return x + y
72
-
73
- result: str = str(add(1, 2))
74
-
75
- def greet(name: str) -> str:
76
- return "Hello " + name
77
-
78
- greet("world")
79
- `);
80
- try {
81
- const pyrightModule = await import("./pyright.js");
82
- const runner = pyrightModule.default;
83
- const result = await runner.run(createMockContext(tmpFile));
84
- expect(result.status).toBe("succeeded");
85
- expect(result.diagnostics.length).toBe(0);
86
- }
87
- finally {
88
- try {
89
- if (fs.existsSync(tmpFile)) {
90
- fs.unlinkSync(tmpFile);
91
- }
92
- }
93
- catch {
94
- // Ignore cleanup errors
95
- }
96
- }
97
- });
98
- });