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,203 +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
- // Helper for safe file cleanup
19
- function safeUnlink(filePath) {
20
- try {
21
- if (fs.existsSync(filePath)) {
22
- fs.unlinkSync(filePath);
23
- }
24
- }
25
- catch {
26
- // Ignore cleanup errors on Windows
27
- }
28
- }
29
- describe("python-slop runner", () => {
30
- const require = createRequire(import.meta.url);
31
- it("should have correct runner definition", async () => {
32
- const slopModule = await import("./python-slop.js");
33
- const runner = slopModule.default;
34
- expect(runner.id).toBe("python-slop");
35
- expect(runner.appliesTo).toEqual(["python"]);
36
- expect(runner.priority).toBe(25);
37
- expect(runner.enabledByDefault).toBe(true);
38
- expect(runner.skipTestFiles).toBe(true);
39
- });
40
- it("should detect ast-grep availability", () => {
41
- const { spawnSync } = require("node:child_process");
42
- const result = spawnSync("npx", ["sg", "--version"], {
43
- encoding: "utf-8",
44
- timeout: 10000,
45
- shell: true,
46
- });
47
- expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
48
- });
49
- it("should detect verbose range-len pattern", async () => {
50
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_range_${Date.now()}.py`);
51
- fs.writeFileSync(tmpFile, `# Slop: using range(len()) instead of enumerate
52
- def process_items(items):
53
- for i in range(len(items)):
54
- print(items[i])
55
- `);
56
- try {
57
- const slopModule = await import("./python-slop.js");
58
- const runner = slopModule.default;
59
- const result = await runner.run(createMockContext(tmpFile));
60
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
61
- expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
62
- d.message.includes("range(len") &&
63
- d.message.includes("enumerate"))).toBe(true);
64
- }
65
- finally {
66
- safeUnlink(tmpFile);
67
- }
68
- });
69
- it("should detect manual min/max pattern", async () => {
70
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_minmax_${Date.now()}.py`);
71
- fs.writeFileSync(tmpFile, `# Slop: manual min/max instead of built-in
72
- def find_max(a, b):
73
- if a > b:
74
- m = a
75
- else:
76
- m = b
77
- return m
78
- `);
79
- try {
80
- const slopModule = await import("./python-slop.js");
81
- const runner = slopModule.default;
82
- const result = await runner.run(createMockContext(tmpFile));
83
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
84
- expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
85
- (d.message.includes("min") || d.message.includes("max")))).toBe(true);
86
- }
87
- finally {
88
- safeUnlink(tmpFile);
89
- }
90
- });
91
- it("should detect defensive None guard", async () => {
92
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_guard_${Date.now()}.py`);
93
- fs.writeFileSync(tmpFile, `# Slop: defensive None guard
94
- def process(data):
95
- if data is None:
96
- return None
97
- return data.upper()
98
- `);
99
- try {
100
- const slopModule = await import("./python-slop.js");
101
- const runner = slopModule.default;
102
- const result = await runner.run(createMockContext(tmpFile));
103
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
104
- expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
105
- (d.message.includes("defensive") ||
106
- d.message.includes("guard")))).toBe(true);
107
- }
108
- finally {
109
- safeUnlink(tmpFile);
110
- }
111
- });
112
- it("should detect list comprehension ceremony", async () => {
113
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_list_${Date.now()}.py`);
114
- fs.writeFileSync(tmpFile, `# Slop: redundant list comprehension
115
- def convert(items):
116
- return [x for x in items]
117
- `);
118
- try {
119
- const slopModule = await import("./python-slop.js");
120
- const runner = slopModule.default;
121
- const result = await runner.run(createMockContext(tmpFile));
122
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
123
- expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
124
- d.message.includes("list") &&
125
- d.message.includes("unnecessary"))).toBe(true);
126
- }
127
- finally {
128
- safeUnlink(tmpFile);
129
- }
130
- });
131
- it("should detect chained comparison opportunity", async () => {
132
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_chain_${Date.now()}.py`);
133
- fs.writeFileSync(tmpFile, `# Slop: could use chained comparison
134
- def check_range(x, a, b):
135
- return a < x and x < b
136
- `);
137
- try {
138
- const slopModule = await import("./python-slop.js");
139
- const runner = slopModule.default;
140
- const result = await runner.run(createMockContext(tmpFile));
141
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
142
- expect(result.diagnostics.some((d) => d.tool === "python-slop" &&
143
- d.message.includes("chained"))).toBe(true);
144
- }
145
- finally {
146
- safeUnlink(tmpFile);
147
- }
148
- });
149
- it("should pass clean Python files", async () => {
150
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_ok_${Date.now()}.py`);
151
- fs.writeFileSync(tmpFile, `# Clean Python code
152
- def process_items(items):
153
- """Process items using proper Python idioms."""
154
- for i, item in enumerate(items):
155
- print(f"{i}: {item}")
156
-
157
- def find_max(a, b):
158
- return max(a, b)
159
-
160
- def check_range(x, min_val, max_val):
161
- return min_val < x < max_val
162
-
163
- def convert(items):
164
- return list(items)
165
- `);
166
- try {
167
- const slopModule = await import("./python-slop.js");
168
- const runner = slopModule.default;
169
- const result = await runner.run(createMockContext(tmpFile));
170
- // Should have no slop issues
171
- const slopIssues = result.diagnostics.filter((d) => d.tool === "python-slop");
172
- expect(slopIssues.length).toBe(0);
173
- }
174
- finally {
175
- safeUnlink(tmpFile);
176
- }
177
- });
178
- it("should categorize by weight correctly", async () => {
179
- const tmpFile = path.join(process.env.TEMP || "/tmp", `slop_test_weight_${Date.now()}.py`);
180
- fs.writeFileSync(tmpFile, `# Multiple slop patterns - weight 3 and weight 4
181
- def bad_code(items):
182
- # Weight 3: range(len)
183
- for i in range(len(items)):
184
- print(items[i])
185
-
186
- # Weight 3: redundant list comprehension
187
- return [x for x in items]
188
- `);
189
- try {
190
- const slopModule = await import("./python-slop.js");
191
- const runner = slopModule.default;
192
- const result = await runner.run(createMockContext(tmpFile));
193
- // Should detect at least the range(len) pattern
194
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
195
- // All should be warnings (weight 3)
196
- const warnings = result.diagnostics.filter((d) => d.severity === "warning");
197
- expect(warnings.length).toBeGreaterThanOrEqual(1);
198
- }
199
- finally {
200
- safeUnlink(tmpFile);
201
- }
202
- });
203
- });
@@ -1,89 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- // Find all TS files
5
- function findTsFiles(dir) {
6
- const files = [];
7
- const entries = fs.readdirSync(dir, { withFileTypes: true });
8
- for (const entry of entries) {
9
- const fullPath = path.join(dir, entry.name);
10
- // Skip node_modules, .git, etc
11
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".pi-lens") {
12
- continue;
13
- }
14
- if (entry.isDirectory()) {
15
- files.push(...findTsFiles(fullPath));
16
- }
17
- else if (entry.isFile() && fullPath.endsWith(".ts") && !fullPath.endsWith(".test.ts")) {
18
- files.push(fullPath);
19
- }
20
- }
21
- return files;
22
- }
23
- function createContext(filePath) {
24
- return {
25
- filePath,
26
- cwd: process.cwd(),
27
- kind: "jsts",
28
- autofix: false,
29
- deltaMode: false,
30
- baselines: { get: () => [], add: () => { }, save: () => { } },
31
- pi: {},
32
- hasTool: async () => false,
33
- log: () => { },
34
- };
35
- }
36
- describe("Codebase scan with NAPI runner", () => {
37
- it("should scan all TypeScript files and report findings", async () => {
38
- const tsFiles = findTsFiles(process.cwd());
39
- console.log(`\nFound ${tsFiles.length} TypeScript files to scan\n`);
40
- const runner = (await import("./ast-grep-napi.js")).default;
41
- const allIssues = [];
42
- let totalTime = 0;
43
- let filesWithIssues = 0;
44
- for (let i = 0; i < Math.min(tsFiles.length, 50); i++) { // Limit to 50 for test speed
45
- const file = tsFiles[i];
46
- const ctx = createContext(file);
47
- const start = Date.now();
48
- const result = await runner.run(ctx);
49
- const elapsed = Date.now() - start;
50
- totalTime += elapsed;
51
- if (result.diagnostics.length > 0) {
52
- filesWithIssues++;
53
- console.log(`${path.relative(process.cwd(), file)} (${elapsed}ms):`);
54
- for (const d of result.diagnostics.slice(0, 5)) { // Show max 5 per file
55
- const line = d.line ?? 0;
56
- const rule = d.rule ?? "unknown";
57
- const message = d.message?.split('\n')[0] ?? "";
58
- console.log(` Line ${line}: [${rule}] ${message}`);
59
- allIssues.push({
60
- file: path.relative(process.cwd(), file),
61
- line,
62
- rule,
63
- message,
64
- });
65
- }
66
- if (result.diagnostics.length > 5) {
67
- console.log(` ... and ${result.diagnostics.length - 5} more`);
68
- }
69
- }
70
- }
71
- console.log(`\n=== SUMMARY (first 50 files) ===`);
72
- console.log(`Files scanned: ${Math.min(tsFiles.length, 50)}/${tsFiles.length}`);
73
- console.log(`Total time: ${totalTime}ms`);
74
- console.log(`Files with issues: ${filesWithIssues}`);
75
- console.log(`Total issues: ${allIssues.length}`);
76
- console.log(`Avg time per file: ${(totalTime / Math.min(tsFiles.length, 50)).toFixed(1)}ms`);
77
- // Group by rule
78
- const byRule = {};
79
- for (const issue of allIssues) {
80
- byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
81
- }
82
- console.log(`\n=== BY RULE ===`);
83
- for (const [rule, count] of Object.entries(byRule).sort((a, b) => b[1] - a[1])) {
84
- console.log(` ${rule}: ${count}`);
85
- }
86
- // This test should pass - we're just scanning
87
- expect(true).toBe(true);
88
- }, 60000); // 60 second timeout for scanning
89
- });
@@ -1,98 +0,0 @@
1
- /**
2
- * Tests for shellcheck 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
- function createMockContext(filePath) {
9
- return {
10
- filePath,
11
- cwd: process.cwd(),
12
- kind: "shell",
13
- autofix: false,
14
- deltaMode: false,
15
- baselines: { get: () => [], add: () => { }, save: () => { } },
16
- pi: {},
17
- hasTool: async () => false,
18
- log: () => { },
19
- };
20
- }
21
- // Helper for safe file cleanup
22
- function safeUnlink(filePath) {
23
- try {
24
- if (fs.existsSync(filePath)) {
25
- fs.unlinkSync(filePath);
26
- }
27
- }
28
- catch {
29
- // Ignore cleanup errors on Windows
30
- }
31
- }
32
- describe("shellcheck runner", () => {
33
- const require = createRequire(import.meta.url);
34
- it("should have correct runner definition", async () => {
35
- const shellcheckModule = await import("./shellcheck.js");
36
- const runner = shellcheckModule.default;
37
- expect(runner.id).toBe("shellcheck");
38
- expect(runner.appliesTo).toEqual(["shell"]);
39
- expect(runner.priority).toBe(20);
40
- expect(runner.enabledByDefault).toBe(true);
41
- expect(runner.skipTestFiles).toBe(false);
42
- });
43
- it("should detect shellcheck availability", () => {
44
- const { spawnSync } = require("node:child_process");
45
- const result = spawnSync("shellcheck", ["--version"], {
46
- encoding: "utf-8",
47
- timeout: 10000,
48
- shell: true,
49
- });
50
- expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy();
51
- });
52
- it("should detect undefined variable", async () => {
53
- const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_test_${Date.now()}.sh`);
54
- fs.writeFileSync(tmpFile, ["#!/bin/bash", "# Test script with issues", 'echo "\$UNDEFINED_VAR"', ""].join("\n"));
55
- try {
56
- const shellcheckModule = await import("./shellcheck.js");
57
- const runner = shellcheckModule.default;
58
- const result = await runner.run(createMockContext(tmpFile));
59
- if (result.status !== "skipped") {
60
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
61
- expect(result.diagnostics.some((d) => d.tool === "shellcheck" &&
62
- (d.message.includes("undefined") ||
63
- d.message.includes("SC2154")))).toBe(true);
64
- }
65
- }
66
- finally {
67
- safeUnlink(tmpFile);
68
- }
69
- });
70
- it("should pass clean shell scripts", async () => {
71
- const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_ok_${Date.now()}.sh`);
72
- fs.writeFileSync(tmpFile, [
73
- "#!/bin/bash",
74
- "# Clean shell script",
75
- "set -euo pipefail",
76
- "",
77
- "main() {",
78
- ' local name="\${1:-world}"',
79
- ' echo "Hello, \${name}!"',
80
- "}",
81
- "",
82
- 'main "\$@"',
83
- "",
84
- ].join("\n"));
85
- try {
86
- const shellcheckModule = await import("./shellcheck.js");
87
- const runner = shellcheckModule.default;
88
- const result = await runner.run(createMockContext(tmpFile));
89
- if (result.status !== "skipped") {
90
- expect(result.diagnostics.length).toBe(0);
91
- expect(result.status).toBe("succeeded");
92
- }
93
- }
94
- finally {
95
- safeUnlink(tmpFile);
96
- }
97
- });
98
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * Tests for spellcheck runner (typos-cli)
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
- function createMockContext(filePath) {
9
- return {
10
- filePath,
11
- cwd: process.cwd(),
12
- kind: "markdown",
13
- autofix: false,
14
- deltaMode: false,
15
- baselines: { get: () => [], add: () => { }, save: () => { } },
16
- pi: {},
17
- hasTool: async () => false,
18
- log: () => { },
19
- };
20
- }
21
- describe("spellcheck runner", () => {
22
- const require = createRequire(import.meta.url);
23
- it("should have correct runner definition", async () => {
24
- const spellcheckModule = await import("./spellcheck.js");
25
- const runner = spellcheckModule.default;
26
- expect(runner.id).toBe("spellcheck");
27
- expect(runner.appliesTo).toEqual(["markdown"]);
28
- expect(runner.priority).toBe(30);
29
- expect(runner.enabledByDefault).toBe(true);
30
- expect(runner.skipTestFiles).toBe(false); // Check docs in test files too
31
- });
32
- it("should detect typos-cli availability", () => {
33
- const { spawnSync } = require("node:child_process");
34
- const result = spawnSync("typos", ["--version"], {
35
- encoding: "utf-8",
36
- timeout: 10000,
37
- shell: true,
38
- });
39
- expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy(); // May or may not be installed
40
- });
41
- it("should detect typos in markdown content", async () => {
42
- const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_test_${Date.now()}.md`);
43
- fs.writeFileSync(tmpFile, `# README
44
-
45
- This is a documnet about recieving data.
46
- The seperation of concerns is important.
47
- `);
48
- try {
49
- const spellcheckModule = await import("./spellcheck.js");
50
- const runner = spellcheckModule.default;
51
- const result = await runner.run(createMockContext(tmpFile));
52
- // If typos-cli is installed, should detect typos
53
- // If not installed, will be skipped
54
- if (result.status !== "skipped") {
55
- // Should detect at least "documnet" and "recieving"
56
- expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
57
- expect(result.diagnostics.some((d) => d.tool === "typos" &&
58
- (d.message.includes("documnet") ||
59
- d.message.includes("recieving") ||
60
- d.message.includes("seperation")))).toBe(true);
61
- }
62
- }
63
- finally {
64
- if (fs.existsSync(tmpFile)) {
65
- fs.unlinkSync(tmpFile);
66
- }
67
- }
68
- });
69
- it("should suggest corrections for typos", async () => {
70
- const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_fix_${Date.now()}.md`);
71
- fs.writeFileSync(tmpFile, `# Test
72
-
73
- This is a recieving test.
74
- `);
75
- try {
76
- const spellcheckModule = await import("./spellcheck.js");
77
- const runner = spellcheckModule.default;
78
- const result = await runner.run(createMockContext(tmpFile));
79
- if (result.status !== "skipped" && result.diagnostics.length > 0) {
80
- // Should have fix suggestions
81
- const fixableDiags = result.diagnostics.filter((d) => d.fixable);
82
- expect(fixableDiags.length).toBeGreaterThanOrEqual(1);
83
- expect(fixableDiags.some((d) => d.fixSuggestion?.toLowerCase().includes("receive"))).toBe(true);
84
- }
85
- }
86
- finally {
87
- if (fs.existsSync(tmpFile)) {
88
- fs.unlinkSync(tmpFile);
89
- }
90
- }
91
- });
92
- it("should pass clean markdown files", async () => {
93
- const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_ok_${Date.now()}.md`);
94
- fs.writeFileSync(tmpFile, `# Clean README
95
-
96
- This is a correct document about receiving data.
97
- The separation of concerns is important.
98
- All spelling is proper in this file.
99
- `);
100
- try {
101
- const spellcheckModule = await import("./spellcheck.js");
102
- const runner = spellcheckModule.default;
103
- const result = await runner.run(createMockContext(tmpFile));
104
- if (result.status !== "skipped") {
105
- // Should have no typos
106
- expect(result.diagnostics.length).toBe(0);
107
- expect(result.status).toBe("succeeded");
108
- }
109
- }
110
- finally {
111
- if (fs.existsSync(tmpFile)) {
112
- fs.unlinkSync(tmpFile);
113
- }
114
- }
115
- });
116
- it("should handle JSON parse errors gracefully", async () => {
117
- const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_json_${Date.now()}.md`);
118
- fs.writeFileSync(tmpFile, `# Test\n\nSimple file.`);
119
- try {
120
- const spellcheckModule = await import("./spellcheck.js");
121
- const runner = spellcheckModule.default;
122
- const result = await runner.run(createMockContext(tmpFile));
123
- // Should not crash on JSON parse issues
124
- expect(["succeeded", "failed", "skipped"]).toContain(result.status);
125
- }
126
- finally {
127
- if (fs.existsSync(tmpFile)) {
128
- fs.unlinkSync(tmpFile);
129
- }
130
- }
131
- });
132
- it("should skip when typos-cli is not available", async () => {
133
- const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_skip_${Date.now()}.md`);
134
- fs.writeFileSync(tmpFile, `# Test\n\nContent with typo: recieve.`);
135
- try {
136
- const spellcheckModule = await import("./spellcheck.js");
137
- const runner = spellcheckModule.default;
138
- // Check if typos is available
139
- const { spawnSync } = require("node:child_process");
140
- const checkResult = spawnSync("typos", ["--version"], {
141
- encoding: "utf-8",
142
- timeout: 5000,
143
- shell: true,
144
- });
145
- const isAvailable = !checkResult.error && checkResult.status === 0;
146
- const result = await runner.run(createMockContext(tmpFile));
147
- if (!isAvailable) {
148
- expect(result.status).toBe("skipped");
149
- expect(result.diagnostics).toHaveLength(0);
150
- }
151
- }
152
- finally {
153
- if (fs.existsSync(tmpFile)) {
154
- fs.unlinkSync(tmpFile);
155
- }
156
- }
157
- });
158
- });