pi-lens 4.1.0 → 4.1.2
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 +185 -0
- package/README.md +106 -0
- package/dist/clients/actionable-warnings-logger.js +2 -1
- package/dist/clients/actionable-warnings.js +14 -5
- package/dist/clients/advisory-provenance.js +48 -18
- package/dist/clients/ast-grep-client.js +1 -3
- package/dist/clients/atomic-write-staging.js +16 -4
- package/dist/clients/bash-file-access.js +99 -6
- package/dist/clients/biome-client.js +1 -3
- package/dist/clients/blocker-freshness.js +21 -8
- package/dist/clients/blocker-past-eof.js +16 -6
- package/dist/clients/bounded-cache.js +24 -8
- package/dist/clients/bounded-pid-file-lock.js +4 -3
- package/dist/clients/bounded-telemetry.js +14 -0
- package/dist/clients/cache-manager.js +6 -7
- package/dist/clients/cache-observability.js +782 -110
- package/dist/clients/call-graph.js +238 -63
- package/dist/clients/codebase-model.js +13 -5
- package/dist/clients/complexity-client.js +1 -3
- package/dist/clients/dead-code-client.js +19 -8
- package/dist/clients/dead-code-logger.js +1 -2
- package/dist/clients/deadline-utils.js +3 -1
- package/dist/clients/debug-heap.js +8 -1
- package/dist/clients/degradation-ledger.js +22 -1
- package/dist/clients/demoted-finding-render.js +155 -0
- package/dist/clients/dependency-checker.js +11 -7
- package/dist/clients/deps/pi-tui.js +8 -2
- package/dist/clients/deps/typebox.js +8 -3
- package/dist/clients/diagnostic-dispositions.js +39 -2
- package/dist/clients/diagnostic-line-freshness.js +5 -1
- package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
- package/dist/clients/dispatch/dispatcher.js +13 -1
- package/dist/clients/dispatch/facts/function-facts.js +3 -1
- package/dist/clients/dispatch/facts/import-facts.js +2 -1
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
- package/dist/clients/dispatch/integration.js +9 -4
- package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
- package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
- package/dist/clients/dispatch/runners/actionlint.js +5 -2
- package/dist/clients/dispatch/runners/biome-check.js +10 -14
- package/dist/clients/dispatch/runners/cpp-check.js +5 -1
- package/dist/clients/dispatch/runners/detekt.js +9 -6
- package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
- package/dist/clients/dispatch/runners/elixir-check.js +3 -1
- package/dist/clients/dispatch/runners/eslint.js +25 -6
- package/dist/clients/dispatch/runners/fish-indent.js +1 -1
- package/dist/clients/dispatch/runners/gleam-check.js +1 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
- package/dist/clients/dispatch/runners/hadolint.js +22 -15
- package/dist/clients/dispatch/runners/helm-lint.js +4 -1
- package/dist/clients/dispatch/runners/htmlhint.js +33 -16
- package/dist/clients/dispatch/runners/javac.js +1 -1
- package/dist/clients/dispatch/runners/ktlint.js +1 -1
- package/dist/clients/dispatch/runners/lsp.js +1 -1
- package/dist/clients/dispatch/runners/markdownlint.js +34 -12
- package/dist/clients/dispatch/runners/mypy.js +15 -17
- package/dist/clients/dispatch/runners/oxlint.js +231 -15
- package/dist/clients/dispatch/runners/php-lint.js +1 -1
- package/dist/clients/dispatch/runners/phpstan.js +52 -15
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
- package/dist/clients/dispatch/runners/pyright.js +23 -14
- package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
- package/dist/clients/dispatch/runners/shellcheck.js +7 -9
- package/dist/clients/dispatch/runners/shfmt.js +5 -2
- package/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
- package/dist/clients/dispatch/runners/stylelint.js +40 -20
- package/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/dist/clients/dispatch/runners/taplo.js +110 -23
- package/dist/clients/dispatch/runners/terragrunt.js +13 -8
- package/dist/clients/dispatch/runners/tflint.js +25 -15
- package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
- package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
- package/dist/clients/dispatch/runners/vale.js +16 -14
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
- package/dist/clients/dispatch/runners/yamllint.js +13 -16
- package/dist/clients/dispatch/runners/zig-check.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +2 -1
- package/dist/clients/dispatch/types.js +6 -1
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
- package/dist/clients/extension-log.js +5 -3
- package/dist/clients/feature-hints.js +8 -1
- package/dist/clients/file-kinds.js +34 -129
- package/dist/clients/file-utils.js +17 -1
- package/dist/clients/finding-delivery-gate.js +79 -2
- package/dist/clients/formatters.js +144 -27
- package/dist/clients/freshness.js +38 -0
- package/dist/clients/git-guard.js +43 -13
- package/dist/clients/go-client.js +1 -3
- package/dist/clients/grammar-source.js +1 -1
- package/dist/clients/gzip-stage-write.js +30 -4
- package/dist/clients/host-edit-normalize.js +1 -3
- package/dist/clients/install-diagnostics.js +4 -2
- package/dist/clients/installer/index.js +86 -77
- package/dist/clients/instance-reaper.js +5 -2
- package/dist/clients/instance-registry.js +4 -2
- package/dist/clients/jscpd-client.js +11 -3
- package/dist/clients/knip-client.js +29 -15
- package/dist/clients/language-profile.js +11 -3
- package/dist/clients/latency-logger.js +13 -2
- package/dist/clients/lens-config.js +1 -1
- package/dist/clients/lens-map.js +8 -3
- package/dist/clients/live-bus-emitter.js +1 -1
- package/dist/clients/lsp/cascade-tier.js +27 -2
- package/dist/clients/lsp/client.js +144 -5
- package/dist/clients/lsp/config.js +4 -6
- package/dist/clients/lsp/edits.js +119 -32
- package/dist/clients/lsp/index.js +245 -41
- package/dist/clients/lsp/inferred-project.js +2 -1
- package/dist/clients/lsp/jvm-runtime.js +4 -1
- package/dist/clients/lsp/launch.js +11 -3
- package/dist/clients/lsp/pending-aux-coverage.js +152 -0
- package/dist/clients/lsp/server.js +19 -7
- package/dist/clients/lsp/tsserver-sync.js +10 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
- package/dist/clients/lsp-document-symbols.js +1 -3
- package/dist/clients/lsp-mutation.js +10 -20
- package/dist/clients/mcp/host-shim.js +1 -1
- package/dist/clients/memory-sampler.js +73 -7
- package/dist/clients/metrics-client.js +1 -3
- package/dist/clients/middle-man-analysis.js +8 -3
- package/dist/clients/module-report-lsp.js +7 -1
- package/dist/clients/module-report.js +28 -18
- package/dist/clients/ndjson-logger.js +117 -28
- package/dist/clients/opaque-mutation-scan.js +220 -0
- package/dist/clients/package-manager.js +6 -1
- package/dist/clients/pipeline.js +57 -9
- package/dist/clients/project-conventions.js +2 -8
- package/dist/clients/project-diagnostics/cache.js +10 -7
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
- package/dist/clients/project-diagnostics/scanner.js +22 -10
- package/dist/clients/project-lens-config.js +12 -5
- package/dist/clients/project-report.js +62 -27
- package/dist/clients/project-snapshot-fingerprint.js +48 -0
- package/dist/clients/project-snapshot-persist-worker.js +15 -1
- package/dist/clients/project-snapshot.js +472 -66
- package/dist/clients/read-expansion.js +12 -2
- package/dist/clients/read-guard-logger.js +12 -6
- package/dist/clients/read-guard-tool-lines.js +5 -2
- package/dist/clients/read-guard.js +88 -19
- package/dist/clients/resource-sampler.js +1 -1
- package/dist/clients/reverse-deps.js +6 -5
- package/dist/clients/review-graph/builder.js +236 -48
- package/dist/clients/review-graph/git-identity.js +8 -2
- package/dist/clients/review-graph/import-resolvers.js +10 -1
- package/dist/clients/review-graph/query.js +2 -1
- package/dist/clients/review-graph/revision-drift.js +21 -0
- package/dist/clients/review-graph/tsconfig-paths.js +4 -2
- package/dist/clients/review-graph/workspace-modules.js +8 -5
- package/dist/clients/ruff-client.js +6 -5
- package/dist/clients/runtime-agent-end.js +63 -31
- package/dist/clients/runtime-context.js +12 -4
- package/dist/clients/runtime-coordinator.js +127 -4
- package/dist/clients/runtime-session.js +55 -0
- package/dist/clients/runtime-tool-call.js +59 -3
- package/dist/clients/runtime-tool-result.js +148 -34
- package/dist/clients/runtime-turn.js +194 -8
- package/dist/clients/rust-client.js +1 -3
- package/dist/clients/safe-spawn.js +150 -11
- package/dist/clients/security-scan-client.js +1 -3
- package/dist/clients/session-event-guard.js +184 -0
- package/dist/clients/session-summary.js +3 -1
- package/dist/clients/sg-runner.js +1 -3
- package/dist/clients/source-filter.js +16 -8
- package/dist/clients/source-walker.js +1 -1
- package/dist/clients/spawn-timeout-cooldown.js +96 -0
- package/dist/clients/startup-scan.js +9 -3
- package/dist/clients/tool-definition.js +7 -0
- package/dist/clients/tool-policy.js +26 -11
- package/dist/clients/tool-render.js +2 -1
- package/dist/clients/tree-sitter-client.js +10 -4
- package/dist/clients/tree-sitter-logger.js +2 -1
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/warm-attach.js +12 -5
- package/dist/clients/widget-state.js +27 -11
- package/dist/clients/word-index.js +3 -1
- package/dist/clients/workspace-topology.js +8 -2
- package/dist/clients/zizmor-config.js +9 -1
- package/dist/i18n.js +13 -5
- package/dist/index.js +15811 -12810
- package/dist/mcp/server.js +3 -1
- package/dist/tools/ast-grep-replace.js +68 -12
- package/dist/tools/lens-diagnostic-mark.js +21 -7
- package/dist/tools/lens-diagnostics.js +174 -38
- package/dist/tools/lsp-diagnostics.js +24 -7
- package/dist/tools/lsp-navigation.js +3 -1
- package/dist/tools/module-report.js +1 -1
- package/dist/tools/project-report.js +1 -1
- package/dist/tools/symbol-search.js +5 -2
- package/docs/durable-store-audit-1202.md +1 -1
- package/docs/servercapabilities.md +1 -1
- package/package.json +160 -143
- package/rules/typos/_typos.toml +2 -2
- package/scripts/analyze-pi-lens-logs.mjs +31 -13
- package/scripts/download-grammars.js +9 -3
- package/scripts/install-selftest.mjs +99 -11
- package/scripts/lib/host-provided-deps.mjs +75 -0
- package/scripts/lib/warm-loader-cache.mjs +285 -0
- package/scripts/rpc-load-check.mjs +30 -9
- package/scripts/warm-loader-cache.mjs +233 -0
|
@@ -13,6 +13,17 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
|
13
13
|
import { getJstsLintPolicyForCwd, hasVitePlusConfig, } from "../../tool-policy.js";
|
|
14
14
|
import { PRIORITY } from "../priorities.js";
|
|
15
15
|
import { resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
|
|
16
|
+
import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
|
|
17
|
+
const OXLINT_NO_FILES = Symbol("oxlint-no-files");
|
|
18
|
+
const OXLINT_NO_FILES_UNCONFIRMED = Symbol("oxlint-no-files-unconfirmed");
|
|
19
|
+
const OXLINT_NO_FILES_BANNER = "No files found to lint. Please check your paths and ignore patterns.";
|
|
20
|
+
const OXLINT_NO_FILES_REPORT_KEYS = new Set([
|
|
21
|
+
"diagnostics",
|
|
22
|
+
"number_of_files",
|
|
23
|
+
"number_of_rules",
|
|
24
|
+
"threads_count",
|
|
25
|
+
"start_time",
|
|
26
|
+
]);
|
|
16
27
|
function resolveLocalVp(cwd) {
|
|
17
28
|
const isWin = process.platform === "win32";
|
|
18
29
|
for (const dir of walkUpDirs(cwd)) {
|
|
@@ -76,31 +87,234 @@ const oxlintRunner = {
|
|
|
76
87
|
const result = await safeSpawnAsync(cmd, args, {
|
|
77
88
|
timeout: 30000,
|
|
78
89
|
});
|
|
79
|
-
// Oxlint
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
// Oxlint exits 0 whenever nothing at ERROR severity was found — that
|
|
91
|
+
// includes a run that found only warnings, its own default severity
|
|
92
|
+
// (#1947). A run that found nothing at all also exits 0, but still
|
|
93
|
+
// prints a report with an empty `diagnostics` array, so parsing
|
|
94
|
+
// unconditionally and branching on the parsed count (below) tells the
|
|
95
|
+
// two apart instead of the exit code discarding the warning case.
|
|
96
|
+
//
|
|
83
97
|
// Parse JSON output. Fall back to the unix-format parser if JSON parsing
|
|
84
98
|
// fails (older oxlint versions, malformed stderr noise, etc.) — keeps the
|
|
85
99
|
// runner producing diagnostics even when the structured-fix metadata is
|
|
86
100
|
// unavailable.
|
|
87
101
|
const stdout = result.stdout ?? "";
|
|
88
102
|
const stderr = result.stderr ?? "";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const parsedOutput = stdout + stderr;
|
|
104
|
+
const noFilesDecision = decideOxlintNoFiles(result, stdout, stderr);
|
|
105
|
+
// #1994's shared gate classifies invocation and parsed-output failures.
|
|
106
|
+
// The private markers preserve one legitimate nonzero/empty shape and
|
|
107
|
+
// force a status-0 lookalike away from clean. Neither escapes this runner.
|
|
108
|
+
// parseToolRun checks process failures before invoking this parser, so
|
|
109
|
+
// spawn/timeout/killed/signal precedence remains owned by #1994.
|
|
110
|
+
const parsedRun = parseToolRun("oxlint", { result, output: parsedOutput }, () => {
|
|
111
|
+
if (noFilesDecision.kind === "expected-no-files") {
|
|
112
|
+
return [OXLINT_NO_FILES];
|
|
113
|
+
}
|
|
114
|
+
if (noFilesDecision.kind === "unconfirmed-no-files" &&
|
|
115
|
+
result.status === 0) {
|
|
116
|
+
return [OXLINT_NO_FILES_UNCONFIRMED];
|
|
117
|
+
}
|
|
118
|
+
let parsed = parseOxlintJson(stdout, ctx.filePath);
|
|
119
|
+
if (parsed.length === 0 && stdout.length > 0) {
|
|
120
|
+
parsed = parseOxlintUnix(parsedOutput, ctx.filePath);
|
|
121
|
+
}
|
|
122
|
+
return parsed;
|
|
123
|
+
}, { parseOutput: parsedOutput });
|
|
124
|
+
if (parsedRun.skipped)
|
|
125
|
+
return parsedRun.skipped;
|
|
126
|
+
if (parsedRun.diagnostics.includes(OXLINT_NO_FILES)) {
|
|
127
|
+
// Real captured bytes show oxlint exits
|
|
128
|
+
// 1 with "No files found" + number_of_files: 0 when a config excludes
|
|
129
|
+
// the target, so "nonzero exit" alone cannot mean "unreadable report
|
|
130
|
+
// of problems" until the no-files shape is ruled out.
|
|
131
|
+
// Read BEFORE reporting "succeeded" — a config (root's or a nested
|
|
132
|
+
// one nearer the file, per oxlint's own discovery) that ignores this
|
|
133
|
+
// file reports the same empty diagnostics array a clean file does.
|
|
134
|
+
// "succeeded"/"none" would say "we checked, it's clean"; this file
|
|
135
|
+
// was never checked at all.
|
|
136
|
+
return {
|
|
137
|
+
status: "skipped",
|
|
138
|
+
diagnostics: [],
|
|
139
|
+
semantic: "none",
|
|
140
|
+
skipReason: "no-files-matched",
|
|
141
|
+
};
|
|
92
142
|
}
|
|
93
|
-
if (diagnostics.
|
|
94
|
-
|
|
143
|
+
if (parsedRun.diagnostics.includes(OXLINT_NO_FILES_UNCONFIRMED)) {
|
|
144
|
+
const reason = noFilesDecision.kind === "unconfirmed-no-files"
|
|
145
|
+
? noFilesDecision.reason
|
|
146
|
+
: "status-zero";
|
|
147
|
+
return {
|
|
148
|
+
status: "failed",
|
|
149
|
+
diagnostics: [
|
|
150
|
+
{
|
|
151
|
+
id: `oxlint:no-files-unconfirmed:${reason}`,
|
|
152
|
+
message: `oxlint no-files report was not canonical (${reason})`,
|
|
153
|
+
filePath: ctx.filePath,
|
|
154
|
+
line: 1,
|
|
155
|
+
column: 1,
|
|
156
|
+
severity: "warning",
|
|
157
|
+
semantic: "warning",
|
|
158
|
+
tool: "oxlint",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
semantic: "warning",
|
|
162
|
+
failureKind: "unconfirmed_output",
|
|
163
|
+
failureMessage: `no-files-${reason}`,
|
|
164
|
+
};
|
|
95
165
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
166
|
+
const diagnostics = parsedRun.diagnostics.filter((diagnostic) => diagnostic !== OXLINT_NO_FILES &&
|
|
167
|
+
diagnostic !== OXLINT_NO_FILES_UNCONFIRMED);
|
|
168
|
+
// A warning-only result on exit 0 is oxlint's normal outcome, not a
|
|
169
|
+
// failure: exit 0 means nothing hit ERROR severity. `status: "failed"`
|
|
170
|
+
// here would stop this arm from reporting "succeeded", which breaks two
|
|
171
|
+
// things downstream — plan.ts's ["eslint", "oxlint", "biome-check-json"]
|
|
172
|
+
// fallback group only stops at the first `status: "succeeded"` runner
|
|
173
|
+
// (dispatcher.ts's `runGroup`), so biome-check-json would run again on
|
|
174
|
+
// every warning-only save (extra spawns, a possible install, duplicate
|
|
175
|
+
// findings); and it would mismatch the sibling convention (biome-check,
|
|
176
|
+
// golangci-lint, rubocop) of keying `status` off blocking severity, not
|
|
177
|
+
// off the tool's raw exit code. The findings themselves still reach the
|
|
178
|
+
// delivery pipeline regardless of `status` — dispatcher.ts buckets by
|
|
179
|
+
// each diagnostic's own `semantic`, so a warning stays a warning.
|
|
180
|
+
return finishParsedRun({
|
|
181
|
+
tool: "oxlint",
|
|
182
|
+
ctx,
|
|
183
|
+
result,
|
|
99
184
|
diagnostics,
|
|
100
|
-
|
|
101
|
-
|
|
185
|
+
classify: (diagnostics) => {
|
|
186
|
+
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
187
|
+
return {
|
|
188
|
+
status: !hasBlocking && result.status === 0 ? "succeeded" : "failed",
|
|
189
|
+
semantic: hasBlocking ? "blocking" : "warning",
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
102
193
|
},
|
|
103
194
|
};
|
|
195
|
+
function oxlintProcessState(result) {
|
|
196
|
+
const failureKind = result.spawnFailure?.kind;
|
|
197
|
+
if (result.signal || result.failure === "signal")
|
|
198
|
+
return "signal";
|
|
199
|
+
if (result.failure === "timeout" || failureKind === "timeout")
|
|
200
|
+
return "timeout";
|
|
201
|
+
if (result.failure === "aborted" || failureKind === "killed")
|
|
202
|
+
return "killed";
|
|
203
|
+
if (result.failure === "spawn" || result.error)
|
|
204
|
+
return "spawn";
|
|
205
|
+
if (result.outputTruncated)
|
|
206
|
+
return "truncated";
|
|
207
|
+
return "normal";
|
|
208
|
+
}
|
|
209
|
+
function isNonnegativeInteger(value) {
|
|
210
|
+
return Number.isInteger(value) && value >= 0;
|
|
211
|
+
}
|
|
212
|
+
function isPositiveInteger(value) {
|
|
213
|
+
return Number.isInteger(value) && value >= 1;
|
|
214
|
+
}
|
|
215
|
+
function isNonnegativeFinite(value) {
|
|
216
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* One decision seam for every oxlint no-files axis (#1998).
|
|
220
|
+
*
|
|
221
|
+
* Real oxlint 1.79.0 establishes the only expected shape: a normal completed
|
|
222
|
+
* process, exit 1, empty stderr, the exact no-files banner, and a JSON object
|
|
223
|
+
* containing exactly the five captured summary fields with canonical types
|
|
224
|
+
* and ranges. Diagnostics must be empty and `number_of_files` must be zero.
|
|
225
|
+
*
|
|
226
|
+
* Any process-control evidence wins before report bytes. Any no-files
|
|
227
|
+
* lookalike that differs on status, stderr, banner, JSON syntax/schema,
|
|
228
|
+
* diagnostics, or file count is unconfirmed rather than clean. A report with
|
|
229
|
+
* no no-files evidence remains an ordinary oxlint report for the normal parser.
|
|
230
|
+
*/
|
|
231
|
+
export function decideOxlintNoFiles(result, stdout, stderr) {
|
|
232
|
+
const processState = oxlintProcessState(result);
|
|
233
|
+
if (processState !== "normal") {
|
|
234
|
+
return {
|
|
235
|
+
kind: "unconfirmed-no-files",
|
|
236
|
+
reason: `process-${processState}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const jsonStart = stdout.indexOf("{");
|
|
240
|
+
const bannerText = (jsonStart === -1 ? stdout : stdout.slice(0, jsonStart)).trim();
|
|
241
|
+
const banner = bannerText === OXLINT_NO_FILES_BANNER
|
|
242
|
+
? "exact"
|
|
243
|
+
: /no files found.+lint/i.test(bannerText)
|
|
244
|
+
? "near"
|
|
245
|
+
: "missing";
|
|
246
|
+
let report;
|
|
247
|
+
let jsonMalformed = jsonStart === -1;
|
|
248
|
+
if (jsonStart !== -1) {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(stdout.slice(jsonStart));
|
|
251
|
+
if (typeof parsed === "object" &&
|
|
252
|
+
parsed !== null &&
|
|
253
|
+
!Array.isArray(parsed)) {
|
|
254
|
+
report = parsed;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
jsonMalformed = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
jsonMalformed = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const hasFileCount = report !== undefined &&
|
|
265
|
+
Object.prototype.hasOwnProperty.call(report, "number_of_files");
|
|
266
|
+
const fileCount = report?.number_of_files;
|
|
267
|
+
const hasNoFilesEvidence = banner !== "missing" ||
|
|
268
|
+
(hasFileCount && (fileCount === 0 || !isNonnegativeInteger(fileCount)));
|
|
269
|
+
if (!hasNoFilesEvidence)
|
|
270
|
+
return { kind: "ordinary-report" };
|
|
271
|
+
if (result.status == null) {
|
|
272
|
+
return { kind: "unconfirmed-no-files", reason: "status-null" };
|
|
273
|
+
}
|
|
274
|
+
if (result.status === 0) {
|
|
275
|
+
return { kind: "unconfirmed-no-files", reason: "status-zero" };
|
|
276
|
+
}
|
|
277
|
+
if (result.status !== 1) {
|
|
278
|
+
return { kind: "unconfirmed-no-files", reason: "status-other" };
|
|
279
|
+
}
|
|
280
|
+
if (stderr.trim().length > 0) {
|
|
281
|
+
return { kind: "unconfirmed-no-files", reason: "stderr-nonempty" };
|
|
282
|
+
}
|
|
283
|
+
if (banner === "near") {
|
|
284
|
+
return { kind: "unconfirmed-no-files", reason: "banner-near" };
|
|
285
|
+
}
|
|
286
|
+
if (banner === "missing") {
|
|
287
|
+
return { kind: "unconfirmed-no-files", reason: "banner-missing" };
|
|
288
|
+
}
|
|
289
|
+
if (jsonMalformed || !report) {
|
|
290
|
+
return { kind: "unconfirmed-no-files", reason: "json-malformed" };
|
|
291
|
+
}
|
|
292
|
+
const keys = Object.keys(report);
|
|
293
|
+
if (keys.some((key) => !OXLINT_NO_FILES_REPORT_KEYS.has(key))) {
|
|
294
|
+
return { kind: "unconfirmed-no-files", reason: "json-unknown-key" };
|
|
295
|
+
}
|
|
296
|
+
if (OXLINT_NO_FILES_REPORT_KEYS.size !== keys.length ||
|
|
297
|
+
[...OXLINT_NO_FILES_REPORT_KEYS].some((key) => !Object.prototype.hasOwnProperty.call(report, key))) {
|
|
298
|
+
return { kind: "unconfirmed-no-files", reason: "json-missing-key" };
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(report.diagnostics) ||
|
|
301
|
+
!isNonnegativeInteger(report.number_of_files) ||
|
|
302
|
+
!isNonnegativeInteger(report.number_of_rules) ||
|
|
303
|
+
!isPositiveInteger(report.threads_count) ||
|
|
304
|
+
!isNonnegativeFinite(report.start_time)) {
|
|
305
|
+
return {
|
|
306
|
+
kind: "unconfirmed-no-files",
|
|
307
|
+
reason: "json-known-field-invalid",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (report.diagnostics.length > 0) {
|
|
311
|
+
return { kind: "unconfirmed-no-files", reason: "diagnostics-nonempty" };
|
|
312
|
+
}
|
|
313
|
+
if (report.number_of_files !== 0) {
|
|
314
|
+
return { kind: "unconfirmed-no-files", reason: "files-nonzero" };
|
|
315
|
+
}
|
|
316
|
+
return { kind: "expected-no-files" };
|
|
317
|
+
}
|
|
104
318
|
// Oxlint codes look like "eslint(no-debugger)" or "oxc(approx-constant)".
|
|
105
319
|
// Strip the plugin prefix so the rule lines up with what users expect.
|
|
106
320
|
// indexOf-based extraction avoids a regex hot-spot Sonar flagged for
|
|
@@ -127,8 +341,10 @@ function parseOxlintJson(raw, filePath) {
|
|
|
127
341
|
catch {
|
|
128
342
|
return [];
|
|
129
343
|
}
|
|
344
|
+
if (!Array.isArray(parsed.diagnostics))
|
|
345
|
+
return [];
|
|
130
346
|
const diagnostics = [];
|
|
131
|
-
for (const d of parsed.diagnostics
|
|
347
|
+
for (const d of parsed.diagnostics) {
|
|
132
348
|
const rule = extractOxlintRule(d.code);
|
|
133
349
|
const label = d.labels?.[0]?.span;
|
|
134
350
|
const lineNum = label?.line ?? 1;
|
|
@@ -33,7 +33,7 @@ const phpLintRunner = {
|
|
|
33
33
|
skipTestFiles: false,
|
|
34
34
|
async run(ctx) {
|
|
35
35
|
const cwd = ctx.cwd || process.cwd();
|
|
36
|
-
if (!(await
|
|
36
|
+
if (!(await php.isAvailableAsync(cwd))) {
|
|
37
37
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
38
38
|
}
|
|
39
39
|
const cmd = php.getCommand(cwd);
|
|
@@ -3,6 +3,12 @@ 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
|
+
import { finishParsedRun } from "./utils/tool-failure.js";
|
|
8
|
+
// phpstan's documented exit codes: 0 = no errors, 1 = errors found, 2 = a
|
|
9
|
+
// fatal/internal error that stopped the analysis. Only 1 is a run that carries
|
|
10
|
+
// findings.
|
|
11
|
+
const PHPSTAN_EXIT_CODES = { ran: [1] };
|
|
6
12
|
const phpstan = createAvailabilityChecker("phpstan", ".phar");
|
|
7
13
|
// phpstan analyses the target file PLUS its dependency closure and keys errors
|
|
8
14
|
// by the real file in `output.files`. Attribute each diagnostic to that key
|
|
@@ -19,7 +25,7 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
19
25
|
? file
|
|
20
26
|
: path.resolve(cwd, file)
|
|
21
27
|
: fallbackPath;
|
|
22
|
-
for (const err of fileErrors
|
|
28
|
+
for (const err of fileErrors?.messages ?? []) {
|
|
23
29
|
diagnostics.push({
|
|
24
30
|
id: `phpstan:${err.line ?? 1}:${err.message.slice(0, 40)}`,
|
|
25
31
|
message: err.message,
|
|
@@ -29,11 +35,38 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
29
35
|
severity: "error",
|
|
30
36
|
semantic: "blocking",
|
|
31
37
|
tool: "phpstan",
|
|
32
|
-
rule: "phpstan",
|
|
38
|
+
rule: err.identifier || "phpstan",
|
|
33
39
|
fixable: false,
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
42
|
}
|
|
43
|
+
// #1937 round 2: the top-level `errors[]` array carries phpstan's
|
|
44
|
+
// FILE-INDEPENDENT findings — internal errors, and ignore patterns that
|
|
45
|
+
// matched nothing. A run can report `totals.errors: 1` with `files: {}`
|
|
46
|
+
// and exit 1, and reading only `files` turned that into zero
|
|
47
|
+
// diagnostics: the same defect class as the `errors`-vs-`messages` bug
|
|
48
|
+
// above, one level up. Pinned by
|
|
49
|
+
// tests/fixtures/runner-output/phpstan/top-level-errors.captured.json.
|
|
50
|
+
//
|
|
51
|
+
// These have no file or line, so they attach to the edited file at line
|
|
52
|
+
// 1. That is the honest placement: the reader needs to see them, and
|
|
53
|
+
// there is nowhere truer to put them.
|
|
54
|
+
for (const message of output.errors ?? []) {
|
|
55
|
+
if (typeof message !== "string" || !message.trim())
|
|
56
|
+
continue;
|
|
57
|
+
diagnostics.push({
|
|
58
|
+
id: `phpstan:global:${message.slice(0, 40)}`,
|
|
59
|
+
message,
|
|
60
|
+
filePath: fallbackPath,
|
|
61
|
+
line: 1,
|
|
62
|
+
column: 1,
|
|
63
|
+
severity: "error",
|
|
64
|
+
semantic: "blocking",
|
|
65
|
+
tool: "phpstan",
|
|
66
|
+
rule: "phpstan/analysis",
|
|
67
|
+
fixable: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
37
70
|
return diagnostics;
|
|
38
71
|
}
|
|
39
72
|
catch {
|
|
@@ -41,7 +74,7 @@ export function parsePhpstanJson(raw, fallbackPath, cwd) {
|
|
|
41
74
|
}
|
|
42
75
|
}
|
|
43
76
|
async function resolvePhpstan(cwd) {
|
|
44
|
-
if (await
|
|
77
|
+
if (await phpstan.isAvailableAsync(cwd))
|
|
45
78
|
return phpstan.getCommand(cwd);
|
|
46
79
|
return resolveVendorToolCommand(cwd, "phpstan", ".bat");
|
|
47
80
|
}
|
|
@@ -66,18 +99,22 @@ const phpstanRunner = {
|
|
|
66
99
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
67
100
|
const absPath = path.resolve(cwd, ctx.filePath);
|
|
68
101
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
// phpstan exits 0 = no errors, 1 = errors found, 2 = fatal. The fatal
|
|
103
|
+
// case used to return `skipped` silently; routing it through the shared
|
|
104
|
+
// seam keeps that verdict and adds the bounded `runner-empty-result` row
|
|
105
|
+
// (#1816), and the exit-1 case now also records when the JSON report
|
|
106
|
+
// parses to nothing (#1948) — the shape the error-count-as-array bug had.
|
|
107
|
+
const run = parseToolRun("phpstan", { result, exitCodes: PHPSTAN_EXIT_CODES }, (out) => parsePhpstanJson(out, ctx.filePath, cwd), { parseOutput: result.stdout ?? "" });
|
|
108
|
+
if (run.skipped)
|
|
109
|
+
return run.skipped;
|
|
110
|
+
const diagnostics = run.diagnostics;
|
|
111
|
+
return finishParsedRun({
|
|
112
|
+
tool: "phpstan",
|
|
113
|
+
ctx,
|
|
114
|
+
result,
|
|
115
|
+
diagnostics,
|
|
116
|
+
classify: () => ({ status: "failed", semantic: "blocking" }),
|
|
117
|
+
});
|
|
81
118
|
},
|
|
82
119
|
};
|
|
83
120
|
export default phpstanRunner;
|
|
@@ -244,7 +244,11 @@ function parsePSAnalyzerOutput(raw, filePath) {
|
|
|
244
244
|
.filter((item) => item.Message && item.Line)
|
|
245
245
|
.map((item) => {
|
|
246
246
|
const sev = (item.Severity ?? "Warning").toLowerCase();
|
|
247
|
-
const severity =
|
|
247
|
+
const severity = sev === "error" || sev === "parseerror"
|
|
248
|
+
? "error"
|
|
249
|
+
: sev === "information"
|
|
250
|
+
? "info"
|
|
251
|
+
: "warning";
|
|
248
252
|
const rule = item.RuleName ?? "PSScriptAnalyzer";
|
|
249
253
|
return {
|
|
250
254
|
id: `psscriptanalyzer-${rule}-${item.Line}`,
|
|
@@ -295,8 +299,10 @@ const psScriptAnalyzerRunner = {
|
|
|
295
299
|
const result = await spawnPs(cmd, [
|
|
296
300
|
"-NoProfile",
|
|
297
301
|
"-NonInteractive",
|
|
298
|
-
"-File",
|
|
299
|
-
|
|
302
|
+
"-File",
|
|
303
|
+
tmpScript,
|
|
304
|
+
"-FilePath",
|
|
305
|
+
absPath,
|
|
300
306
|
]);
|
|
301
307
|
const hostStallMs = sampler.stop();
|
|
302
308
|
const elapsedMs = Date.now() - startedAt;
|
|
@@ -396,9 +402,7 @@ const psScriptAnalyzerRunner = {
|
|
|
396
402
|
// than latched. It must still never be read as a clean file
|
|
397
403
|
// (#1598): empty/malformed stdout on a successful exit is a lost
|
|
398
404
|
// or truncated write, not evidence of zero diagnostics.
|
|
399
|
-
const unreadableReason = result.stdout.trim()
|
|
400
|
-
? "unparseable"
|
|
401
|
-
: "empty";
|
|
405
|
+
const unreadableReason = result.stdout.trim() ? "unparseable" : "empty";
|
|
402
406
|
logAvailabilityDecision({
|
|
403
407
|
tool: "psscriptanalyzer-stdout",
|
|
404
408
|
verdict: "unavailable",
|
|
@@ -11,6 +11,7 @@ import { getLSPService } from "../../lsp/index.js";
|
|
|
11
11
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
12
12
|
import { PRIORITY } from "../priorities.js";
|
|
13
13
|
import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
|
|
14
|
+
import { finishParsedRun } from "./utils/tool-failure.js";
|
|
14
15
|
const pyright = createAvailabilityChecker("pyright", ".exe");
|
|
15
16
|
const pyrightRunner = {
|
|
16
17
|
id: "pyright",
|
|
@@ -31,7 +32,7 @@ const pyrightRunner = {
|
|
|
31
32
|
// Get pyright command - try multiple strategies
|
|
32
33
|
let cmd = null;
|
|
33
34
|
// Strategy 1: Check cached availability (fast path)
|
|
34
|
-
if (await
|
|
35
|
+
if (await pyright.isAvailableAsync(cwd)) {
|
|
35
36
|
cmd = pyright.getCommand(cwd);
|
|
36
37
|
}
|
|
37
38
|
// Strategy 2: use the shared availability taxonomy and install suppression.
|
|
@@ -64,24 +65,32 @@ const pyrightRunner = {
|
|
|
64
65
|
}
|
|
65
66
|
const output = (result.stdout || "").trim();
|
|
66
67
|
if (!output) {
|
|
67
|
-
|
|
68
|
+
// Empty stdout with a nonzero exit and stderr chatter is an unreadable
|
|
69
|
+
// report of problems, never clean (#1839). The helper turns that into
|
|
70
|
+
// failed + parse-error; exit 0 stays clean.
|
|
71
|
+
return finishParsedRun({
|
|
72
|
+
tool: "pyright",
|
|
73
|
+
ctx,
|
|
74
|
+
result,
|
|
75
|
+
diagnostics: [],
|
|
76
|
+
});
|
|
68
77
|
}
|
|
69
78
|
try {
|
|
70
79
|
const data = JSON.parse(output);
|
|
71
80
|
const diagnostics = parsePyrightOutput(data, ctx.filePath);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
status: hasErrors ? "failed" : "succeeded",
|
|
81
|
+
return finishParsedRun({
|
|
82
|
+
tool: "pyright",
|
|
83
|
+
ctx,
|
|
84
|
+
result,
|
|
78
85
|
diagnostics,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? "
|
|
83
|
-
: "
|
|
84
|
-
|
|
86
|
+
classify: (diagnostics) => {
|
|
87
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
88
|
+
return {
|
|
89
|
+
status: hasErrors ? "failed" : "succeeded",
|
|
90
|
+
semantic: hasErrors ? "blocking" : "warning",
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
85
94
|
// pi-lens-ignore: missing-error-propagation
|
|
86
95
|
}
|
|
87
96
|
catch {
|
|
@@ -32,7 +32,7 @@ const makeClippyProbe = (cargoExe) => createCwdCachedProbe((cwd) => safeSpawnAsy
|
|
|
32
32
|
}), { tool: "clippy", budgetMs: CLIPPY_PROBE_BUDGET_MS });
|
|
33
33
|
const clippyAvailabilityByCargo = new Map();
|
|
34
34
|
function getClippyProbe(cargoExe) {
|
|
35
|
-
return clippyAvailabilityByCargo.get(cargoExe) ?? refreshClippyProbe(cargoExe);
|
|
35
|
+
return (clippyAvailabilityByCargo.get(cargoExe) ?? refreshClippyProbe(cargoExe));
|
|
36
36
|
}
|
|
37
37
|
/** Replace the cached probe so a post-install state is observed. */
|
|
38
38
|
function refreshClippyProbe(cargoExe) {
|
|
@@ -24,6 +24,7 @@ import * as path from "node:path";
|
|
|
24
24
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
25
25
|
import { PRIORITY } from "../priorities.js";
|
|
26
26
|
import { createAvailabilityChecker, lspPrimaryCoversFile, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
|
|
27
|
+
import { finishParsedRun } from "./utils/tool-failure.js";
|
|
27
28
|
const shellcheck = createAvailabilityChecker("shellcheck", ".exe");
|
|
28
29
|
function findShellcheckConfig(cwd) {
|
|
29
30
|
const local = path.join(cwd, ".shellcheckrc");
|
|
@@ -122,7 +123,7 @@ const shellcheckRunner = {
|
|
|
122
123
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
123
124
|
}
|
|
124
125
|
let cmd = null;
|
|
125
|
-
if (await
|
|
126
|
+
if (await shellcheck.isAvailableAsync(cwd)) {
|
|
126
127
|
cmd = shellcheck.getCommand(cwd);
|
|
127
128
|
}
|
|
128
129
|
else {
|
|
@@ -157,15 +158,12 @@ const shellcheckRunner = {
|
|
|
157
158
|
// Parse diagnostics
|
|
158
159
|
const raw = result.stdout + result.stderr;
|
|
159
160
|
const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
status: hasBlocking ? "failed" : "succeeded",
|
|
161
|
+
return finishParsedRun({
|
|
162
|
+
tool: "shellcheck",
|
|
163
|
+
ctx,
|
|
164
|
+
result,
|
|
166
165
|
diagnostics,
|
|
167
|
-
|
|
168
|
-
};
|
|
166
|
+
});
|
|
169
167
|
},
|
|
170
168
|
};
|
|
171
169
|
export default shellcheckRunner;
|
|
@@ -34,7 +34,7 @@ const shfmtRunner = {
|
|
|
34
34
|
async run(ctx) {
|
|
35
35
|
const cwd = ctx.cwd || process.cwd();
|
|
36
36
|
let cmd = null;
|
|
37
|
-
if (await
|
|
37
|
+
if (await shfmt.isAvailableAsync(cwd)) {
|
|
38
38
|
cmd = shfmt.getCommand(cwd);
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
@@ -79,7 +79,10 @@ const shfmtRunner = {
|
|
|
79
79
|
// .editorconfig — otherwise this nags on every shell write against shfmt's
|
|
80
80
|
// defaults (#211). Parse errors (above) are always reported.
|
|
81
81
|
if (!hasEditorConfig(cwd)) {
|
|
82
|
-
|
|
82
|
+
// #211: without project opt-in, do not nag about formatting. But
|
|
83
|
+
// shfmt DID exit 1 — differences exist — so this is "chose not to
|
|
84
|
+
// check", reported as skipped, never as a clean result (#1839).
|
|
85
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
83
86
|
}
|
|
84
87
|
// Extract first changed line from diff if possible
|
|
85
88
|
const diffOutput = result.stdout ?? result.stderr ?? "";
|
|
@@ -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
|
}
|