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.
Files changed (157) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/README.md +45 -2
  3. package/dist/clients/actionable-warnings.js +44 -16
  4. package/dist/clients/advisory-provenance.js +187 -51
  5. package/dist/clients/cache-manager.js +64 -5
  6. package/dist/clients/cascade-format.js +24 -0
  7. package/dist/clients/child-unref.js +17 -8
  8. package/dist/clients/config-diagnostic-codes.js +2 -0
  9. package/dist/clients/config-locations.js +237 -9
  10. package/dist/clients/config-resolve.js +7 -1
  11. package/dist/clients/degradation-ledger.js +33 -3
  12. package/dist/clients/dependency-checker.js +87 -64
  13. package/dist/clients/diagnostic-dispositions.js +39 -3
  14. package/dist/clients/dispatch/finding-policy.js +118 -1
  15. package/dist/clients/dispatch/indent-detect.js +8 -2
  16. package/dist/clients/dispatch/inline-suppressions.js +23 -4
  17. package/dist/clients/dispatch/rule-ignores.js +14 -7
  18. package/dist/clients/dispatch/runners/actionlint.js +26 -28
  19. package/dist/clients/dispatch/runners/biome-check.js +38 -25
  20. package/dist/clients/dispatch/runners/cpp-check.js +32 -49
  21. package/dist/clients/dispatch/runners/credo.js +16 -14
  22. package/dist/clients/dispatch/runners/cue-vet.js +25 -7
  23. package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  24. package/dist/clients/dispatch/runners/detekt.js +11 -4
  25. package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  26. package/dist/clients/dispatch/runners/elixir-check.js +31 -46
  27. package/dist/clients/dispatch/runners/eslint.js +29 -46
  28. package/dist/clients/dispatch/runners/gleam-check.js +91 -11
  29. package/dist/clients/dispatch/runners/go-vet.js +16 -2
  30. package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  31. package/dist/clients/dispatch/runners/hadolint.js +27 -15
  32. package/dist/clients/dispatch/runners/htmlhint.js +17 -8
  33. package/dist/clients/dispatch/runners/javac.js +31 -21
  34. package/dist/clients/dispatch/runners/ktlint.js +34 -60
  35. package/dist/clients/dispatch/runners/lsp.js +6 -5
  36. package/dist/clients/dispatch/runners/markdownlint.js +12 -5
  37. package/dist/clients/dispatch/runners/mypy.js +2 -1
  38. package/dist/clients/dispatch/runners/oxlint.js +22 -6
  39. package/dist/clients/dispatch/runners/php-lint.js +39 -16
  40. package/dist/clients/dispatch/runners/phpstan.js +1 -1
  41. package/dist/clients/dispatch/runners/rubocop.js +27 -18
  42. package/dist/clients/dispatch/runners/ruff.js +18 -19
  43. package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  44. package/dist/clients/dispatch/runners/shellcheck.js +6 -2
  45. package/dist/clients/dispatch/runners/spellcheck.js +10 -3
  46. package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  47. package/dist/clients/dispatch/runners/stylelint.js +10 -2
  48. package/dist/clients/dispatch/runners/swiftlint.js +11 -2
  49. package/dist/clients/dispatch/runners/taplo.js +12 -6
  50. package/dist/clients/dispatch/runners/tflint.js +30 -15
  51. package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  52. package/dist/clients/dispatch/runners/trivy-config.js +8 -2
  53. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  54. package/dist/clients/dispatch/runners/vale.js +15 -3
  55. package/dist/clients/dispatch/runners/yamllint.js +12 -2
  56. package/dist/clients/dispatch/runners/zig-check.js +27 -39
  57. package/dist/clients/dispatch/suppress-writer.js +13 -3
  58. package/dist/clients/effective-config.js +3 -1
  59. package/dist/clients/extension-log.js +48 -1
  60. package/dist/clients/finding-delivery-gate.js +53 -23
  61. package/dist/clients/formatters.js +49 -11
  62. package/dist/clients/git-guard.js +247 -34
  63. package/dist/clients/gradle-ktfmt-style.js +121 -25
  64. package/dist/clients/inline-blocker-dispositions.js +186 -0
  65. package/dist/clients/install-diagnostics.js +19 -6
  66. package/dist/clients/installer/index.js +281 -32
  67. package/dist/clients/jscpd-client.js +8 -0
  68. package/dist/clients/latency-logger.js +8 -0
  69. package/dist/clients/lens-config.js +66 -8
  70. package/dist/clients/lsp/cascade-tier.js +3 -0
  71. package/dist/clients/lsp/client.js +139 -11
  72. package/dist/clients/lsp/config.js +9 -7
  73. package/dist/clients/lsp/content-limits.js +41 -0
  74. package/dist/clients/lsp/index.js +42 -8
  75. package/dist/clients/lsp/launch.js +23 -2
  76. package/dist/clients/lsp/server.js +106 -25
  77. package/dist/clients/lsp/sync-kind.js +14 -0
  78. package/dist/clients/lsp/wait-policy/strategies.js +105 -16
  79. package/dist/clients/mcp/analyze.js +9 -2
  80. package/dist/clients/mcp/ipc.js +232 -44
  81. package/dist/clients/mcp/review.js +53 -7
  82. package/dist/clients/opengrep-client.js +11 -6
  83. package/dist/clients/path-utils.js +51 -9
  84. package/dist/clients/persistent-reverify.js +271 -0
  85. package/dist/clients/pipeline.js +136 -67
  86. package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  87. package/dist/clients/project-lens-config.js +11 -3
  88. package/dist/clients/review-graph/builder.js +34 -26
  89. package/dist/clients/review-graph/git-identity.js +16 -10
  90. package/dist/clients/ruff-client.js +31 -4
  91. package/dist/clients/runtime-coordinator.js +38 -4
  92. package/dist/clients/runtime-tool-call.js +16 -5
  93. package/dist/clients/runtime-tool-result.js +27 -12
  94. package/dist/clients/runtime-turn.js +571 -287
  95. package/dist/clients/safe-spawn.js +256 -65
  96. package/dist/clients/skills-resolver.js +38 -2
  97. package/dist/clients/spawn-output-cap.js +84 -1
  98. package/dist/clients/tree-sitter-client.js +119 -58
  99. package/dist/clients/turn-end/lane.js +51 -0
  100. package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  101. package/dist/clients/turn-end/lanes/secrets.js +204 -0
  102. package/dist/clients/warm-attach.js +59 -0
  103. package/dist/clients/widget-state.js +294 -17
  104. package/dist/clients/workspace-topology.js +57 -3
  105. package/dist/clients/zizmor-config.js +26 -13
  106. package/dist/index.js +6342 -3995
  107. package/dist/mcp/analyze-cli.js +4 -0
  108. package/dist/mcp/server.js +33 -21
  109. package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  110. package/dist/tools/lens-diagnostic-mark.js +14 -2
  111. package/dist/tools/lens-diagnostics.js +127 -33
  112. package/dist/tools/lsp-diagnostics.js +150 -12
  113. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  114. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  115. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  116. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  117. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  118. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  119. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  120. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  121. package/docs/adr/0009-reported-path-attribution.md +114 -0
  122. package/docs/adr/README.md +26 -0
  123. package/docs/agent-tools.md +18 -1
  124. package/docs/ast-grep_rules_catalog.md +4 -3
  125. package/docs/configuration.md +55 -1
  126. package/docs/engineering-principles.md +202 -0
  127. package/docs/environment-variables.md +61 -4
  128. package/docs/features.md +2 -2
  129. package/docs/globalconfig.md +6 -1
  130. package/docs/language-coverage.md +17 -0
  131. package/docs/lsp-capability-matrix.md +98 -31
  132. package/docs/pi-lens-fixer.md +57 -3
  133. package/docs/pi-lens-investigator.md +9 -0
  134. package/docs/pi-lens-reviewer.md +16 -0
  135. package/docs/pi-lens-subagent.md +9 -0
  136. package/docs/public-api-stability.md +1 -0
  137. package/docs/release-qa-baseline.md +2 -0
  138. package/docs/servercapabilities.md +21 -11
  139. package/docs/settings.md +12 -3
  140. package/docs/subagent-compat.md +1 -1
  141. package/package.json +5 -3
  142. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  143. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  144. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  145. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  146. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  147. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  148. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  149. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  150. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  151. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  152. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  153. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  154. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  155. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  156. package/scripts/install-selftest.mjs +21 -17
  157. package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
+ import { pathsEqual } from "../../path-utils.js";
24
25
  import { safeSpawnAsync } from "../../safe-spawn.js";
25
26
  import { resolveRunnerCwd } from "../../tool-cwd.js";
26
27
  import { PRIORITY } from "../priorities.js";
@@ -61,8 +62,9 @@ function findShellcheckConfig(cwd) {
61
62
  *
62
63
  * Levels: "error", "warning", "info", "style"
63
64
  */
64
- function parseShellcheckOutput(raw, filePath) {
65
+ function parseShellcheckOutput(raw, filePath, cwd) {
65
66
  const diagnostics = [];
67
+ const absTarget = path.resolve(cwd, filePath);
66
68
  if (!raw.trim()) {
67
69
  return diagnostics;
68
70
  }
@@ -74,6 +76,8 @@ function parseShellcheckOutput(raw, filePath) {
74
76
  for (const item of parsed) {
75
77
  if (!item.message || !item.line)
76
78
  continue;
79
+ if (!item.file || !pathsEqual(path.resolve(cwd, item.file), absTarget))
80
+ continue;
77
81
  // Map shellcheck levels to our severity
78
82
  const severityMap = {
79
83
  error: "error",
@@ -157,7 +161,7 @@ const shellcheckRunner = {
157
161
  }
158
162
  // Parse diagnostics
159
163
  const raw = result.stdout + result.stderr;
160
- const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
164
+ const diagnostics = parseShellcheckOutput(raw, ctx.filePath, cwd);
161
165
  return finishParsedRun({
162
166
  tool: "shellcheck",
163
167
  ctx,
@@ -18,12 +18,14 @@
18
18
  * Install: cargo install typos-cli
19
19
  * Or: npm install -g typos-cli (if wrapped)
20
20
  */
21
+ import * as path from "node:path";
22
+ import { pathsEqual } from "../../path-utils.js";
21
23
  import { safeSpawnAsync } from "../../safe-spawn.js";
22
24
  import { PRIORITY } from "../priorities.js";
23
25
  import { createAvailabilityChecker, resolveRunnerCwd, } from "./utils/runner-helpers.js";
24
26
  import { parseToolRun } from "./utils/tool-failure.js";
25
27
  const typos = createAvailabilityChecker("typos", ".exe");
26
- // typos-cli exit codes: 0 = no typos, 2 = typos found, 1 = an error that
28
+ // EXIT TABLE (typos-cli 1.29 docs https://github.com/crate-ci/typos): 0 = no typos, 2 = typos found, 1 = an error that
27
29
  // stopped the scan. Anything nonzero outside {2} is a rejected invocation.
28
30
  const TYPOS_EXIT_CODES = { ran: [2] };
29
31
  /**
@@ -38,8 +40,9 @@ const TYPOS_EXIT_CODES = { ran: [2] };
38
40
  * "corrections": ["receive"]
39
41
  * }
40
42
  */
41
- function parseTyposOutput(raw, filePath) {
43
+ function parseTyposOutput(raw, filePath, cwd) {
42
44
  const diagnostics = [];
45
+ const absTarget = path.resolve(cwd, filePath);
43
46
  if (!raw.trim()) {
44
47
  return diagnostics;
45
48
  }
@@ -52,6 +55,10 @@ function parseTyposOutput(raw, filePath) {
52
55
  const parsed = JSON.parse(line);
53
56
  if (!parsed.typo || !parsed.line_num)
54
57
  continue;
58
+ // #3295: every typos JSONL record names its own `path`; a `typos.toml`
59
+ // `files.extend-exclude` or a directory argv makes that a second file.
60
+ if (parsed.path && !pathsEqual(path.resolve(cwd, parsed.path), absTarget))
61
+ continue;
55
62
  const corrections = parsed.corrections?.join(", ") || "no suggestions";
56
63
  const message = `Typo: "${parsed.typo}" → ${corrections}`;
57
64
  diagnostics.push({
@@ -104,7 +111,7 @@ const spellcheckRunner = {
104
111
  // yield nothing leaves a record. `parseOutput` keeps the historical
105
112
  // split: the gate judges "did it run" on stdout, the parser reads both
106
113
  // streams.
107
- const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
114
+ const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath, cwd), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
108
115
  if (run.skipped)
109
116
  return run.skipped;
110
117
  const diagnostics = run.diagnostics;
@@ -1,3 +1,5 @@
1
+ import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
1
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
4
  import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
3
5
  import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
@@ -11,6 +13,7 @@ const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
11
13
  // Only 2 is a rejected invocation.
12
14
  //
13
15
  // The exit TABLE is what protects this runner, not the nothing-to-parse rule:
16
+ // EXIT TABLE (SQLFluff 3.1 docs https://docs.sqlfluff.com/en/stable/production.html): 0 clean; 1 findings/error; other nonzero rejected.
14
17
  // sqlfluff writes its user errors to STDOUT, not stderr (live: an unknown
15
18
  // dialect prints the error there), so stdout is non-empty and the fallback
16
19
  // rule would read the failed run as a run that found nothing.
@@ -73,7 +76,7 @@ const SQLFLUFF_FIXABLE_RULES = new Set([
73
76
  "ST01",
74
77
  "ST02",
75
78
  ]);
76
- function parseSqlfluffOutput(raw, filePath) {
79
+ function parseSqlfluffOutput(raw, filePath, cwd) {
77
80
  if (!raw.trim())
78
81
  return [];
79
82
  try {
@@ -81,7 +84,13 @@ function parseSqlfluffOutput(raw, filePath) {
81
84
  if (!Array.isArray(parsed))
82
85
  return [];
83
86
  const diagnostics = [];
87
+ const absTarget = path.resolve(cwd, filePath);
84
88
  for (const item of parsed) {
89
+ // #3295: sqlfluff's JSON is an array of FILES; a jinja `{% include %}`
90
+ // or a directory argv puts a second one in it.
91
+ if (item.filepath &&
92
+ !pathsEqual(path.resolve(cwd, item.filepath), absTarget))
93
+ continue;
85
94
  for (const v of item.violations ?? []) {
86
95
  if (!v.description)
87
96
  continue;
@@ -158,7 +167,7 @@ const sqlfluffRunner = {
158
167
  // reported the SQL file as clean.
159
168
  // #1948: sqlfluff exit 1 with a JSON report we read nothing out of is a
160
169
  // parser break, not a clean file.
161
- const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath), { parseOutput: result.stdout ?? "" });
170
+ const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath, cwd), { parseOutput: result.stdout ?? "" });
162
171
  if (run.skipped)
163
172
  return run.skipped;
164
173
  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 { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js";
@@ -18,6 +19,7 @@ const stylelint = createAvailabilityChecker("stylelint", ".cmd");
18
19
  // exists to name.
19
20
  //
20
21
  // #1937 corrected the safety argument that used to sit here: it claimed a
22
+ // EXIT TABLE (Stylelint 16 docs https://stylelint.io/user-guide/cli): 0 clean; 1 findings; 2 fatal/error; other nonzero rejected.
21
23
  // fatal stylelint "writes nothing to stdout", which is true but useless,
22
24
  // because a stylelint that DID find problems writes nothing to stdout either.
23
25
  // `stylelintReport` below is what now separates the two.
@@ -80,11 +82,17 @@ const STYLELINT_FIXABLE_RULES = new Set([
80
82
  "string-quotes",
81
83
  "value-no-vendor-prefix",
82
84
  ]);
83
- function parseStylelintJson(raw, filePath) {
85
+ function parseStylelintJson(raw, filePath, cwd) {
84
86
  try {
85
87
  const results = JSON.parse(raw);
86
88
  const diagnostics = [];
89
+ const absTarget = path.resolve(cwd, filePath);
87
90
  for (const result of results) {
91
+ // #3295: stylelint keys each result by its own `source`; a config
92
+ // `files`/`overrides` entry or an `@import` makes that a DIFFERENT file.
93
+ if (result.source &&
94
+ !pathsEqual(path.resolve(cwd, result.source), absTarget))
95
+ continue;
88
96
  for (const w of result.warnings) {
89
97
  const severity = w.severity === "error" ? "error" : "warning";
90
98
  const fixable = STYLELINT_FIXABLE_RULES.has(w.rule);
@@ -143,7 +151,7 @@ const stylelintRunner = {
143
151
  const raw = stylelintReport(result.stdout, result.stderr);
144
152
  // #1948: the stdout-vs-stderr report bug landed here as exit 2 plus a
145
153
  // full JSON report and zero parsed warnings. That now leaves a record.
146
- const run = parseToolRun("stylelint", { result, output: raw, exitCodes: STYLELINT_EXIT_CODES }, (out) => parseStylelintJson(out, ctx.filePath));
154
+ const run = parseToolRun("stylelint", { result, output: raw, exitCodes: STYLELINT_EXIT_CODES }, (out) => parseStylelintJson(out, ctx.filePath, cwd));
147
155
  if (run.skipped)
148
156
  return run.skipped;
149
157
  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 { PRIORITY } from "../priorities.js";
@@ -68,10 +69,11 @@ const SWIFTLINT_FIXABLE_RULES = new Set([
68
69
  "void_return",
69
70
  "yoda_condition",
70
71
  ]);
71
- function parseSwiftLintOutput(raw, filePath) {
72
+ function parseSwiftLintOutput(raw, filePath, cwd) {
72
73
  const diagnostics = [];
73
74
  if (!raw.trim())
74
75
  return diagnostics;
76
+ const absTarget = path.resolve(cwd, filePath);
75
77
  try {
76
78
  const parsed = JSON.parse(raw);
77
79
  if (!Array.isArray(parsed))
@@ -79,6 +81,9 @@ function parseSwiftLintOutput(raw, filePath) {
79
81
  for (const item of parsed) {
80
82
  if (!item.reason)
81
83
  continue;
84
+ // #3295: `.swiftlint.yml` `included:` widens the run past the argv.
85
+ if (item.file && !pathsEqual(path.resolve(cwd, item.file), absTarget))
86
+ continue;
82
87
  const severityMap = {
83
88
  error: "error",
84
89
  warning: "warning",
@@ -135,7 +140,11 @@ const swiftlintRunner = {
135
140
  // never ran.
136
141
  // SwiftLint exits non-zero on violations — stdout still has the JSON, so
137
142
  // a nonzero exit whose JSON yields nothing is a parser break (#1948).
138
- const run = parseToolRun("swiftlint", { result }, (out) => parseSwiftLintOutput(out, ctx.filePath));
143
+ const run = parseToolRun("swiftlint", {
144
+ result,
145
+ // EXIT TABLE (SwiftLint 0.56 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
146
+ exitCodes: { ran: [1, 2] },
147
+ }, (out) => parseSwiftLintOutput(out, ctx.filePath, cwd));
139
148
  if (run.skipped)
140
149
  return run.skipped;
141
150
  const diagnostics = run.diagnostics;
@@ -1,5 +1,7 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { findLocalBinUpwards } from "../../package-manager.js";
4
+ import { stripAnsi } from "../../sanitize.js";
3
5
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
6
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
7
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
@@ -12,6 +14,7 @@ const taplo = createAvailabilityChecker("taplo", ".exe");
12
14
  //
13
15
  // #1937 round 2: the parser fix alone turned the old silent-clean into a false
14
16
  // BLOCKING diagnostic. clap prints `error: unexpected argument '--output'
17
+ // EXIT TABLE (taplo 0.9 measured fixture): 0 clean; 1 findings; other nonzero rejected/error.
15
18
  // found`, which starts with the same lowercase `error:` a real taplo
16
19
  // diagnostic does, so a mistyped flag would have reddened every valid TOML
17
20
  // file. Both readings are wrong; the exit code is what separates them.
@@ -21,7 +24,7 @@ const HEADER_PATTERN = /^\s*(error|warning):(.*)$/;
21
24
  /** The box-drawing lead taplo puts on a codespan location line. */
22
25
  const LOCATION_LEAD = "┌─";
23
26
  /** The `:line:column` tail of a codespan location line. */
24
- const LOCATION_PATTERN = /:(\d+):(\d+)$/;
27
+ const LOCATION_PATTERN = /^\s*┌─\s+(.+?):(\d+):(\d+)$/;
25
28
  /**
26
29
  * Parse `taplo lint` output (#1937).
27
30
  *
@@ -48,9 +51,10 @@ const LOCATION_PATTERN = /:(\d+):(\d+)$/;
48
51
  * (#1937 round 2). The exit-code table in the runner is the primary guard;
49
52
  * this is the structural one, and each covers a case the other does not.
50
53
  */
51
- export function parseTaploOutput(raw, filePath) {
54
+ export function parseTaploOutput(raw, filePath, cwd) {
52
55
  const diagnostics = [];
53
- const lines = (raw ?? "").split(/\r?\n/);
56
+ const absTarget = path.resolve(cwd, filePath);
57
+ const lines = stripAnsi(raw ?? "").split(/\r?\n/);
54
58
  for (let i = 0; i < lines.length; i++) {
55
59
  // Lowercase `error:`/`warning:` starts a diagnostic. taplo's tracing
56
60
  // lines are uppercase (`ERROR taplo:lint_files: ...`), so a
@@ -79,8 +83,10 @@ export function parseTaploOutput(raw, filePath) {
79
83
  const location = LOCATION_PATTERN.exec(lines[j].trimEnd());
80
84
  if (!location)
81
85
  continue;
82
- line = Number.parseInt(location[1], 10) || 1;
83
- column = Number.parseInt(location[2], 10) || 1;
86
+ if (!pathsEqual(path.resolve(cwd, location[1].trim()), absTarget))
87
+ continue;
88
+ line = Number.parseInt(location[2], 10) || 1;
89
+ column = Number.parseInt(location[3], 10) || 1;
84
90
  break;
85
91
  }
86
92
  if (line === null)
@@ -155,7 +161,7 @@ const taploRunner = {
155
161
  // outright lie, so taplo opts into `skipWhenParsedNothing` (#1948). This
156
162
  // replaces a hand-rolled per-runner copy of the same rule, which wrote
157
163
  // its own `runner-empty-result` row with its own wording.
158
- const run = parseToolRun("taplo", { result, output: raw, exitCodes: TAPLO_EXIT_CODES }, (out) => parseTaploOutput(out, ctx.filePath), { skipWhenParsedNothing: true });
164
+ const run = parseToolRun("taplo", { result, output: raw, exitCodes: TAPLO_EXIT_CODES }, (out) => parseTaploOutput(out, ctx.filePath, cwd), { skipWhenParsedNothing: true });
159
165
  if (run.skipped)
160
166
  return run.skipped;
161
167
  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";
@@ -23,24 +24,34 @@ function findTflintConfig(fileDir) {
23
24
  const dir = findNearestDirWithAnyBasename(fileDir, [TFLINT_CONFIG]);
24
25
  return dir ? path.join(dir, TFLINT_CONFIG) : null;
25
26
  }
26
- function parseTflintOutput(raw, filePath) {
27
+ function parseTflintOutput(raw, filePath, toolCwd) {
27
28
  try {
28
29
  const parsed = JSON.parse(raw);
29
30
  const issues = parsed.issues ?? [];
30
- return issues.map((issue) => {
31
+ // #3295: tflint loads EVERY `*.tf` in its working directory — `--filter` is
32
+ // best-effort — and names each issue's own `range.filename`, relative to
33
+ // the directory it ran in (`toolCwd`, which is the file's dir, not the
34
+ // runner cwd).
35
+ const absTarget = path.resolve(toolCwd, filePath);
36
+ return issues.flatMap((issue) => {
37
+ if (issue.range?.filename &&
38
+ !pathsEqual(path.resolve(toolCwd, issue.range.filename), absTarget))
39
+ return [];
31
40
  const severity = issue.rule.severity === "error" ? "error" : "warning";
32
- return {
33
- id: `tflint-${issue.rule.name}-${issue.range.start.line}`,
34
- message: `[${issue.rule.name}] ${issue.message}`,
35
- filePath,
36
- line: issue.range.start.line,
37
- column: issue.range.start.column,
38
- severity,
39
- semantic: severity === "error" ? "blocking" : "warning",
40
- tool: "tflint",
41
- rule: issue.rule.name,
42
- fixable: false,
43
- };
41
+ return [
42
+ {
43
+ id: `tflint-${issue.rule.name}-${issue.range.start.line}`,
44
+ message: `[${issue.rule.name}] ${issue.message}`,
45
+ filePath,
46
+ line: issue.range.start.line,
47
+ column: issue.range.start.column,
48
+ severity,
49
+ semantic: severity === "error" ? "blocking" : "warning",
50
+ tool: "tflint",
51
+ rule: issue.rule.name,
52
+ fixable: false,
53
+ },
54
+ ];
44
55
  });
45
56
  }
46
57
  catch {
@@ -88,7 +99,11 @@ const tflintRunner = {
88
99
  // not a clean file. No exit-code table: tflint's nonzero codes are not
89
100
  // verified against a real binary here, so the conservative
90
101
  // nothing-to-parse rule stays the only discriminator.
91
- const run = parseToolRun("tflint", { result }, (out) => parseTflintOutput(out, ctx.filePath));
102
+ const run = parseToolRun("tflint", {
103
+ result,
104
+ // EXIT TABLE (TFLint 0.52 measured fixture): 0 clean; 1 error; 2 findings; other nonzero rejected.
105
+ exitCodes: { ran: [1, 2] },
106
+ }, (out) => parseTflintOutput(out, absPath, fileDir));
92
107
  if (run.skipped)
93
108
  return run.skipped;
94
109
  const diagnostics = run.diagnostics;
@@ -585,6 +585,29 @@ const treeSitterRunner = {
585
585
  }
586
586
  }
587
587
  if (diagnostics.length === 0) {
588
+ // #3409: zero diagnostics because the language's GRAMMAR never loaded is
589
+ // not a clean file, and `runQueriesOnFile` returns [] for both. The
590
+ // honest discriminator, once the run has already demanded the language,
591
+ // is the client's own loaded-language map: a grammar that could not be
592
+ // resolved, fetched, or decoded is absent from it. Reported through this
593
+ // runner's existing "the analysis did not run" arm (the same
594
+ // `runner_skip` + `status: "skipped"` shape as client-unavailable,
595
+ // init-failed and unsupported-extension) instead of as a pass with
596
+ // nothing found — which is what made a degraded language on a compiled
597
+ // host indistinguishable from a clean file in `runner_complete`.
598
+ //
599
+ // The abort term keeps a mid-run WASM abort (#402, its own
600
+ // `runtime_abort` record) from being misreported as a missing grammar.
601
+ if (!isTreeSitterWasmAborted() && !client.getLanguage(languageId)) {
602
+ logTreeSitter({
603
+ phase: "runner_skip",
604
+ filePath,
605
+ languageId,
606
+ reason: "grammar_unavailable",
607
+ status: "skipped",
608
+ });
609
+ return { status: "skipped", diagnostics: [], semantic: "none" };
610
+ }
588
611
  logTreeSitter({
589
612
  phase: "runner_complete",
590
613
  filePath,
@@ -38,6 +38,7 @@
38
38
  import { formatToolFailure } from "./utils/tool-failure.js";
39
39
  import * as fs from "node:fs";
40
40
  import * as path from "node:path";
41
+ import { pathsEqual } from "../../path-utils.js";
41
42
  import { incrementDegradationCount } from "../../degradation-ledger.js";
42
43
  import { safeSpawnAsync } from "../../safe-spawn.js";
43
44
  import { resolveRunnerCwd } from "../../tool-cwd.js";
@@ -89,7 +90,7 @@ function normalizeSeverity(raw) {
89
90
  * CauseMetadata: { StartLine } }] }] }`. CRITICAL → blocking, the rest advisory.
90
91
  * Defensive: malformed input returns `[]`. Exported for unit tests.
91
92
  */
92
- export function parseTrivyConfigOutput(raw, filePath) {
93
+ export function parseTrivyConfigOutput(raw, filePath, cwd = process.cwd()) {
93
94
  if (!raw.trim())
94
95
  return [];
95
96
  let parsed;
@@ -103,9 +104,14 @@ export function parseTrivyConfigOutput(raw, filePath) {
103
104
  if (!Array.isArray(results))
104
105
  return [];
105
106
  const diagnostics = [];
107
+ const absTarget = path.resolve(cwd, filePath);
106
108
  for (const resultEntry of results) {
107
109
  if (!resultEntry || typeof resultEntry !== "object")
108
110
  continue;
111
+ const target = resultEntry.Target;
112
+ if (typeof target !== "string" ||
113
+ !pathsEqual(path.resolve(cwd, target), absTarget))
114
+ continue;
109
115
  const rows = resultEntry
110
116
  .Misconfigurations;
111
117
  if (!Array.isArray(rows))
@@ -232,7 +238,7 @@ const trivyConfigRunner = {
232
238
  });
233
239
  return { status: "skipped", diagnostics: [], semantic: "none" };
234
240
  }
235
- const diagnostics = parseTrivyConfigOutput(result.stdout || "", ctx.filePath);
241
+ const diagnostics = parseTrivyConfigOutput(result.stdout || "", ctx.filePath, cwd);
236
242
  if (diagnostics.length === 0) {
237
243
  return { status: "succeeded", diagnostics: [], semantic: "none" };
238
244
  }
@@ -4,6 +4,8 @@
4
4
  * Common patterns for parsing tool output into standardized diagnostics.
5
5
  * Supports the common `file:line:col: message` format used by most linters.
6
6
  */
7
+ import * as path from "node:path";
8
+ import { pathsEqual } from "../../../path-utils.js";
7
9
  import { stripAnsi } from "../../../sanitize.js";
8
10
  import { getAutofixCapability } from "../../../tool-policy.js";
9
11
  /**
@@ -11,8 +13,13 @@ import { getAutofixCapability } from "../../../tool-policy.js";
11
13
  * Common format: file:line:col: message (with variations)
12
14
  */
13
15
  function createLineParser(config) {
14
- return (raw, filePath) => {
16
+ return (raw, filePath, cwd) => {
15
17
  const diagnostics = [];
18
+ // #3295: `config.regex` is documented above as capturing the FILE in group
19
+ // 1, and every parser built here then dropped it and stamped the dispatched
20
+ // path on every line. This is the shared factory, so the predicate lives
21
+ // here once rather than in each `createLineParser` caller.
22
+ const absTarget = path.resolve(cwd, filePath);
16
23
  // Optionally strip ANSI codes (for tools that output colored text)
17
24
  const clean = config.stripAnsi !== false ? stripAnsi(raw) : raw;
18
25
  const lines = clean.split("\n").filter((l) => l.trim());
@@ -20,6 +27,9 @@ function createLineParser(config) {
20
27
  const match = line.match(config.regex);
21
28
  if (!match)
22
29
  continue;
30
+ const reported = match[1];
31
+ if (reported && !pathsEqual(path.resolve(cwd, reported), absTarget))
32
+ continue;
23
33
  const lineNum = parseInt(match[2], 10);
24
34
  const colNum = parseInt(match[3], 10);
25
35
  const severity = config.getSeverity
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { pathsEqual } from "../../path-utils.js";
3
4
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { PRIORITY } from "../priorities.js";
@@ -25,17 +26,24 @@ function findValeConfig(cwd) {
25
26
  }
26
27
  return undefined;
27
28
  }
28
- export function parseValeOutput(raw, filePath) {
29
+ export function parseValeOutput(raw, filePath, cwd) {
29
30
  const diagnostics = [];
30
31
  if (!raw.trim())
31
32
  return diagnostics;
33
+ const absTarget = path.resolve(cwd, filePath);
32
34
  try {
33
35
  const parsed = JSON.parse(raw);
34
36
  if (!parsed || typeof parsed !== "object")
35
37
  return diagnostics;
36
- for (const alerts of Object.values(parsed)) {
38
+ // #3295: vale's top level is keyed BY the linted file's path — the only
39
+ // place the reported path appears. `Object.values` dropped it, so a
40
+ // `.vale.ini` glob or a directory argv charged a second file's alerts to
41
+ // the dispatched one.
42
+ for (const [reported, alerts] of Object.entries(parsed)) {
37
43
  if (!Array.isArray(alerts))
38
44
  continue;
45
+ if (reported && !pathsEqual(path.resolve(cwd, reported), absTarget))
46
+ continue;
39
47
  for (const alert of alerts) {
40
48
  if (!alert.Message)
41
49
  continue;
@@ -98,7 +106,11 @@ const valeRunner = {
98
106
  // #1948: `parseToolRun` adds the second gate. Vale's `Data.Files`
99
107
  // envelope bug produced exactly this shape — exit 1, a full JSON
100
108
  // report on stdout, zero alerts parsed — and left no record.
101
- const run = parseToolRun("vale", { result }, (raw) => parseValeOutput(raw, ctx.filePath));
109
+ const run = parseToolRun("vale", {
110
+ result,
111
+ // EXIT TABLE (Vale 3.9.6 docs https://vale.sh/docs/topics/metrics/): 0 clean; 1 findings; 2 error; other nonzero rejected.
112
+ exitCodes: { ran: [1, 2] },
113
+ }, (raw) => parseValeOutput(raw, ctx.filePath, cwd));
102
114
  if (run.skipped)
103
115
  return run.skipped;
104
116
  const diagnostics = run.diagnostics;
@@ -1,3 +1,5 @@
1
+ import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
1
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
4
  import { logRunnerAdvisoryOnce } from "../../tool-cwd.js";
3
5
  import { getLinterPolicyForCwd, hasYamllintConfig } from "../../tool-policy.js";
@@ -7,14 +9,17 @@ import { parseToolRun } from "./utils/tool-failure.js";
7
9
  import { finishParsedRun } from "./utils/tool-failure.js";
8
10
  const yamllint = createAvailabilityChecker("yamllint", ".exe");
9
11
  export { hasYamllintConfig };
10
- function parseYamllintParsable(raw, filePath) {
12
+ function parseYamllintParsable(raw, filePath, cwd) {
11
13
  const diagnostics = [];
14
+ const absTarget = path.resolve(cwd, filePath);
12
15
  for (const line of raw.split(/\r?\n/)) {
13
16
  if (!line.trim())
14
17
  continue;
15
18
  const match = line.match(/^(.*?):(\d+):(\d+):\s*\[(error|warning)\]\s*(.*?)\s*\(([^)]+)\)\s*$/i);
16
19
  if (!match)
17
20
  continue;
21
+ if (!pathsEqual(path.resolve(cwd, match[1]), absTarget))
22
+ continue;
18
23
  const severity = match[4].toLowerCase() === "error" ? "error" : "warning";
19
24
  diagnostics.push({
20
25
  id: `yamllint-${match[2]}-${match[3]}-${match[6]}`,
@@ -70,7 +75,12 @@ const yamllintRunner = {
70
75
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
71
76
  // #1948: classify on stdout as before, parse both streams as before, and
72
77
  // record when a failing run yields nothing.
73
- const run = parseToolRun("yamllint", { result, output: result.stdout }, (out) => parseYamllintParsable(out, ctx.filePath), { parseOutput: raw });
78
+ const run = parseToolRun("yamllint", {
79
+ result,
80
+ output: result.stdout,
81
+ // EXIT TABLE (yamllint 1.35 docs https://yamllint.readthedocs.io/en/stable/): 0 clean; 1 findings; 2 error; other nonzero rejected.
82
+ exitCodes: { ran: [1, 2] },
83
+ }, (out) => parseYamllintParsable(out, ctx.filePath, cwd), { parseOutput: raw });
74
84
  if (run.skipped)
75
85
  return run.skipped;
76
86
  const diagnostics = run.diagnostics;
@@ -1,21 +1,23 @@
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 { createAvailabilityChecker } from "./utils/runner-helpers.js";
5
6
  import { PRIORITY } from "../priorities.js";
7
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
6
8
  // zig rejects `--version`; the version subcommand is `zig version`. Using the
7
9
  // default probe would make this runner skip on every machine.
8
10
  const zig = createAvailabilityChecker("zig", ".exe", ["version"]);
9
- function parseZigOutput(raw, filePath) {
11
+ function parseZigOutput(raw, filePath, cwd) {
10
12
  const diagnostics = [];
13
+ const absTarget = path.resolve(cwd, filePath);
11
14
  for (const line of raw.split(/\r?\n/)) {
12
15
  const match = line.match(/^(.*?):(\d+):(\d+):\s*(error|warning|note):\s*(.+)$/);
13
16
  if (!match)
14
17
  continue;
15
18
  const [, rawFile, lineStr, colStr, level, message] = match;
16
- const resolvedSource = path.resolve(rawFile.trim());
17
- const resolvedTarget = path.resolve(filePath);
18
- if (resolvedSource !== resolvedTarget)
19
+ // #3278: one seam for reported-path attribution — see javac.ts.
20
+ if (!pathsEqual(path.resolve(cwd, rawFile.trim()), absTarget))
19
21
  continue;
20
22
  const severity = level === "error" ? "error" : "warning";
21
23
  diagnostics.push({
@@ -35,12 +37,6 @@ function parseZigOutput(raw, filePath) {
35
37
  }
36
38
  return diagnostics;
37
39
  }
38
- function firstOutputLine(result) {
39
- return `${result.stderr || ""}\n${result.stdout || ""}`
40
- .trim()
41
- .split(/\r?\n/, 1)[0]
42
- .slice(0, 200);
43
- }
44
40
  const zigCheckRunner = {
45
41
  id: "zig-check",
46
42
  appliesTo: ["zig"],
@@ -57,35 +53,27 @@ const zigCheckRunner = {
57
53
  if (result.error && !result.stdout && !result.stderr) {
58
54
  return { status: "skipped", diagnostics: [], semantic: "none" };
59
55
  }
60
- const diagnostics = parseZigOutput(`${result.stderr || ""}\n${result.stdout || ""}`, ctx.filePath);
61
- if (diagnostics.length === 0) {
62
- if (result.status && result.status !== 0) {
63
- return {
64
- status: "failed",
65
- diagnostics: [
66
- {
67
- id: "zig-check-nonzero-no-diagnostics",
68
- message: firstOutputLine(result) ||
69
- "zig build-exe exited non-zero without structured diagnostics",
70
- filePath: ctx.filePath,
71
- severity: "warning",
72
- semantic: "warning",
73
- tool: "zig",
74
- rule: "zig-check",
75
- fixable: false,
76
- },
77
- ],
78
- semantic: "warning",
79
- };
80
- }
81
- return { status: "succeeded", diagnostics: [], semantic: "none" };
82
- }
83
- const hasErrors = diagnostics.some((d) => d.severity === "error");
84
- return {
85
- status: hasErrors ? "failed" : "succeeded",
86
- diagnostics,
87
- semantic: "warning",
88
- };
56
+ const raw = `${result.stdout || ""}\n${result.stderr || ""}`;
57
+ const parsed = parseToolRun("zig-check", {
58
+ result,
59
+ output: raw,
60
+ // EXIT TABLE (Zig 0.13 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
61
+ exitCodes: { ran: [1, 2] },
62
+ }, (output) => parseZigOutput(output, ctx.filePath, cwd));
63
+ if (parsed.skipped)
64
+ return parsed.skipped;
65
+ return finishParsedRun({
66
+ tool: "zig-check",
67
+ ctx,
68
+ result,
69
+ diagnostics: parsed.diagnostics,
70
+ classify: (diagnostics) => ({
71
+ status: diagnostics.some((d) => d.severity === "error")
72
+ ? "failed"
73
+ : "succeeded",
74
+ semantic: "warning",
75
+ }),
76
+ });
89
77
  },
90
78
  };
91
79
  export default zigCheckRunner;