pi-lens 2.2.9 → 3.0.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 +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Runs `cargo clippy` for Rust files to catch common mistakes.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { safeSpawn } from "../../safe-spawn.js";
|
|
7
7
|
import { stripAnsi } from "../../sanitize.js";
|
|
8
8
|
const rustClippyRunner = {
|
|
9
9
|
id: "rust-clippy",
|
|
@@ -12,10 +12,8 @@ const rustClippyRunner = {
|
|
|
12
12
|
enabledByDefault: true,
|
|
13
13
|
async run(ctx) {
|
|
14
14
|
// Check if cargo is available
|
|
15
|
-
const check =
|
|
16
|
-
encoding: "utf-8",
|
|
15
|
+
const check = safeSpawn("cargo", ["--version"], {
|
|
17
16
|
timeout: 5000,
|
|
18
|
-
shell: true,
|
|
19
17
|
});
|
|
20
18
|
if (check.error || check.status !== 0) {
|
|
21
19
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -26,10 +24,8 @@ const rustClippyRunner = {
|
|
|
26
24
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
27
25
|
}
|
|
28
26
|
// Run cargo clippy on the package
|
|
29
|
-
const result =
|
|
30
|
-
encoding: "utf-8",
|
|
27
|
+
const result = safeSpawn("cargo", ["clippy", "--message-format=json", "-q"], {
|
|
31
28
|
timeout: 60000,
|
|
32
|
-
shell: true,
|
|
33
29
|
cwd: cargoToml.replace("Cargo.toml", ""),
|
|
34
30
|
});
|
|
35
31
|
const raw = stripAnsi(result.stdout + result.stderr);
|
|
@@ -57,8 +53,8 @@ const rustClippyRunner = {
|
|
|
57
53
|
function findCargoToml(filePath) {
|
|
58
54
|
const { dirname, join } = require("node:path");
|
|
59
55
|
const { existsSync } = require("node:fs");
|
|
60
|
-
let dir = filePath;
|
|
61
|
-
|
|
56
|
+
let dir = dirname(filePath);
|
|
57
|
+
while (dir !== "/" && dir !== ".") {
|
|
62
58
|
const cargoPath = join(dir, "Cargo.toml");
|
|
63
59
|
if (existsSync(cargoPath)) {
|
|
64
60
|
return cargoPath;
|
|
@@ -70,35 +66,35 @@ function findCargoToml(filePath) {
|
|
|
70
66
|
}
|
|
71
67
|
return undefined;
|
|
72
68
|
}
|
|
73
|
-
function parseClippyOutput(raw,
|
|
69
|
+
function parseClippyOutput(raw, filePath) {
|
|
74
70
|
const diagnostics = [];
|
|
75
|
-
const lines = raw.split("\n");
|
|
71
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
76
72
|
for (const line of lines) {
|
|
77
|
-
if (!line.trim())
|
|
78
|
-
continue;
|
|
79
73
|
try {
|
|
80
74
|
const msg = JSON.parse(line);
|
|
81
|
-
if (msg.message
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
if (msg.reason !== "compiler-message")
|
|
76
|
+
continue;
|
|
77
|
+
const message = msg.message;
|
|
78
|
+
if (!message)
|
|
79
|
+
continue;
|
|
80
|
+
// Only include messages for this file or project-wide
|
|
81
|
+
const span = message.spans?.[0];
|
|
82
|
+
if (!span)
|
|
83
|
+
continue;
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
id: `clippy-${message.code?.code || "unknown"}`,
|
|
86
|
+
message: message.message || "Clippy warning",
|
|
87
|
+
filePath: span.file || filePath,
|
|
88
|
+
line: span.line_start || 0,
|
|
89
|
+
column: span.column_start || 0,
|
|
90
|
+
severity: message.level === "error" ? "error" : "warning",
|
|
91
|
+
semantic: message.level === "error" ? "blocking" : "warning",
|
|
92
|
+
tool: "rust-clippy",
|
|
93
|
+
rule: message.code?.code,
|
|
94
|
+
});
|
|
99
95
|
}
|
|
100
96
|
catch {
|
|
101
|
-
// Not JSON, skip
|
|
97
|
+
// Not a JSON line, skip
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
100
|
return diagnostics;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { safeSpawn } from "../../safe-spawn.js";
|
|
8
9
|
import { stripAnsi } from "../../sanitize.js";
|
|
9
10
|
import type {
|
|
10
11
|
Diagnostic,
|
|
@@ -21,10 +22,8 @@ const rustClippyRunner: RunnerDefinition = {
|
|
|
21
22
|
|
|
22
23
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
23
24
|
// Check if cargo is available
|
|
24
|
-
const check =
|
|
25
|
-
encoding: "utf-8",
|
|
25
|
+
const check = safeSpawn("cargo", ["--version"], {
|
|
26
26
|
timeout: 5000,
|
|
27
|
-
shell: true,
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
if (check.error || check.status !== 0) {
|
|
@@ -38,13 +37,11 @@ const rustClippyRunner: RunnerDefinition = {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
// Run cargo clippy on the package
|
|
41
|
-
const result =
|
|
40
|
+
const result = safeSpawn(
|
|
42
41
|
"cargo",
|
|
43
42
|
["clippy", "--message-format=json", "-q"],
|
|
44
43
|
{
|
|
45
|
-
encoding: "utf-8",
|
|
46
44
|
timeout: 60000,
|
|
47
|
-
shell: true,
|
|
48
45
|
cwd: cargoToml.replace("Cargo.toml", ""),
|
|
49
46
|
},
|
|
50
47
|
);
|
|
@@ -80,8 +77,8 @@ function findCargoToml(filePath: string): string | undefined {
|
|
|
80
77
|
const { dirname, join } = require("node:path");
|
|
81
78
|
const { existsSync } = require("node:fs");
|
|
82
79
|
|
|
83
|
-
let dir = filePath;
|
|
84
|
-
|
|
80
|
+
let dir = dirname(filePath);
|
|
81
|
+
while (dir !== "/" && dir !== ".") {
|
|
85
82
|
const cargoPath = join(dir, "Cargo.toml");
|
|
86
83
|
if (existsSync(cargoPath)) {
|
|
87
84
|
return cargoPath;
|
|
@@ -90,38 +87,39 @@ function findCargoToml(filePath: string): string | undefined {
|
|
|
90
87
|
if (parent === dir) break;
|
|
91
88
|
dir = parent;
|
|
92
89
|
}
|
|
90
|
+
|
|
93
91
|
return undefined;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
function parseClippyOutput(raw: string,
|
|
94
|
+
function parseClippyOutput(raw: string, filePath: string): Diagnostic[] {
|
|
97
95
|
const diagnostics: Diagnostic[] = [];
|
|
98
|
-
const lines = raw.split("\n");
|
|
96
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
99
97
|
|
|
100
98
|
for (const line of lines) {
|
|
101
|
-
if (!line.trim()) continue;
|
|
102
|
-
|
|
103
99
|
try {
|
|
104
100
|
const msg = JSON.parse(line);
|
|
105
|
-
if (msg.message
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
101
|
+
if (msg.reason !== "compiler-message") continue;
|
|
102
|
+
|
|
103
|
+
const message = msg.message;
|
|
104
|
+
if (!message) continue;
|
|
105
|
+
|
|
106
|
+
// Only include messages for this file or project-wide
|
|
107
|
+
const span = message.spans?.[0];
|
|
108
|
+
if (!span) continue;
|
|
109
|
+
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
id: `clippy-${message.code?.code || "unknown"}`,
|
|
112
|
+
message: message.message || "Clippy warning",
|
|
113
|
+
filePath: span.file || filePath,
|
|
114
|
+
line: span.line_start || 0,
|
|
115
|
+
column: span.column_start || 0,
|
|
116
|
+
severity: message.level === "error" ? "error" : "warning",
|
|
117
|
+
semantic: message.level === "error" ? "blocking" : "warning",
|
|
118
|
+
tool: "rust-clippy",
|
|
119
|
+
rule: message.code?.code,
|
|
120
|
+
});
|
|
123
121
|
} catch {
|
|
124
|
-
// Not JSON, skip
|
|
122
|
+
// Not a JSON line, skip
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type { DispatchContext } from "../types.js";
|
|
5
|
+
|
|
6
|
+
// Find all TS files
|
|
7
|
+
function findTsFiles(dir: string): string[] {
|
|
8
|
+
const files: string[] = [];
|
|
9
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = path.join(dir, entry.name);
|
|
13
|
+
|
|
14
|
+
// Skip node_modules, .git, etc
|
|
15
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".pi-lens") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
files.push(...findTsFiles(fullPath));
|
|
21
|
+
} else if (entry.isFile() && fullPath.endsWith(".ts") && !fullPath.endsWith(".test.ts")) {
|
|
22
|
+
files.push(fullPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createContext(filePath: string): DispatchContext {
|
|
30
|
+
return {
|
|
31
|
+
filePath,
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
kind: "jsts",
|
|
34
|
+
autofix: false,
|
|
35
|
+
deltaMode: false,
|
|
36
|
+
baselines: { get: () => [], add: () => {}, save: () => {} } as any,
|
|
37
|
+
pi: {} as any,
|
|
38
|
+
hasTool: async () => false,
|
|
39
|
+
log: () => {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("Codebase scan with NAPI runner", () => {
|
|
44
|
+
it("should scan all TypeScript files and report findings", async () => {
|
|
45
|
+
const tsFiles = findTsFiles(process.cwd());
|
|
46
|
+
console.log(`\nFound ${tsFiles.length} TypeScript files to scan\n`);
|
|
47
|
+
|
|
48
|
+
const runner = (await import("./ast-grep-napi.js")).default;
|
|
49
|
+
|
|
50
|
+
const allIssues: Array<{ file: string; line: number; rule: string; message: string }> = [];
|
|
51
|
+
let totalTime = 0;
|
|
52
|
+
let filesWithIssues = 0;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < Math.min(tsFiles.length, 50); i++) { // Limit to 50 for test speed
|
|
55
|
+
const file = tsFiles[i];
|
|
56
|
+
const ctx = createContext(file);
|
|
57
|
+
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
const result = await runner.run(ctx);
|
|
60
|
+
const elapsed = Date.now() - start;
|
|
61
|
+
totalTime += elapsed;
|
|
62
|
+
|
|
63
|
+
if (result.diagnostics.length > 0) {
|
|
64
|
+
filesWithIssues++;
|
|
65
|
+
console.log(`${path.relative(process.cwd(), file)} (${elapsed}ms):`);
|
|
66
|
+
for (const d of result.diagnostics.slice(0, 5)) { // Show max 5 per file
|
|
67
|
+
const line = d.line ?? 0;
|
|
68
|
+
const rule = d.rule ?? "unknown";
|
|
69
|
+
const message = d.message?.split('\n')[0] ?? "";
|
|
70
|
+
console.log(` Line ${line}: [${rule}] ${message}`);
|
|
71
|
+
allIssues.push({
|
|
72
|
+
file: path.relative(process.cwd(), file),
|
|
73
|
+
line,
|
|
74
|
+
rule,
|
|
75
|
+
message,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (result.diagnostics.length > 5) {
|
|
79
|
+
console.log(` ... and ${result.diagnostics.length - 5} more`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n=== SUMMARY (first 50 files) ===`);
|
|
85
|
+
console.log(`Files scanned: ${Math.min(tsFiles.length, 50)}/${tsFiles.length}`);
|
|
86
|
+
console.log(`Total time: ${totalTime}ms`);
|
|
87
|
+
console.log(`Files with issues: ${filesWithIssues}`);
|
|
88
|
+
console.log(`Total issues: ${allIssues.length}`);
|
|
89
|
+
console.log(`Avg time per file: ${(totalTime / Math.min(tsFiles.length, 50)).toFixed(1)}ms`);
|
|
90
|
+
|
|
91
|
+
// Group by rule
|
|
92
|
+
const byRule: Record<string, number> = {};
|
|
93
|
+
for (const issue of allIssues) {
|
|
94
|
+
byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\n=== BY RULE ===`);
|
|
98
|
+
for (const [rule, count] of Object.entries(byRule).sort((a, b) => b[1] - a[1])) {
|
|
99
|
+
console.log(` ${rule}: ${count}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// This test should pass - we're just scanning
|
|
103
|
+
expect(true).toBe(true);
|
|
104
|
+
}, 60000); // 60 second timeout for scanning
|
|
105
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shellcheck runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Industry-standard linter for shell scripts (bash, sh, zsh).
|
|
5
|
+
* Detects syntax errors, undefined variables, quoting issues, and best practices.
|
|
6
|
+
*
|
|
7
|
+
* Why shellcheck?
|
|
8
|
+
* - Industry standard (used in CI/CD everywhere)
|
|
9
|
+
* - Comprehensive checks (syntax, variables, quotes, best practices)
|
|
10
|
+
* - JSON output for easy parsing
|
|
11
|
+
* - Available on all platforms (apt, brew, cargo, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Alternative considered: bash-language-server
|
|
14
|
+
* - LSP approach like OpenCode uses
|
|
15
|
+
* - Richer features but heavier
|
|
16
|
+
* - shellcheck is simpler and faster for basic linting
|
|
17
|
+
*
|
|
18
|
+
* Install: apt install shellcheck, brew install shellcheck, or cargo install shellcheck
|
|
19
|
+
*
|
|
20
|
+
* Config: .shellcheckrc (optional, zero-config works)
|
|
21
|
+
*/
|
|
22
|
+
import { safeSpawn } from "../../safe-spawn.js";
|
|
23
|
+
import { createAvailabilityChecker, createConfigFinder, } from "./utils/runner-helpers.js";
|
|
24
|
+
const shellcheck = createAvailabilityChecker("shellcheck", ".exe");
|
|
25
|
+
const findShellcheckConfig = createConfigFinder(".shellcheckrc");
|
|
26
|
+
/**
|
|
27
|
+
* Parse shellcheck JSON output
|
|
28
|
+
*
|
|
29
|
+
* Format: Array of check objects
|
|
30
|
+
* [{
|
|
31
|
+
* "file": "script.sh",
|
|
32
|
+
* "line": 10,
|
|
33
|
+
* "endLine": 10,
|
|
34
|
+
* "column": 5,
|
|
35
|
+
* "endColumn": 10,
|
|
36
|
+
* "level": "warning",
|
|
37
|
+
* "code": 2154,
|
|
38
|
+
* "message": "var is referenced but not assigned.",
|
|
39
|
+
* "fix": null
|
|
40
|
+
* }]
|
|
41
|
+
*
|
|
42
|
+
* Levels: "error", "warning", "info", "style"
|
|
43
|
+
*/
|
|
44
|
+
function parseShellcheckOutput(raw, filePath) {
|
|
45
|
+
const diagnostics = [];
|
|
46
|
+
if (!raw.trim()) {
|
|
47
|
+
return diagnostics;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (!Array.isArray(parsed)) {
|
|
52
|
+
return diagnostics;
|
|
53
|
+
}
|
|
54
|
+
for (const item of parsed) {
|
|
55
|
+
if (!item.message || !item.line)
|
|
56
|
+
continue;
|
|
57
|
+
// Map shellcheck levels to our severity
|
|
58
|
+
const severityMap = {
|
|
59
|
+
error: "error",
|
|
60
|
+
warning: "warning",
|
|
61
|
+
info: "info",
|
|
62
|
+
style: "info",
|
|
63
|
+
};
|
|
64
|
+
const severity = severityMap[item.level || "warning"] || "warning";
|
|
65
|
+
const ruleCode = item.code ? `SC${item.code}` : "unknown";
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
id: `shellcheck-${item.line}-${ruleCode}`,
|
|
68
|
+
message: `[${ruleCode}] ${item.message}`,
|
|
69
|
+
filePath,
|
|
70
|
+
line: item.line,
|
|
71
|
+
column: item.column || 1,
|
|
72
|
+
severity,
|
|
73
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
74
|
+
tool: "shellcheck",
|
|
75
|
+
rule: ruleCode,
|
|
76
|
+
fixable: !!item.fix,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// JSON parse failed, return empty
|
|
82
|
+
return diagnostics;
|
|
83
|
+
}
|
|
84
|
+
return diagnostics;
|
|
85
|
+
}
|
|
86
|
+
const shellcheckRunner = {
|
|
87
|
+
id: "shellcheck",
|
|
88
|
+
appliesTo: ["shell"],
|
|
89
|
+
priority: 20,
|
|
90
|
+
enabledByDefault: true,
|
|
91
|
+
skipTestFiles: false, // Shell scripts in test directories should still be checked
|
|
92
|
+
async run(ctx) {
|
|
93
|
+
// Skip if shellcheck is not installed
|
|
94
|
+
if (!shellcheck.isAvailable(ctx.cwd || process.cwd())) {
|
|
95
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
96
|
+
}
|
|
97
|
+
// Check if user explicitly disabled shellcheck
|
|
98
|
+
if (ctx.pi.getFlag("no-shellcheck")) {
|
|
99
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
100
|
+
}
|
|
101
|
+
// Determine shell dialect from file extension
|
|
102
|
+
const shellDialect = ctx.filePath.endsWith(".zsh")
|
|
103
|
+
? "bash"
|
|
104
|
+
: ctx.filePath.endsWith(".fish")
|
|
105
|
+
? "bash"
|
|
106
|
+
: ctx.filePath.endsWith(".sh")
|
|
107
|
+
? "bash"
|
|
108
|
+
: "bash"; // Default to bash for generic shell files
|
|
109
|
+
// Build args
|
|
110
|
+
// --format json: JSON output
|
|
111
|
+
// --shell: Specify shell dialect (bash, sh, zsh, ksh, busybox)
|
|
112
|
+
// --severity: Minimum severity (we'll filter ourselves)
|
|
113
|
+
const args = [
|
|
114
|
+
"--format",
|
|
115
|
+
"json",
|
|
116
|
+
"--shell",
|
|
117
|
+
shellDialect,
|
|
118
|
+
];
|
|
119
|
+
// Check for config file
|
|
120
|
+
const configPath = findShellcheckConfig(ctx.cwd);
|
|
121
|
+
if (!configPath) {
|
|
122
|
+
// No config file, use default settings
|
|
123
|
+
// Exclude "style" and "info" by default to reduce noise
|
|
124
|
+
args.push("--severity", "warning");
|
|
125
|
+
}
|
|
126
|
+
args.push(ctx.filePath);
|
|
127
|
+
const result = safeSpawn(shellcheck.getCommand(), args, {
|
|
128
|
+
timeout: 15000,
|
|
129
|
+
});
|
|
130
|
+
// shellcheck exits with code 1 if issues found, 0 if clean
|
|
131
|
+
if (result.status === 0 && !result.stdout?.trim()) {
|
|
132
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
133
|
+
}
|
|
134
|
+
// Parse diagnostics
|
|
135
|
+
const raw = result.stdout + result.stderr;
|
|
136
|
+
const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
|
|
137
|
+
if (diagnostics.length === 0) {
|
|
138
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
status: "failed",
|
|
142
|
+
diagnostics,
|
|
143
|
+
semantic: "warning",
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
export default shellcheckRunner;
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
});
|