pi-lens 4.2.0 → 4.3.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 +543 -0
- package/README.md +45 -2
- package/dist/clients/actionable-warnings.js +44 -16
- package/dist/clients/advisory-provenance.js +187 -51
- package/dist/clients/cache-manager.js +64 -5
- package/dist/clients/cascade-format.js +24 -0
- package/dist/clients/child-unref.js +17 -8
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-locations.js +237 -9
- package/dist/clients/config-resolve.js +7 -1
- package/dist/clients/degradation-ledger.js +33 -3
- package/dist/clients/dependency-checker.js +87 -64
- package/dist/clients/diagnostic-dispositions.js +39 -3
- package/dist/clients/dispatch/finding-policy.js +118 -1
- package/dist/clients/dispatch/indent-detect.js +8 -2
- package/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/dist/clients/dispatch/rule-ignores.js +14 -7
- package/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/dist/clients/dispatch/runners/credo.js +16 -14
- package/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/dist/clients/dispatch/runners/detekt.js +11 -4
- package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/dist/clients/dispatch/runners/eslint.js +29 -46
- package/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/dist/clients/dispatch/runners/javac.js +31 -21
- package/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/dist/clients/dispatch/runners/lsp.js +6 -5
- package/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/dist/clients/dispatch/runners/ruff.js +18 -19
- package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/dist/clients/dispatch/runners/taplo.js +12 -6
- package/dist/clients/dispatch/runners/tflint.js +30 -15
- package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/dist/clients/dispatch/runners/vale.js +15 -3
- package/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/dist/clients/dispatch/suppress-writer.js +13 -3
- package/dist/clients/effective-config.js +3 -1
- package/dist/clients/extension-log.js +48 -1
- package/dist/clients/finding-delivery-gate.js +53 -23
- package/dist/clients/formatters.js +49 -11
- package/dist/clients/git-guard.js +247 -34
- package/dist/clients/gradle-ktfmt-style.js +121 -25
- package/dist/clients/inline-blocker-dispositions.js +186 -0
- package/dist/clients/install-diagnostics.js +19 -6
- package/dist/clients/installer/index.js +281 -32
- package/dist/clients/jscpd-client.js +8 -0
- package/dist/clients/latency-logger.js +8 -0
- package/dist/clients/lens-config.js +66 -8
- package/dist/clients/lsp/cascade-tier.js +3 -0
- package/dist/clients/lsp/client.js +139 -11
- package/dist/clients/lsp/config.js +9 -7
- package/dist/clients/lsp/content-limits.js +41 -0
- package/dist/clients/lsp/index.js +42 -8
- package/dist/clients/lsp/launch.js +23 -2
- package/dist/clients/lsp/server.js +106 -25
- package/dist/clients/lsp/sync-kind.js +14 -0
- package/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/dist/clients/mcp/analyze.js +9 -2
- package/dist/clients/mcp/ipc.js +232 -44
- package/dist/clients/mcp/review.js +53 -7
- package/dist/clients/opengrep-client.js +11 -6
- package/dist/clients/path-utils.js +51 -9
- package/dist/clients/persistent-reverify.js +271 -0
- package/dist/clients/pipeline.js +136 -67
- package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/dist/clients/project-lens-config.js +11 -3
- package/dist/clients/review-graph/builder.js +34 -26
- package/dist/clients/review-graph/git-identity.js +16 -10
- package/dist/clients/ruff-client.js +31 -4
- package/dist/clients/runtime-coordinator.js +38 -4
- package/dist/clients/runtime-tool-call.js +16 -5
- package/dist/clients/runtime-tool-result.js +27 -12
- package/dist/clients/runtime-turn.js +571 -287
- package/dist/clients/safe-spawn.js +256 -65
- package/dist/clients/skills-resolver.js +38 -2
- package/dist/clients/spawn-output-cap.js +84 -1
- package/dist/clients/tree-sitter-client.js +119 -58
- package/dist/clients/turn-end/lane.js +51 -0
- package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/dist/clients/warm-attach.js +59 -0
- package/dist/clients/widget-state.js +294 -17
- package/dist/clients/workspace-topology.js +57 -3
- package/dist/clients/zizmor-config.js +26 -13
- package/dist/index.js +6342 -3995
- package/dist/mcp/analyze-cli.js +4 -0
- package/dist/mcp/server.js +33 -21
- package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/dist/tools/lens-diagnostic-mark.js +14 -2
- package/dist/tools/lens-diagnostics.js +127 -33
- package/dist/tools/lsp-diagnostics.js +150 -12
- package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/docs/adr/0009-reported-path-attribution.md +114 -0
- package/docs/adr/README.md +26 -0
- package/docs/agent-tools.md +18 -1
- package/docs/ast-grep_rules_catalog.md +4 -3
- package/docs/configuration.md +55 -1
- package/docs/engineering-principles.md +202 -0
- package/docs/environment-variables.md +61 -4
- package/docs/features.md +2 -2
- package/docs/globalconfig.md +6 -1
- package/docs/language-coverage.md +17 -0
- package/docs/lsp-capability-matrix.md +98 -31
- package/docs/pi-lens-fixer.md +57 -3
- package/docs/pi-lens-investigator.md +9 -0
- package/docs/pi-lens-reviewer.md +16 -0
- package/docs/pi-lens-subagent.md +9 -0
- package/docs/public-api-stability.md +1 -0
- package/docs/release-qa-baseline.md +2 -0
- package/docs/servercapabilities.md +21 -11
- package/docs/settings.md +12 -3
- package/docs/subagent-compat.md +1 -1
- package/package.json +5 -3
- package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/scripts/install-selftest.mjs +21 -17
- package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getProjectDataDir } from "../../file-utils.js";
|
|
4
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
4
5
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
5
6
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
6
7
|
import { PRIORITY } from "../priorities.js";
|
|
7
8
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
9
|
+
import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
|
|
8
10
|
// Per-cwd cached `--version` probes (#120). Before this, each dispatch
|
|
9
11
|
// invocation fired a fresh `safeSpawnAsync` per command — once per Elixir
|
|
10
12
|
// file save.
|
|
@@ -34,15 +36,15 @@ function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
|
|
|
34
36
|
const lines = raw.split(/\r?\n/);
|
|
35
37
|
// elixirc reports paths RELATIVE to its cwd (e.g. `bad.ex`, not the absolute
|
|
36
38
|
// path we passed), so resolve the reported path against the runner cwd — not
|
|
37
|
-
// process.cwd().
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
// process.cwd(). Two resolved spellings of ONE file still differ in ways
|
|
40
|
+
// only the filesystem can settle — Elixir 1.16+ normalizes to a lowercase
|
|
41
|
+
// drive letter and forward slashes (`c:/...`), which never string-equals
|
|
42
|
+
// `C:\...` on Windows — so ask `pathsEqual`, the repo's on-disk identity
|
|
43
|
+
// seam, instead of hand-rolling a win32 case fold here. It folds case
|
|
44
|
+
// exactly where the filesystem does and nowhere else, which a
|
|
45
|
+
// `process.platform` test cannot express; `terragrunt.ts:90` asks the same
|
|
46
|
+
// question the same way (#1193).
|
|
47
|
+
const matchesTarget = (sourcePath) => pathsEqual(path.resolve(cwd, sourcePath.trim()), resolvedTarget);
|
|
46
48
|
for (let index = 0; index < lines.length; index++) {
|
|
47
49
|
const line = lines[index];
|
|
48
50
|
// Defense-in-depth: cap line length before regex matching. The input is
|
|
@@ -130,12 +132,6 @@ function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
|
|
|
130
132
|
}
|
|
131
133
|
return diagnostics;
|
|
132
134
|
}
|
|
133
|
-
function firstOutputLine(result) {
|
|
134
|
-
return `${result.stderr || ""}\n${result.stdout || ""}`
|
|
135
|
-
.trim()
|
|
136
|
-
.split(/\r?\n/, 1)[0]
|
|
137
|
-
.slice(0, 200);
|
|
138
|
-
}
|
|
139
135
|
const elixirCheckRunner = {
|
|
140
136
|
id: "elixir-check",
|
|
141
137
|
appliesTo: ["elixir"],
|
|
@@ -163,47 +159,36 @@ const elixirCheckRunner = {
|
|
|
163
159
|
cwd,
|
|
164
160
|
timeout: 30000,
|
|
165
161
|
});
|
|
166
|
-
if (result.error && !result.stdout && !result.stderr) {
|
|
167
|
-
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
168
|
-
}
|
|
169
162
|
const raw = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
170
163
|
const hasProjectContext = command === "mix";
|
|
171
|
-
const
|
|
164
|
+
const parsed = parseToolRun("elixir-check", {
|
|
165
|
+
result,
|
|
166
|
+
output: raw,
|
|
167
|
+
// EXIT TABLE (Elixir 1.16 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
|
|
168
|
+
exitCodes: { ran: [1, 2] },
|
|
169
|
+
}, (out) => parseElixirOutput(out, ctx.filePath, cwd).map((d) => hasProjectContext
|
|
172
170
|
? d
|
|
173
171
|
: {
|
|
174
172
|
...d,
|
|
175
173
|
// A direct elixirc invocation cannot resolve Mix dependencies.
|
|
176
174
|
// Standalone findings inform the agent, but cannot block it.
|
|
177
175
|
semantic: "warning",
|
|
178
|
-
});
|
|
179
|
-
if (
|
|
180
|
-
|
|
176
|
+
}));
|
|
177
|
+
if (parsed.skipped)
|
|
178
|
+
return parsed.skipped;
|
|
179
|
+
return finishParsedRun({
|
|
180
|
+
tool: "elixir-check",
|
|
181
|
+
ctx,
|
|
182
|
+
result,
|
|
183
|
+
diagnostics: parsed.diagnostics,
|
|
184
|
+
classify: (diagnostics) => {
|
|
185
|
+
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
181
186
|
return {
|
|
182
|
-
status: "failed",
|
|
183
|
-
|
|
184
|
-
{
|
|
185
|
-
id: "elixir-check-nonzero-no-diagnostics",
|
|
186
|
-
message: firstOutputLine(result) ||
|
|
187
|
-
`${command} exited non-zero without structured diagnostics`,
|
|
188
|
-
filePath: ctx.filePath,
|
|
189
|
-
severity: "error",
|
|
190
|
-
semantic: hasProjectContext ? "blocking" : "warning",
|
|
191
|
-
tool: "elixir-check",
|
|
192
|
-
rule: command,
|
|
193
|
-
fixable: false,
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
semantic: hasProjectContext ? "blocking" : "warning",
|
|
187
|
+
status: hasBlocking ? "failed" : "succeeded",
|
|
188
|
+
semantic: hasBlocking ? "blocking" : "warning",
|
|
197
189
|
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
202
|
-
return {
|
|
203
|
-
status: hasBlocking ? "failed" : "succeeded",
|
|
204
|
-
diagnostics,
|
|
205
|
-
semantic: hasBlocking ? "blocking" : "warning",
|
|
206
|
-
};
|
|
190
|
+
},
|
|
191
|
+
});
|
|
207
192
|
},
|
|
208
193
|
};
|
|
209
194
|
export default elixirCheckRunner;
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Gate: skips when no ESLint config is detected (project uses Biome/OxLint instead).
|
|
8
8
|
*/
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
9
11
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
10
12
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
11
13
|
import { getAutofixCapability, hasEslintConfig } from "../../tool-policy.js";
|
|
12
14
|
import { PRIORITY } from "../priorities.js";
|
|
13
15
|
import { createCwdCachedProbe, resolveToolCommand, } from "./utils/runner-helpers.js";
|
|
16
|
+
import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
|
|
14
17
|
const ESLINT_PROBE_BUDGET_MS = 5000;
|
|
15
18
|
// Per-cwd cached eslint `--version` verification (#120). Before this, every
|
|
16
19
|
// dispatch invocation ran a fresh `safeSpawnAsync(cmd, ["--version"])` after
|
|
@@ -40,12 +43,18 @@ function getEslintProbe(cmd) {
|
|
|
40
43
|
eslintAvailabilityByCmd.set(cmd, created);
|
|
41
44
|
return created;
|
|
42
45
|
}
|
|
43
|
-
function parseEslintJson(raw, filePath) {
|
|
46
|
+
function parseEslintJson(raw, filePath, cwd) {
|
|
44
47
|
try {
|
|
45
48
|
const results = JSON.parse(raw);
|
|
46
49
|
const autofix = getAutofixCapability("eslint");
|
|
47
50
|
const diagnostics = [];
|
|
51
|
+
const absTarget = path.resolve(cwd, filePath);
|
|
48
52
|
for (const fileResult of results) {
|
|
53
|
+
// #3295: flat config `files`/`ignores` and a directory argv both put a
|
|
54
|
+
// SECOND result in this array; each names its own `filePath`.
|
|
55
|
+
if (fileResult.filePath &&
|
|
56
|
+
!pathsEqual(path.resolve(cwd, fileResult.filePath), absTarget))
|
|
57
|
+
continue;
|
|
49
58
|
for (const msg of fileResult.messages) {
|
|
50
59
|
const severity = msg.severity === 2 ? "error" : "warning";
|
|
51
60
|
diagnostics.push({
|
|
@@ -92,11 +101,11 @@ const eslintRunner = {
|
|
|
92
101
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
93
102
|
}
|
|
94
103
|
const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-error-on-unmatched-pattern", ctx.filePath], { timeout: 30000, cwd });
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
// Exit table: 0 is clean-or-findings, and 1/2 are ran outcomes whose
|
|
105
|
+
// JSON parser decides whether findings or a parse/config error reached the
|
|
106
|
+
// user. In particular, status 2 carries file-local fatal parse messages
|
|
107
|
+
// as JSON and must not be discarded before parsing.
|
|
108
|
+
//
|
|
100
109
|
// ESLint exits 0 whenever nothing reached ERROR severity — that
|
|
101
110
|
// includes a run that found only warnings (#1954), which also prints a
|
|
102
111
|
// full JSON report. So parse stdout unconditionally and branch on the
|
|
@@ -109,46 +118,20 @@ const eslintRunner = {
|
|
|
109
118
|
if (raw.length === 0 && result.status !== 0) {
|
|
110
119
|
raw = result.stderr || "";
|
|
111
120
|
}
|
|
112
|
-
const parsed =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
severity: "warning",
|
|
127
|
-
semantic: "warning",
|
|
128
|
-
tool: "eslint",
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
semantic: "warning",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const diagnostics = parsed.diagnostics;
|
|
135
|
-
if (diagnostics.length === 0) {
|
|
136
|
-
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
137
|
-
}
|
|
138
|
-
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
|
|
139
|
-
// A warning-only result on exit 0 is ESLint's normal outcome, not a
|
|
140
|
-
// failure: exit 0 means nothing hit ERROR severity. Keying status off
|
|
141
|
-
// blocking severity plus exit code (the sibling convention from oxlint,
|
|
142
|
-
// biome-check, golangci-lint, rubocop) keeps plan.ts's fallback group
|
|
143
|
-
// ["eslint", "oxlint", "biome-check-json"] stopping at eslint instead
|
|
144
|
-
// of re-running oxlint and biome-check-json on every warning-only save.
|
|
145
|
-
// The findings reach delivery regardless of status — dispatcher.ts
|
|
146
|
-
// buckets by each diagnostic's own `semantic`.
|
|
147
|
-
return {
|
|
148
|
-
status: !hasErrors && result.status === 0 ? "succeeded" : "failed",
|
|
149
|
-
diagnostics,
|
|
150
|
-
semantic: hasErrors ? "blocking" : "warning",
|
|
151
|
-
};
|
|
121
|
+
const parsed = parseToolRun("eslint", {
|
|
122
|
+
result,
|
|
123
|
+
output: raw,
|
|
124
|
+
// EXIT TABLE (ESLint 9.10 docs https://eslint.org/docs/latest/use/command-line-interface): 0 clean; 1 findings; 2 fatal findings/error; other nonzero rejected.
|
|
125
|
+
exitCodes: { ran: [1, 2] },
|
|
126
|
+
}, (rawOutput) => parseEslintJson(rawOutput, ctx.filePath, cwd).diagnostics);
|
|
127
|
+
if (parsed.skipped)
|
|
128
|
+
return parsed.skipped;
|
|
129
|
+
return finishParsedRun({
|
|
130
|
+
tool: "eslint",
|
|
131
|
+
ctx,
|
|
132
|
+
result,
|
|
133
|
+
diagnostics: parsed.diagnostics,
|
|
134
|
+
});
|
|
152
135
|
},
|
|
153
136
|
};
|
|
154
137
|
export default eslintRunner;
|
|
@@ -1,28 +1,108 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
1
3
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
4
|
+
import { stripAnsi } from "../../sanitize.js";
|
|
2
5
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
3
6
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
4
7
|
import { PRIORITY } from "../priorities.js";
|
|
5
8
|
const gleam = createAvailabilityChecker("gleam", ".exe");
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* gleam renders every located diagnostic through
|
|
11
|
+
* `codespan_reporting::term::emit` (v1.18.1
|
|
12
|
+
* `compiler-core/src/diagnostic.rs:92-133`, unchanged since v1.6.3), so its
|
|
13
|
+
* location line is codespan's LOCUS line, not a bare `path:line:col`:
|
|
14
|
+
*
|
|
15
|
+
* ```text
|
|
16
|
+
* error: Type mismatch
|
|
17
|
+
* ┌─ /abs/proj/src/app.gleam:1:8
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `<outer gutter spaces> ┌─ <name>:<line>:<column>` —
|
|
21
|
+
* codespan-reporting 0.13.1 `src/term/renderer.rs:379-396` (the gutter, then
|
|
22
|
+
* `chars().snippet_start`, then one space, then the locus) and `:869-878`
|
|
23
|
+
* (`{name}:{line_number}:{column_number}`), with `snippet_start` = `┌─` in the
|
|
24
|
+
* default box-drawing set (`src/term/config.rs:419-429`, reached because gleam
|
|
25
|
+
* passes `Config::default()`, whose display style is Rich).
|
|
26
|
+
*
|
|
27
|
+
* `name` is `location.path` verbatim (`diagnostic.rs:92-94`,
|
|
28
|
+
* `files.add(main_location_path, …)`), and the CLI builds that path by walking
|
|
29
|
+
* UP from the absolute `std::env::current_dir()` to the nearest `gleam.toml`
|
|
30
|
+
* (`compiler-cli/src/fs.rs:32-62`) and joining `src`/`test`
|
|
31
|
+
* (`compiler-core/src/paths.rs:42-48`) before enumerating the directory — so it
|
|
32
|
+
* is ABSOLUTE, with host-native separators. Resolving it against the cwd the
|
|
33
|
+
* tool ran in is therefore a no-op today and the ADR 0009 shape for the
|
|
34
|
+
* hypothetical relative spelling.
|
|
35
|
+
*
|
|
36
|
+
* Capturing the path REQUIRES matching the gutter: the pre-#3285 regex
|
|
37
|
+
* `/^(.+?):(\d+):(\d+)$/` captured ` ┌─ /abs/proj/src/app.gleam`, gutter
|
|
38
|
+
* included, and only an `endsWith` compare tolerated that. An equality
|
|
39
|
+
* predicate (the seam every other runner now uses) drops every gleam diagnostic
|
|
40
|
+
* unless the gutter is out of the capture — which is why PR #3284 reverted its
|
|
41
|
+
* own fold here and filed #3285.
|
|
42
|
+
*/
|
|
43
|
+
const GLEAM_LOCUS = /^\s*┌─\s+(.+?):(\d+):(\d+)$/;
|
|
44
|
+
const GLEAM_HEADER = /^\s*(error|warning):\s*(.+?)\s*$/;
|
|
45
|
+
const GLEAM_LABEL = /^\s*│\s*\^+\s*(.*?)\s*$/;
|
|
46
|
+
/**
|
|
47
|
+
* codespan writes ONE title line per diagnostic and then everything that
|
|
48
|
+
* belongs to it — locus, borders, snippet, labels, notes — before the next
|
|
49
|
+
* title (`codespan-reporting` 0.13.1 `src/term/renderer.rs:141-210`). Splitting
|
|
50
|
+
* on the title is therefore the diagnostic boundary, and it is what keeps an
|
|
51
|
+
* adjacent diagnostic's label out of its predecessor's message. Lines before
|
|
52
|
+
* the first title are gleam's own progress output (`Compiling demo`), which
|
|
53
|
+
* belongs to no diagnostic.
|
|
54
|
+
*/
|
|
55
|
+
function gleamBlocks(lines) {
|
|
56
|
+
const blocks = [];
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const header = line.match(GLEAM_HEADER);
|
|
59
|
+
if (header)
|
|
60
|
+
blocks.push({ header, body: [] });
|
|
61
|
+
else
|
|
62
|
+
blocks.at(-1)?.body.push(line);
|
|
63
|
+
}
|
|
64
|
+
return blocks;
|
|
65
|
+
}
|
|
66
|
+
function gleamMessage(title, body) {
|
|
67
|
+
const labels = body.flatMap((line) => line.match(GLEAM_LABEL)?.[1]?.trim() || []);
|
|
68
|
+
return labels.length > 0 ? `${title} — ${labels.join(" ")}` : title;
|
|
69
|
+
}
|
|
70
|
+
function parseGleamOutput(raw, filePath, cwd) {
|
|
7
71
|
const diagnostics = [];
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
72
|
+
const absTarget = path.resolve(cwd, filePath);
|
|
73
|
+
// `stripAnsi` (the seam `ruff.ts`, `go-vet.ts` and `rust-clippy.ts` already
|
|
74
|
+
// use): gleam colours the gutter whenever `FORCE_COLOR` is set non-empty,
|
|
75
|
+
// whatever stderr is (`compiler-cli/src/cli.rs:194-207` — `ColorChoice::Never`
|
|
76
|
+
// only when it is neither forced nor a terminal), and the SGR bytes sit
|
|
77
|
+
// between the line start and `┌─`. The old suffix compare never saw the
|
|
78
|
+
// prefix; an anchored capture would drop every diagnostic in that
|
|
79
|
+
// environment.
|
|
80
|
+
const lines = stripAnsi(raw).split(/\r?\n/);
|
|
81
|
+
for (const { header, body } of gleamBlocks(lines)) {
|
|
82
|
+
const severity = header[1] === "warning" ? "warning" : "error";
|
|
83
|
+
const title = `${header[1]}: ${header[2]}`;
|
|
84
|
+
// A Gleam diagnostic with NO locus describes the whole project: `check`
|
|
85
|
+
// receives no file argument (`Diagnostic::write_title`, no codespan emit).
|
|
86
|
+
// Leave it for the nonzero-without-diagnostics fallback instead of
|
|
87
|
+
// charging it to whichever file happened to trigger dispatch — ADR 0009:
|
|
88
|
+
// without a reported path there is no evidence it is about this file.
|
|
89
|
+
const location = body
|
|
90
|
+
.find((line) => GLEAM_LOCUS.test(line))
|
|
91
|
+
?.match(GLEAM_LOCUS);
|
|
11
92
|
if (!location)
|
|
12
93
|
continue;
|
|
13
94
|
const [, sourcePath, lineStr, colStr] = location;
|
|
14
|
-
|
|
95
|
+
// #3278: one seam for reported-path attribution — see javac.ts.
|
|
96
|
+
if (!pathsEqual(path.resolve(cwd, sourcePath.trim()), absTarget))
|
|
15
97
|
continue;
|
|
16
|
-
}
|
|
17
|
-
const message = lines.slice(i + 1).find((line) => line.trim().length > 0);
|
|
18
98
|
diagnostics.push({
|
|
19
99
|
id: `gleam-check-${lineStr}-${colStr}`,
|
|
20
|
-
message:
|
|
100
|
+
message: gleamMessage(title, body),
|
|
21
101
|
filePath,
|
|
22
102
|
line: Number.parseInt(lineStr, 10) || 1,
|
|
23
103
|
column: Number.parseInt(colStr, 10) || 1,
|
|
24
|
-
severity
|
|
25
|
-
semantic: "blocking",
|
|
104
|
+
severity,
|
|
105
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
26
106
|
tool: "gleam",
|
|
27
107
|
rule: "gleam-check",
|
|
28
108
|
fixable: false,
|
|
@@ -54,7 +134,7 @@ const gleamCheckRunner = {
|
|
|
54
134
|
if (result.error && !result.stdout && !result.stderr) {
|
|
55
135
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
56
136
|
}
|
|
57
|
-
const diagnostics = parseGleamOutput(`${result.stderr || ""}\n${result.stdout || ""}`, ctx.filePath);
|
|
137
|
+
const diagnostics = parseGleamOutput(`${result.stderr || ""}\n${result.stdout || ""}`, ctx.filePath, cwd);
|
|
58
138
|
if (diagnostics.length === 0) {
|
|
59
139
|
if (result.status && result.status !== 0) {
|
|
60
140
|
return {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { relative, resolve, sep, posix } from "node:path";
|
|
7
7
|
import { goClient } from "../../go-client.js";
|
|
8
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
8
9
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
9
10
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
10
11
|
import { stripAnsi } from "../../sanitize.js";
|
|
@@ -71,15 +72,28 @@ const goVetRunner = {
|
|
|
71
72
|
// cwd before comparing, so go's path FORM (a leading `./` for the
|
|
72
73
|
// module-root package, an absolute path, `.` segments) can't cause the
|
|
73
74
|
// edited file's OWN diagnostics to be silently dropped.
|
|
75
|
+
//
|
|
76
|
+
// #3277: resolving is not enough — the two sides are SPELLED by
|
|
77
|
+
// different actors. `ctx.filePath` is `normalizeMapKey`-canonical
|
|
78
|
+
// (#2016) while go prints its own spelling: `cmd/go` rewrites the
|
|
79
|
+
// package-dir prefix to a cwd-relative one only when the rewrite
|
|
80
|
+
// matches byte-for-byte (`replacePrefix`), even though the check that
|
|
81
|
+
// authorized it (`filepath.Rel`) compares case-insensitively on
|
|
82
|
+
// Windows, so a differently-cased ABSOLUTE path reaches this filter.
|
|
83
|
+
// A bare `===` then drops every line for the edited file and the run
|
|
84
|
+
// is reported clean — the #209 defect. `pathsEqual` asks the
|
|
85
|
+
// filesystem instead of a platform list, so it is also right on a
|
|
86
|
+
// case-folding POSIX mount. It does NOT resolve symlinks on POSIX
|
|
87
|
+
// (`clients/path-utils.ts:181`); nothing here relies on that.
|
|
74
88
|
const absTarget = resolve(ctx.filePath);
|
|
75
89
|
const relevant = raw
|
|
76
90
|
.split("\n")
|
|
77
91
|
.filter((line) => {
|
|
78
92
|
const m = line.match(/^(.+?):(\d+):(\d+):\s*(.+)/);
|
|
79
|
-
return m != null && resolve(cwd, m[1].trim())
|
|
93
|
+
return m != null && pathsEqual(resolve(cwd, m[1].trim()), absTarget);
|
|
80
94
|
})
|
|
81
95
|
.join("\n");
|
|
82
|
-
const diagnostics = parseGoVetOutput(relevant, ctx.filePath);
|
|
96
|
+
const diagnostics = parseGoVetOutput(relevant, ctx.filePath, cwd);
|
|
83
97
|
// Edited file clean → succeeded: a sibling-file error no longer flags
|
|
84
98
|
// the edited file's turn (it surfaces when that file is itself edited).
|
|
85
99
|
return diagnostics.length > 0
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
14
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
14
15
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
15
16
|
import { getLinterPolicyForCwd, hasGolangciConfig } from "../../tool-policy.js";
|
|
16
17
|
import { PRIORITY } from "../priorities.js";
|
|
17
18
|
import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
|
|
19
|
+
import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
|
|
18
20
|
const golangci = createAvailabilityChecker("golangci-lint", ".exe");
|
|
19
21
|
/**
|
|
20
22
|
* Summarize a golangci-lint Replacement into a single-line fixSuggestion.
|
|
@@ -39,13 +41,21 @@ function describeReplacement(replacement) {
|
|
|
39
41
|
}
|
|
40
42
|
return "Apply golangci-lint suggested fix";
|
|
41
43
|
}
|
|
42
|
-
function parseGolangciJson(raw, filePath) {
|
|
44
|
+
function parseGolangciJson(raw, filePath, cwd) {
|
|
43
45
|
try {
|
|
44
46
|
const output = JSON.parse(raw);
|
|
45
47
|
if (!output.Issues)
|
|
46
48
|
return [];
|
|
47
|
-
const absFile = path.resolve(filePath);
|
|
48
|
-
|
|
49
|
+
const absFile = path.resolve(cwd, filePath);
|
|
50
|
+
// #3278: one seam for reported-path attribution — see javac.ts. The base
|
|
51
|
+
// matters here more than anywhere else in the family: golangci-lint's
|
|
52
|
+
// `PathPrettifier` OVERWRITES `Pos.Filename` with `filepath.Rel(basePath,
|
|
53
|
+
// …)` before the JSON printer sees it (v1.64.8,
|
|
54
|
+
// `pkg/result/processors/path_prettifier.go:31` +
|
|
55
|
+
// `path_relativity.go:43`), so the spelling is relative to the child's
|
|
56
|
+
// base path — the `cwd` we spawned it in — and `path.resolve` with no
|
|
57
|
+
// base resolved it against the EXTENSION's cwd instead.
|
|
58
|
+
return output.Issues.filter((issue) => pathsEqual(path.resolve(cwd, issue.Pos.Filename), absFile)).map((issue) => {
|
|
49
59
|
const severity = issue.Severity === "error" ? "error" : "warning";
|
|
50
60
|
// golangci-lint's --out-format=json emits a Replacement object per
|
|
51
61
|
// issue when `golangci-lint run --fix` would deterministically
|
|
@@ -95,26 +105,31 @@ const golangciRunner = {
|
|
|
95
105
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
96
106
|
// Run on the specific file. golangci-lint accepts file paths directly.
|
|
97
107
|
const result = await safeSpawnAsync(cmd, ["run", "--out-format=json", ctx.filePath], { timeout: 60000, cwd });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
// Exit table: 0 is clean-or-findings; 1 means issues, 3 failure,
|
|
109
|
+
// 4 timeout, and 5 missing config. Every nonzero code is a ran outcome
|
|
110
|
+
// whose parser decides: JSON issues remain findings, while emitted
|
|
111
|
+
// non-JSON text becomes a parse-error diagnostic and no output stays
|
|
112
|
+
// skipped. Keep stderr as the parse input when stdout is absent so tool
|
|
113
|
+
// errors cannot be mistaken for a clean file.
|
|
114
|
+
const raw = (result.stdout ?? "").length > 0
|
|
115
|
+
? (result.stdout ?? "")
|
|
116
|
+
: result.status !== 0
|
|
117
|
+
? result.stderr || ""
|
|
118
|
+
: "";
|
|
119
|
+
const parsed = parseToolRun("golangci-lint", {
|
|
120
|
+
result,
|
|
121
|
+
output: raw,
|
|
122
|
+
// EXIT TABLE (golangci-lint 1.60 docs https://golangci-lint.run/docs/welcome/quick-start/): 0 clean; 1 findings; 2 error; 3 error; 4 error; 5 error; other nonzero rejected.
|
|
123
|
+
exitCodes: { ran: [1, 2, 3, 4, 5] },
|
|
124
|
+
}, (raw) => parseGolangciJson(raw, ctx.filePath, cwd));
|
|
125
|
+
if (parsed.skipped)
|
|
126
|
+
return parsed.skipped;
|
|
127
|
+
return finishParsedRun({
|
|
128
|
+
tool: "golangci-lint",
|
|
129
|
+
ctx,
|
|
130
|
+
result,
|
|
131
|
+
diagnostics: parsed.diagnostics,
|
|
132
|
+
});
|
|
118
133
|
},
|
|
119
134
|
};
|
|
120
135
|
export default golangciRunner;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
2
3
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
3
4
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
4
5
|
import { getLinterPolicyForCwd } from "../../tool-policy.js";
|
|
@@ -7,25 +8,32 @@ import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } fro
|
|
|
7
8
|
import { parseToolRun } from "./utils/tool-failure.js";
|
|
8
9
|
import { finishParsedRun } from "./utils/tool-failure.js";
|
|
9
10
|
const hadolint = createAvailabilityChecker("hadolint", ".exe");
|
|
10
|
-
function parseHadolintOutput(raw, filePath) {
|
|
11
|
+
function parseHadolintOutput(raw, filePath, cwd) {
|
|
11
12
|
try {
|
|
12
13
|
const parsed = JSON.parse(raw);
|
|
13
14
|
if (!Array.isArray(parsed))
|
|
14
15
|
return [];
|
|
15
|
-
|
|
16
|
+
const absTarget = path.resolve(cwd, filePath);
|
|
17
|
+
return parsed.flatMap((item) => {
|
|
18
|
+
// #3295: hadolint takes a LIST of Dockerfiles and names each finding's
|
|
19
|
+
// own `file`.
|
|
20
|
+
if (item.file && !pathsEqual(path.resolve(cwd, item.file), absTarget))
|
|
21
|
+
return [];
|
|
16
22
|
const severity = item.level === "error" ? "error" : "warning";
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: `hadolint-${item.code}-${item.line}`,
|
|
26
|
+
message: `[${item.code}] ${item.message}`,
|
|
27
|
+
filePath,
|
|
28
|
+
line: item.line,
|
|
29
|
+
column: item.column ?? 1,
|
|
30
|
+
severity,
|
|
31
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
32
|
+
tool: "hadolint",
|
|
33
|
+
rule: item.code,
|
|
34
|
+
fixable: false,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
catch {
|
|
@@ -57,7 +65,11 @@ const hadolintRunner = {
|
|
|
57
65
|
// #1948: hadolint runs with `--no-fail`, so it exits 0 even when it finds
|
|
58
66
|
// something. A nonzero exit therefore means hadolint itself failed, and
|
|
59
67
|
// zero parsed diagnostics out of whatever it printed is a parser break.
|
|
60
|
-
const run = parseToolRun("hadolint", {
|
|
68
|
+
const run = parseToolRun("hadolint", {
|
|
69
|
+
result,
|
|
70
|
+
// EXIT TABLE (Hadolint 2.12 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
|
|
71
|
+
exitCodes: { ran: [1, 2] },
|
|
72
|
+
}, (out) => parseHadolintOutput(out, ctx.filePath, cwd));
|
|
61
73
|
if (run.skipped)
|
|
62
74
|
return run.skipped;
|
|
63
75
|
const diagnostics = run.diagnostics;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { pathsEqual } from "../../path-utils.js";
|
|
2
3
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
3
4
|
import { resolveRunnerCwd } from "../../tool-cwd.js";
|
|
4
5
|
import { getLinterPolicyForCwd } from "../../tool-policy.js";
|
|
@@ -15,19 +16,22 @@ const HTMLHINT_RULES = {
|
|
|
15
16
|
"spec-char-escape": true,
|
|
16
17
|
"id-unique": true,
|
|
17
18
|
};
|
|
18
|
-
function parseHtmlhintOutput(raw, filePath) {
|
|
19
|
+
function parseHtmlhintOutput(raw, filePath, cwd) {
|
|
19
20
|
const diagnostics = [];
|
|
21
|
+
const absTarget = path.resolve(cwd, filePath);
|
|
20
22
|
// unix format: "file:line:col: message [severity/rule]"
|
|
21
|
-
const lineRe =
|
|
23
|
+
const lineRe = /^(.+?):(\d+):(\d+): (.+?) \[(error|warning)\/([^\]]+)\]/;
|
|
22
24
|
for (const line of raw.split("\n")) {
|
|
23
25
|
const match = line.match(lineRe);
|
|
24
26
|
if (!match)
|
|
25
27
|
continue;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
28
|
+
if (!pathsEqual(path.resolve(cwd, match[1]), absTarget))
|
|
29
|
+
continue;
|
|
30
|
+
const lineNum = parseInt(match[2], 10);
|
|
31
|
+
const col = parseInt(match[3], 10);
|
|
32
|
+
const message = match[4].trim();
|
|
33
|
+
const level = match[5];
|
|
34
|
+
const rule = match[6].trim();
|
|
31
35
|
const severity = level === "error" ? "error" : "warning";
|
|
32
36
|
diagnostics.push({
|
|
33
37
|
id: `htmlhint-${rule}-${lineNum}`,
|
|
@@ -86,7 +90,12 @@ const htmlhintRunner = {
|
|
|
86
90
|
// #1948: htmlhint exits 1 when it finds errors and prints them in `unix`
|
|
87
91
|
// format on stdout. Zero parsed out of a nonzero exit is a parser break.
|
|
88
92
|
const output = result.stdout || result.stderr || "";
|
|
89
|
-
const run = parseToolRun("htmlhint", {
|
|
93
|
+
const run = parseToolRun("htmlhint", {
|
|
94
|
+
result,
|
|
95
|
+
output,
|
|
96
|
+
// EXIT TABLE (HTMLHint 1.1 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
|
|
97
|
+
exitCodes: { ran: [1, 2] },
|
|
98
|
+
}, (out) => parseHtmlhintOutput(out, ctx.filePath, cwd));
|
|
90
99
|
if (run.skipped)
|
|
91
100
|
return run.skipped;
|
|
92
101
|
const diagnostics = run.diagnostics;
|