@herbertgao/pi-extensions 2026.8.7 → 2026.8.8
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/README.md +7 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +6 -5
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +19 -4
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +9 -9
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body degradation for a DEMOTED finding (#1944).
|
|
3
|
+
*
|
|
4
|
+
* The demotion gates (#1631 dependency-drift, #1641 past-EOF) move a record
|
|
5
|
+
* out of the authoritative blocker channel and into the advisory channel.
|
|
6
|
+
* Until this module existed they changed the CHANNEL and nothing else: the
|
|
7
|
+
* advisory embedded the blocker body verbatim, so the agent still read
|
|
8
|
+
* "🔴 STOP — 11 issue(s) must be fixed" with dead line numbers under two
|
|
9
|
+
* hedge lines it ignored. Live evidence (session 01a0234c, 2026-08-21): the
|
|
10
|
+
* agent spent four re-run detours and about 7.4k tokens on a record whose
|
|
11
|
+
* coordinates no longer existed, then explained an unrelated REAL blocker
|
|
12
|
+
* away as "mid-edit state" in the same session.
|
|
13
|
+
*
|
|
14
|
+
* The rule this module owns, once, for every gated surface: a demoted body
|
|
15
|
+
* loses its authority vocabulary and its dead coordinates, and it says so in
|
|
16
|
+
* its own words. `clients/finding-delivery-gate.ts` documents the surrounding
|
|
17
|
+
* contract; this is the one implementation of its "Demotion rendering" arm
|
|
18
|
+
* for surfaces that carry a RENDERED BODY rather than a per-diagnostic row.
|
|
19
|
+
* Per-row surfaces (`widget-state.ts`, `tools/lens-diagnostics.ts`) already
|
|
20
|
+
* swap the coordinate for `PAST_EOF_STALE_MARKER` at render time and have no
|
|
21
|
+
* body to degrade.
|
|
22
|
+
*
|
|
23
|
+
* Pure and leaf: no store, no coordinator, no I/O. The caller supplies the
|
|
24
|
+
* dead lines it already computed while deciding to demote, so this module
|
|
25
|
+
* never re-derives a verdict from rendered prose (the
|
|
26
|
+
* re-derivation-vs-correlation screen).
|
|
27
|
+
*/
|
|
28
|
+
/** Suffix that replaces a cited coordinate the file no longer has. */
|
|
29
|
+
export const DEAD_LINE_ANNOTATION = "(line no longer exists)";
|
|
30
|
+
/**
|
|
31
|
+
* Authority vocabulary a demoted body must not carry, with its degraded
|
|
32
|
+
* replacement. Ordered: the full banner rewrite runs first so the narrower
|
|
33
|
+
* fallbacks only ever see text the banner rule did not already cover.
|
|
34
|
+
*
|
|
35
|
+
* This table is the single source of truth for the rule. A surface must not
|
|
36
|
+
* hand-roll its own "strip the STOP" pass — that is how #1631, #1641, and
|
|
37
|
+
* #1664 each ended up with a different answer to the same question.
|
|
38
|
+
*
|
|
39
|
+
* POSITION-ANCHORED (#1944 review F1). These patterns run over the BANNER
|
|
40
|
+
* region only, never the whole body. Diagnostic messages are arbitrary text
|
|
41
|
+
* from arbitrary runners, and they legitimately contain both tokens: the
|
|
42
|
+
* reviewer's probe turned ts(2304) `Cannot find name 'STOP'` into `Cannot
|
|
43
|
+
* find name 'Previously flagged'`, and mangled an English "must be fixed"
|
|
44
|
+
* inside a rule message. Rewriting a finding's own words is a worse defect
|
|
45
|
+
* than the banner it was cleaning up. See `splitBannerRegion`.
|
|
46
|
+
*/
|
|
47
|
+
const AUTHORITY_REWRITES = [
|
|
48
|
+
[
|
|
49
|
+
/[^\S\n]*(?:🔴|⛔|❌)?[^\S\n]*STOP[^\S\n]*[—–-][^\S\n]*(\d+)[^\S\n]+issue\(s\)[^\S\n]+must be fixed:?/g,
|
|
50
|
+
"$1 issue(s) were flagged before this file changed; the coordinates below may no longer exist:",
|
|
51
|
+
],
|
|
52
|
+
[/(?:🔴|⛔|❌)[^\S\n]*STOP\b[^\S\n]*[—–-]?/g, "Previously flagged —"],
|
|
53
|
+
[/\bSTOP\b/g, "Previously flagged"],
|
|
54
|
+
[/\bmust be fixed\b/g, "were flagged"],
|
|
55
|
+
];
|
|
56
|
+
/** ` L310: message` / `L310 message` at the head of a rendered row. */
|
|
57
|
+
const CITED_LINE_RE = /^([^\S\n]*)L(\d+)\b/;
|
|
58
|
+
/**
|
|
59
|
+
* Split a rendered body into its BANNER region and its CONTENT region
|
|
60
|
+
* (#1944 review F1).
|
|
61
|
+
*
|
|
62
|
+
* The body this module degrades is `InlineBlockerRecord.summary`, and its one
|
|
63
|
+
* producer is `formatDiagnostics(inlineBlockers, "blocking")`
|
|
64
|
+
* (`clients/dispatch/utils/format-utils.ts`), reaching the store as
|
|
65
|
+
* `dispatchResult.blockerOutput` -> `PipelineResult.inlineBlockerSummary` ->
|
|
66
|
+
* `recordInlineBlockers`. (`buildEnrichedBlockerOutput` in
|
|
67
|
+
* `clients/pipeline.ts` renders the same shape with code snippets, but it
|
|
68
|
+
* feeds the TOOL-RESULT output shown inline during the write, not this
|
|
69
|
+
* stored summary. #1944 review round: naming it here as a producer was
|
|
70
|
+
* wrong.) The shape is a header, then one ` L<n>: <message>` row per
|
|
71
|
+
* diagnostic, then indented continuations (`💡 Fix: …`) and an optional
|
|
72
|
+
* `… and N more` tail. So the first cited-line row is the boundary:
|
|
73
|
+
* everything above it is ours to rewrite, everything from it down is the
|
|
74
|
+
* findings' own words.
|
|
75
|
+
*
|
|
76
|
+
* A body with no cited row at all — `formatDiagnostics` omits `L<n>` for a
|
|
77
|
+
* lineless diagnostic — falls back to the first NON-BLANK line. Leading
|
|
78
|
+
* blanks must be skipped rather than counted (#1944 verify round, Edge-D):
|
|
79
|
+
* `formatDiagnostics` opens its output with a newline, so a naive "first
|
|
80
|
+
* line is the banner" rule degrades an empty string and leaves the real
|
|
81
|
+
* header at full authority. Production is currently saved from that only by
|
|
82
|
+
* an undocumented `.trim()` at `clients/pipeline.ts` (`inlineBlockerSummary`),
|
|
83
|
+
* and this module claims to own the rule, so it must hold on its own input.
|
|
84
|
+
*/
|
|
85
|
+
function splitBannerRegion(lines) {
|
|
86
|
+
const firstCitedRow = lines.findIndex((line) => CITED_LINE_RE.test(line));
|
|
87
|
+
if (firstCitedRow !== -1)
|
|
88
|
+
return { bannerEnd: firstCitedRow };
|
|
89
|
+
const firstNonBlank = lines.findIndex((line) => line.trim() !== "");
|
|
90
|
+
return { bannerEnd: firstNonBlank === -1 ? 0 : firstNonBlank + 1 };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Degrade one demoted finding's rendered body.
|
|
94
|
+
*
|
|
95
|
+
* Two independent transforms, deliberately separable so each can be proven by
|
|
96
|
+
* its own test (and so deleting either one reds a different assertion):
|
|
97
|
+
*
|
|
98
|
+
* 1. Authority vocabulary: the STOP banner and the "must be fixed"
|
|
99
|
+
* imperative are rewritten to past-tense, non-directive wording.
|
|
100
|
+
* 2. Dead coordinates: every cited `L<n>` whose line the file no longer has
|
|
101
|
+
* renders as `L<n> (line no longer exists)`. The message text stays —
|
|
102
|
+
* demote, never drop (#1419).
|
|
103
|
+
*/
|
|
104
|
+
export function degradeDemotedFindingBody(summary, options = {}) {
|
|
105
|
+
const deadLines = new Set(options.deadLines ?? []);
|
|
106
|
+
let authorityMarkersRemoved = 0;
|
|
107
|
+
const sourceLines = summary.split("\n");
|
|
108
|
+
const { bannerEnd } = splitBannerRegion(sourceLines);
|
|
109
|
+
const degradedLines = sourceLines.map((line, index) => {
|
|
110
|
+
// Content rows keep every character. Only the banner region is ours.
|
|
111
|
+
if (index >= bannerEnd)
|
|
112
|
+
return line;
|
|
113
|
+
let rewritten = line;
|
|
114
|
+
for (const [pattern, replacement] of AUTHORITY_REWRITES) {
|
|
115
|
+
rewritten = rewritten.replace(pattern, (...args) => {
|
|
116
|
+
authorityMarkersRemoved += 1;
|
|
117
|
+
// Expand `$n` against this match's capture groups by hand. A nested
|
|
118
|
+
// `String.replace` with the SAME global regex would clobber the
|
|
119
|
+
// outer call's `lastIndex` mid-iteration.
|
|
120
|
+
return replacement.replace(/\$(\d)/g, (_, digit) => String(args[Number(digit)] ?? ""));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return rewritten;
|
|
124
|
+
});
|
|
125
|
+
let body = degradedLines.join("\n");
|
|
126
|
+
const deadLinesAnnotated = [];
|
|
127
|
+
if (deadLines.size > 0) {
|
|
128
|
+
body = degradedLines
|
|
129
|
+
.map((line) => {
|
|
130
|
+
const hit = CITED_LINE_RE.exec(line);
|
|
131
|
+
if (!hit)
|
|
132
|
+
return line;
|
|
133
|
+
const cited = Number.parseInt(hit[2] ?? "", 10);
|
|
134
|
+
if (!Number.isFinite(cited) || !deadLines.has(cited))
|
|
135
|
+
return line;
|
|
136
|
+
deadLinesAnnotated.push(cited);
|
|
137
|
+
return `${hit[1] ?? ""}L${cited} ${DEAD_LINE_ANNOTATION}${line.slice(hit[0].length)}`;
|
|
138
|
+
})
|
|
139
|
+
.join("\n");
|
|
140
|
+
}
|
|
141
|
+
return { body, authorityMarkersRemoved, deadLinesAnnotated };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The one-line note a surface appends when it RETIRES a demoted finding after
|
|
145
|
+
* its single degraded delivery. Rendered inside the advisory itself, so an
|
|
146
|
+
* empty advisory section can only ever mean "nothing to say" — if something
|
|
147
|
+
* was dropped, this line is in the payload (#1944 AC3).
|
|
148
|
+
*/
|
|
149
|
+
export function formatRetirementNote(deadLines) {
|
|
150
|
+
const cited = deadLines.length > 0
|
|
151
|
+
? ` (cited ${deadLines.length === 1 ? "line" : "lines"} ${deadLines.join(", ")})`
|
|
152
|
+
: "";
|
|
153
|
+
return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
|
|
154
|
+
"so this finding cannot be re-confirmed and will not be served again.");
|
|
155
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized accessor for `@earendil-works/pi-tui
|
|
3
|
-
*
|
|
2
|
+
* Centralized accessor for `@earendil-works/pi-tui`, routed through here for a
|
|
3
|
+
* uniform dep surface. pi-tui is a pi-bundled core package: the host resolves
|
|
4
|
+
* the bare specifier from its own runtime, so pi-lens declares it as an OPTIONAL
|
|
5
|
+
* peer plus a devDependency and never as a runtime dependency. A runtime
|
|
6
|
+
* dependency makes `npm install --omit=dev` vendor a private second copy that
|
|
7
|
+
* Node evaluates at import — ~97ms of the 838ms git-install module import in
|
|
8
|
+
* #1926. `scripts/lib/host-provided-deps.mjs` holds the list, and
|
|
9
|
+
* `tests/packaging.test.ts` pins the declaration.
|
|
4
10
|
*
|
|
5
11
|
* Re-export named bindings, not `export *`: with the package kept external, a
|
|
6
12
|
* wildcard re-export leaves the namespace undefined at runtime under the bundle.
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized accessor for `typebox
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Centralized accessor for `typebox`, routed through here for a uniform dep
|
|
3
|
+
* surface. typebox is a pi-bundled core package: the host resolves the bare
|
|
4
|
+
* specifier from its own runtime, so pi-lens declares it as an OPTIONAL peer
|
|
5
|
+
* plus a devDependency and never as a runtime dependency. A runtime dependency
|
|
6
|
+
* makes `npm install --omit=dev` vendor a private second copy that Node
|
|
7
|
+
* evaluates at import — 688 files, ~400ms of the 838ms git-install module
|
|
8
|
+
* import in #1926. `scripts/lib/host-provided-deps.mjs` holds the list, and
|
|
9
|
+
* `tests/packaging.test.ts` pins the declaration.
|
|
5
10
|
*
|
|
6
11
|
* Re-export named bindings, not `export *`: with the package kept external, a
|
|
7
12
|
* wildcard re-export leaves the namespace undefined at runtime under the bundle.
|
|
@@ -4,7 +4,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
4
4
|
import { getLinterPolicyForCwd } from "../../tool-policy.js";
|
|
5
5
|
import { PRIORITY } from "../priorities.js";
|
|
6
6
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
7
|
-
import {
|
|
7
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
8
8
|
const detekt = createAvailabilityChecker("detekt", ".bat");
|
|
9
9
|
const DETEKT_CONFIG_CANDIDATES = [
|
|
10
10
|
"detekt.yml",
|
|
@@ -146,11 +146,14 @@ const detektRunner = {
|
|
|
146
146
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
147
147
|
const absPath = path.resolve(cwd, ctx.filePath);
|
|
148
148
|
const result = await safeSpawnAsync(cmd, ["--input", absPath, "--config", configPath], { cwd, timeout: 60000 });
|
|
149
|
+
// #1948: detekt reports across both streams, so the gate and the parser
|
|
150
|
+
// read the same concatenated string. A nonzero exit that yields zero
|
|
151
|
+
// findings out of real output is a parser break, not clean Kotlin.
|
|
149
152
|
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const diagnostics =
|
|
153
|
+
const run = parseToolRun("detekt", { result, output: raw }, (out) => parseDetektOutput(out, ctx.filePath));
|
|
154
|
+
if (run.skipped)
|
|
155
|
+
return run.skipped;
|
|
156
|
+
const diagnostics = run.diagnostics;
|
|
154
157
|
if (diagnostics.length === 0) {
|
|
155
158
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
156
159
|
}
|
|
@@ -163,4 +166,4 @@ const detektRunner = {
|
|
|
163
166
|
},
|
|
164
167
|
};
|
|
165
168
|
export default detektRunner;
|
|
166
|
-
export {
|
|
169
|
+
export { DETEKT_FIXABLE_RULES, parseDetektOutput };
|
|
@@ -3,7 +3,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
3
3
|
import { getLinterPolicyForCwd } from "../../tool-policy.js";
|
|
4
4
|
import { PRIORITY } from "../priorities.js";
|
|
5
5
|
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
7
7
|
const hadolint = createAvailabilityChecker("hadolint", ".exe");
|
|
8
8
|
function parseHadolintOutput(raw, filePath) {
|
|
9
9
|
try {
|
|
@@ -43,7 +43,7 @@ const hadolintRunner = {
|
|
|
43
43
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
44
44
|
}
|
|
45
45
|
let cmd = null;
|
|
46
|
-
if (await
|
|
46
|
+
if (await hadolint.isAvailableAsync(cwd)) {
|
|
47
47
|
cmd = hadolint.getCommand(cwd);
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
@@ -53,11 +53,13 @@ const hadolintRunner = {
|
|
|
53
53
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
54
54
|
}
|
|
55
55
|
const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-fail", path.resolve(cwd, ctx.filePath)], { cwd });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
56
|
+
// #1948: hadolint runs with `--no-fail`, so it exits 0 even when it finds
|
|
57
|
+
// something. A nonzero exit therefore means hadolint itself failed, and
|
|
58
|
+
// zero parsed diagnostics out of whatever it printed is a parser break.
|
|
59
|
+
const run = parseToolRun("hadolint", { result }, (out) => parseHadolintOutput(out, ctx.filePath));
|
|
60
|
+
if (run.skipped)
|
|
61
|
+
return run.skipped;
|
|
62
|
+
const diagnostics = run.diagnostics;
|
|
61
63
|
if (diagnostics.length === 0) {
|
|
62
64
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
63
65
|
}
|
|
@@ -3,7 +3,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
3
3
|
import { getLinterPolicyForCwd } from "../../tool-policy.js";
|
|
4
4
|
import { PRIORITY } from "../priorities.js";
|
|
5
5
|
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
7
7
|
const htmlhint = createAvailabilityChecker("htmlhint");
|
|
8
8
|
const HTMLHINT_RULES = {
|
|
9
9
|
"tag-pair": true,
|
|
@@ -55,7 +55,7 @@ const htmlhintRunner = {
|
|
|
55
55
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
56
56
|
}
|
|
57
57
|
let cmd = null;
|
|
58
|
-
if (await
|
|
58
|
+
if (await htmlhint.isAvailableAsync(cwd)) {
|
|
59
59
|
cmd = htmlhint.getCommand(cwd);
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
@@ -72,11 +72,13 @@ const htmlhintRunner = {
|
|
|
72
72
|
"unix",
|
|
73
73
|
path.resolve(cwd, ctx.filePath),
|
|
74
74
|
], { cwd });
|
|
75
|
+
// #1948: htmlhint exits 1 when it finds errors and prints them in `unix`
|
|
76
|
+
// format on stdout. Zero parsed out of a nonzero exit is a parser break.
|
|
75
77
|
const output = result.stdout || result.stderr || "";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const diagnostics =
|
|
78
|
+
const run = parseToolRun("htmlhint", { result, output }, (out) => parseHtmlhintOutput(out, ctx.filePath));
|
|
79
|
+
if (run.skipped)
|
|
80
|
+
return run.skipped;
|
|
81
|
+
const diagnostics = run.diagnostics;
|
|
80
82
|
if (diagnostics.length === 0) {
|
|
81
83
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
82
84
|
}
|
|
@@ -2,7 +2,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
2
2
|
import { getLinterPolicyForCwd, markdownlintConfigArgs, } from "../../tool-policy.js";
|
|
3
3
|
import { PRIORITY } from "../priorities.js";
|
|
4
4
|
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
6
6
|
const markdownlint = createAvailabilityChecker("markdownlint-cli2", ".cmd");
|
|
7
7
|
// markdownlint-cli2 exit codes (its README's "Exit Codes" section): 0 = no
|
|
8
8
|
// findings, 1 = lint findings, 2 = unexpected error (unreadable config, bad
|
|
@@ -98,7 +98,7 @@ const markdownlintRunner = {
|
|
|
98
98
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
99
99
|
}
|
|
100
100
|
let cmd = null;
|
|
101
|
-
if (await
|
|
101
|
+
if (await markdownlint.isAvailableAsync(cwd)) {
|
|
102
102
|
cmd = markdownlint.getCommand(cwd);
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
@@ -118,14 +118,12 @@ const markdownlintRunner = {
|
|
|
118
118
|
// reported as a clean Markdown file. An empty result must distinguish
|
|
119
119
|
// clean from errored.
|
|
120
120
|
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return skipped;
|
|
128
|
-
const diagnostics = parseMarkdownlintOutput(raw, ctx.filePath);
|
|
121
|
+
// #1948: one seam for both gates — the tool produced nothing, and the
|
|
122
|
+
// tool produced something the parser could not read.
|
|
123
|
+
const run = parseToolRun("markdownlint", { result, output: raw, exitCodes: MARKDOWNLINT_EXIT_CODES }, (out) => parseMarkdownlintOutput(out, ctx.filePath));
|
|
124
|
+
if (run.skipped)
|
|
125
|
+
return run.skipped;
|
|
126
|
+
const diagnostics = run.diagnostics;
|
|
129
127
|
if (diagnostics.length === 0) {
|
|
130
128
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
131
129
|
}
|
|
@@ -3,7 +3,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
3
3
|
import { hasMypyConfig } from "../../tool-policy.js";
|
|
4
4
|
import { PRIORITY } from "../priorities.js";
|
|
5
5
|
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
7
7
|
const mypy = createAvailabilityChecker("mypy", "");
|
|
8
8
|
// mypy exit codes: 0 = no type errors, 1 = type errors found, 2 = a blocking
|
|
9
9
|
// error. 2 is NOT a rejected invocation on its own — mypy reports a source
|
|
@@ -67,7 +67,7 @@ const mypyRunner = {
|
|
|
67
67
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
68
68
|
}
|
|
69
69
|
let cmd = null;
|
|
70
|
-
if (await
|
|
70
|
+
if (await mypy.isAvailableAsync(cwd)) {
|
|
71
71
|
cmd = mypy.getCommand(cwd);
|
|
72
72
|
}
|
|
73
73
|
else {
|
|
@@ -81,14 +81,14 @@ const mypyRunner = {
|
|
|
81
81
|
// stdout only (see MYPY_EXIT_CODES) so an exit-2 SYNTAX error, which
|
|
82
82
|
// does write a diagnostic there, still reaches the parser.
|
|
83
83
|
const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
if (skipped)
|
|
90
|
-
return skipped;
|
|
91
|
-
const diagnostics =
|
|
84
|
+
// #1948: `parseOutput` keeps the split this runner already had — the gate
|
|
85
|
+
// judges "did it run" on stdout alone, the parser still reads both
|
|
86
|
+
// streams — so adding the parsed-nothing record does not widen the
|
|
87
|
+
// did-it-run verdict.
|
|
88
|
+
const run = parseToolRun("mypy", { result, output: result.stdout, exitCodes: MYPY_EXIT_CODES }, (out) => parseMypyOutput(out, ctx.filePath, cwd), { parseOutput: raw });
|
|
89
|
+
if (run.skipped)
|
|
90
|
+
return run.skipped;
|
|
91
|
+
const diagnostics = run.diagnostics;
|
|
92
92
|
if (diagnostics.length === 0) {
|
|
93
93
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
94
94
|
}
|
|
@@ -76,10 +76,13 @@ const oxlintRunner = {
|
|
|
76
76
|
const result = await safeSpawnAsync(cmd, args, {
|
|
77
77
|
timeout: 30000,
|
|
78
78
|
});
|
|
79
|
-
// Oxlint
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// Oxlint exits 0 whenever nothing at ERROR severity was found — that
|
|
80
|
+
// includes a run that found only warnings, its own default severity
|
|
81
|
+
// (#1947). A run that found nothing at all also exits 0, but still
|
|
82
|
+
// prints a report with an empty `diagnostics` array, so parsing
|
|
83
|
+
// unconditionally and branching on the parsed count (below) tells the
|
|
84
|
+
// two apart instead of the exit code discarding the warning case.
|
|
85
|
+
//
|
|
83
86
|
// Parse JSON output. Fall back to the unix-format parser if JSON parsing
|
|
84
87
|
// fails (older oxlint versions, malformed stderr noise, etc.) — keeps the
|
|
85
88
|
// runner producing diagnostics even when the structured-fix metadata is
|
|
@@ -94,8 +97,20 @@ const oxlintRunner = {
|
|
|
94
97
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
95
98
|
}
|
|
96
99
|
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
100
|
+
// A warning-only result on exit 0 is oxlint's normal outcome, not a
|
|
101
|
+
// failure: exit 0 means nothing hit ERROR severity. `status: "failed"`
|
|
102
|
+
// here would stop this arm from reporting "succeeded", which breaks two
|
|
103
|
+
// things downstream — plan.ts's ["eslint", "oxlint", "biome-check-json"]
|
|
104
|
+
// fallback group only stops at the first `status: "succeeded"` runner
|
|
105
|
+
// (dispatcher.ts's `runGroup`), so biome-check-json would run again on
|
|
106
|
+
// every warning-only save (extra spawns, a possible install, duplicate
|
|
107
|
+
// findings); and it would mismatch the sibling convention (biome-check,
|
|
108
|
+
// golangci-lint, rubocop) of keying `status` off blocking severity, not
|
|
109
|
+
// off the tool's raw exit code. The findings themselves still reach the
|
|
110
|
+
// delivery pipeline regardless of `status` — dispatcher.ts buckets by
|
|
111
|
+
// each diagnostic's own `semantic`, so a warning stays a warning.
|
|
97
112
|
return {
|
|
98
|
-
status: "failed",
|
|
113
|
+
status: !hasBlocking && result.status === 0 ? "succeeded" : "failed",
|
|
99
114
|
diagnostics,
|
|
100
115
|
semantic: hasBlocking ? "blocking" : "warning",
|
|
101
116
|
};
|
|
@@ -3,6 +3,11 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
3
3
|
import { getLinterPolicyForCwd, hasPhpstanConfig } from "../../tool-policy.js";
|
|
4
4
|
import { PRIORITY } from "../priorities.js";
|
|
5
5
|
import { createAvailabilityChecker, resolveVendorToolCommand, } from "./utils/runner-helpers.js";
|
|
6
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
7
|
+
// phpstan's documented exit codes: 0 = no errors, 1 = errors found, 2 = a
|
|
8
|
+
// fatal/internal error that stopped the analysis. Only 1 is a run that carries
|
|
9
|
+
// findings.
|
|
10
|
+
const PHPSTAN_EXIT_CODES = { ran: [1] };
|
|
6
11
|
const phpstan = createAvailabilityChecker("phpstan", ".phar");
|
|
7
12
|
// phpstan analyses the target file PLUS its dependency closure and keys errors
|
|
8
13
|
// by the real file in `output.files`. Attribute each diagnostic to that key
|
|
@@ -19,7 +24,7 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
19
24
|
? file
|
|
20
25
|
: path.resolve(cwd, file)
|
|
21
26
|
: fallbackPath;
|
|
22
|
-
for (const err of fileErrors
|
|
27
|
+
for (const err of fileErrors?.messages ?? []) {
|
|
23
28
|
diagnostics.push({
|
|
24
29
|
id: `phpstan:${err.line ?? 1}:${err.message.slice(0, 40)}`,
|
|
25
30
|
message: err.message,
|
|
@@ -29,11 +34,38 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
29
34
|
severity: "error",
|
|
30
35
|
semantic: "blocking",
|
|
31
36
|
tool: "phpstan",
|
|
32
|
-
rule: "phpstan",
|
|
37
|
+
rule: err.identifier || "phpstan",
|
|
33
38
|
fixable: false,
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
}
|
|
42
|
+
// #1937 round 2: the top-level `errors[]` array carries phpstan's
|
|
43
|
+
// FILE-INDEPENDENT findings — internal errors, and ignore patterns that
|
|
44
|
+
// matched nothing. A run can report `totals.errors: 1` with `files: {}`
|
|
45
|
+
// and exit 1, and reading only `files` turned that into zero
|
|
46
|
+
// diagnostics: the same defect class as the `errors`-vs-`messages` bug
|
|
47
|
+
// above, one level up. Pinned by
|
|
48
|
+
// tests/fixtures/runner-output/phpstan/top-level-errors.captured.json.
|
|
49
|
+
//
|
|
50
|
+
// These have no file or line, so they attach to the edited file at line
|
|
51
|
+
// 1. That is the honest placement: the reader needs to see them, and
|
|
52
|
+
// there is nowhere truer to put them.
|
|
53
|
+
for (const message of output.errors ?? []) {
|
|
54
|
+
if (typeof message !== "string" || !message.trim())
|
|
55
|
+
continue;
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
id: `phpstan:global:${message.slice(0, 40)}`,
|
|
58
|
+
message,
|
|
59
|
+
filePath: fallbackPath,
|
|
60
|
+
line: 1,
|
|
61
|
+
column: 1,
|
|
62
|
+
severity: "error",
|
|
63
|
+
semantic: "blocking",
|
|
64
|
+
tool: "phpstan",
|
|
65
|
+
rule: "phpstan/analysis",
|
|
66
|
+
fixable: false,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
37
69
|
return diagnostics;
|
|
38
70
|
}
|
|
39
71
|
catch {
|
|
@@ -41,7 +73,7 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
41
73
|
}
|
|
42
74
|
}
|
|
43
75
|
async function resolvePhpstan(cwd) {
|
|
44
|
-
if (await
|
|
76
|
+
if (await phpstan.isAvailableAsync(cwd))
|
|
45
77
|
return phpstan.getCommand(cwd);
|
|
46
78
|
return resolveVendorToolCommand(cwd, "phpstan", ".bat");
|
|
47
79
|
}
|
|
@@ -66,14 +98,15 @@ const phpstanRunner = {
|
|
|
66
98
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
67
99
|
const absPath = path.resolve(cwd, ctx.filePath);
|
|
68
100
|
const result = await safeSpawnAsync(cmd, ["analyse", "--error-format=json", "--no-progress", absPath], { timeout: 30000, cwd });
|
|
69
|
-
// phpstan exits 0 = no errors, 1 = errors found, 2 = fatal
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
// phpstan exits 0 = no errors, 1 = errors found, 2 = fatal. The fatal
|
|
102
|
+
// case used to return `skipped` silently; routing it through the shared
|
|
103
|
+
// seam keeps that verdict and adds the bounded `runner-empty-result` row
|
|
104
|
+
// (#1816), and the exit-1 case now also records when the JSON report
|
|
105
|
+
// parses to nothing (#1948) — the shape the error-count-as-array bug had.
|
|
106
|
+
const run = parseToolRun("phpstan", { result, exitCodes: PHPSTAN_EXIT_CODES }, (out) => parsePhpstanJson(out, ctx.filePath, cwd), { parseOutput: result.stdout ?? "" });
|
|
107
|
+
if (run.skipped)
|
|
108
|
+
return run.skipped;
|
|
109
|
+
const diagnostics = run.diagnostics;
|
|
77
110
|
if (diagnostics.length === 0) {
|
|
78
111
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
79
112
|
}
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* Install: cargo install typos-cli
|
|
19
19
|
* Or: npm install -g typos-cli (if wrapped)
|
|
20
20
|
*/
|
|
21
|
-
import { skipUnlessToolRan } from "./utils/tool-failure.js";
|
|
22
21
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
23
22
|
import { PRIORITY } from "../priorities.js";
|
|
24
23
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
24
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
25
25
|
const typos = createAvailabilityChecker("typos", ".exe");
|
|
26
26
|
// typos-cli exit codes: 0 = no typos, 2 = typos found, 1 = an error that
|
|
27
27
|
// stopped the scan. Anything nonzero outside {2} is a rejected invocation.
|
|
@@ -98,20 +98,15 @@ const spellcheckRunner = {
|
|
|
98
98
|
// (unreadable config, bad argument). The `status === 2 || stdout` test
|
|
99
99
|
// below is false for exit 1 with an empty stdout, so a failed run was
|
|
100
100
|
// reported as a clean file. Only 0 and 2 are runs.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
111
|
-
}
|
|
112
|
-
// Parse diagnostics
|
|
113
|
-
const raw = result.stdout + result.stderr;
|
|
114
|
-
const diagnostics = parseTyposOutput(raw, ctx.filePath);
|
|
101
|
+
//
|
|
102
|
+
// #1948: parse through the shared seam so an exit-2 run whose JSON lines
|
|
103
|
+
// yield nothing leaves a record. `parseOutput` keeps the historical
|
|
104
|
+
// split: the gate judges "did it run" on stdout, the parser reads both
|
|
105
|
+
// streams.
|
|
106
|
+
const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
|
|
107
|
+
if (run.skipped)
|
|
108
|
+
return run.skipped;
|
|
109
|
+
const diagnostics = run.diagnostics;
|
|
115
110
|
if (diagnostics.length === 0) {
|
|
116
111
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
117
112
|
}
|
|
@@ -2,7 +2,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
2
2
|
import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
|
|
3
3
|
import { PRIORITY } from "../priorities.js";
|
|
4
4
|
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parseToolRun } from "./utils/tool-failure.js";
|
|
6
6
|
const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
|
|
7
7
|
// sqlfluff exit codes (its CLI docs): 0 = clean, 1 = violations found, 2 = a
|
|
8
8
|
// user error that stopped the lint (unknown dialect, unreadable config).
|
|
@@ -127,7 +127,7 @@ const sqlfluffRunner = {
|
|
|
127
127
|
ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
|
|
128
128
|
}
|
|
129
129
|
let cmd = null;
|
|
130
|
-
if (await
|
|
130
|
+
if (await sqlfluff.isAvailableAsync(cwd)) {
|
|
131
131
|
cmd = sqlfluff.getCommand(cwd);
|
|
132
132
|
}
|
|
133
133
|
else {
|
|
@@ -135,10 +135,19 @@ const sqlfluffRunner = {
|
|
|
135
135
|
}
|
|
136
136
|
if (!cmd)
|
|
137
137
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
138
|
-
|
|
138
|
+
// #1937: this used to be `args.splice(2, 0, "--dialect", "ansi")` on
|
|
139
|
+
// `["lint", "--format", "json", file]`, which inserts BETWEEN the flag
|
|
140
|
+
// and its value and yields `lint --format --dialect ansi json file`.
|
|
141
|
+
// sqlfluff rejects `--dialect` as the value of `--format` and exits 2,
|
|
142
|
+
// so every SQL project WITHOUT a .sqlfluff — exactly the case this
|
|
143
|
+
// dialect default exists to serve — was never linted. Built in order
|
|
144
|
+
// rather than patched by index, and pinned by the argv-tie assertion
|
|
145
|
+
// in runners/captured-real-output.test.ts.
|
|
146
|
+
const args = ["lint", "--format", "json"];
|
|
139
147
|
if (!hasConfig) {
|
|
140
|
-
args.
|
|
148
|
+
args.push("--dialect", "ansi");
|
|
141
149
|
}
|
|
150
|
+
args.push(ctx.filePath);
|
|
142
151
|
const result = await safeSpawnAsync(cmd, args, {
|
|
143
152
|
timeout: 20000,
|
|
144
153
|
cwd,
|
|
@@ -146,13 +155,12 @@ const sqlfluffRunner = {
|
|
|
146
155
|
// #1816: this runner read `result.status` zero times, so a user error
|
|
147
156
|
// fell through `parseSqlfluffOutput`'s JSON catch to zero violations and
|
|
148
157
|
// reported the SQL file as clean.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const diagnostics = parseSqlfluffOutput(result.stdout ?? "", ctx.filePath);
|
|
158
|
+
// #1948: sqlfluff exit 1 with a JSON report we read nothing out of is a
|
|
159
|
+
// parser break, not a clean file.
|
|
160
|
+
const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath), { parseOutput: result.stdout ?? "" });
|
|
161
|
+
if (run.skipped)
|
|
162
|
+
return run.skipped;
|
|
163
|
+
const diagnostics = run.diagnostics;
|
|
156
164
|
if (diagnostics.length === 0) {
|
|
157
165
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
158
166
|
}
|