@paretools/go 0.8.4 → 0.9.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 (50) hide show
  1. package/dist/lib/formatters.d.ts +27 -10
  2. package/dist/lib/formatters.d.ts.map +1 -1
  3. package/dist/lib/formatters.js +212 -30
  4. package/dist/lib/formatters.js.map +1 -1
  5. package/dist/lib/go-runner.d.ts +1 -1
  6. package/dist/lib/go-runner.d.ts.map +1 -1
  7. package/dist/lib/go-runner.js +3 -3
  8. package/dist/lib/go-runner.js.map +1 -1
  9. package/dist/lib/parsers.d.ts +45 -10
  10. package/dist/lib/parsers.d.ts.map +1 -1
  11. package/dist/lib/parsers.js +490 -33
  12. package/dist/lib/parsers.js.map +1 -1
  13. package/dist/schemas/index.d.ts +148 -4
  14. package/dist/schemas/index.d.ts.map +1 -1
  15. package/dist/schemas/index.js +96 -5
  16. package/dist/schemas/index.js.map +1 -1
  17. package/dist/tools/build.d.ts.map +1 -1
  18. package/dist/tools/build.js +61 -2
  19. package/dist/tools/build.js.map +1 -1
  20. package/dist/tools/env.d.ts.map +1 -1
  21. package/dist/tools/env.js +14 -4
  22. package/dist/tools/env.js.map +1 -1
  23. package/dist/tools/fmt.d.ts.map +1 -1
  24. package/dist/tools/fmt.js +26 -3
  25. package/dist/tools/fmt.js.map +1 -1
  26. package/dist/tools/generate.d.ts.map +1 -1
  27. package/dist/tools/generate.js +54 -2
  28. package/dist/tools/generate.js.map +1 -1
  29. package/dist/tools/get.d.ts.map +1 -1
  30. package/dist/tools/get.js +34 -3
  31. package/dist/tools/get.js.map +1 -1
  32. package/dist/tools/golangci-lint.d.ts.map +1 -1
  33. package/dist/tools/golangci-lint.js +144 -4
  34. package/dist/tools/golangci-lint.js.map +1 -1
  35. package/dist/tools/list.d.ts.map +1 -1
  36. package/dist/tools/list.js +66 -5
  37. package/dist/tools/list.js.map +1 -1
  38. package/dist/tools/mod-tidy.d.ts.map +1 -1
  39. package/dist/tools/mod-tidy.js +69 -4
  40. package/dist/tools/mod-tidy.js.map +1 -1
  41. package/dist/tools/run.d.ts.map +1 -1
  42. package/dist/tools/run.js +59 -6
  43. package/dist/tools/run.js.map +1 -1
  44. package/dist/tools/test.d.ts.map +1 -1
  45. package/dist/tools/test.js +86 -2
  46. package/dist/tools/test.js.map +1 -1
  47. package/dist/tools/vet.d.ts.map +1 -1
  48. package/dist/tools/vet.js +54 -4
  49. package/dist/tools/vet.js.map +1 -1
  50. package/package.json +2 -2
@@ -1,11 +1,37 @@
1
1
  const GO_ERROR_RE = /^(.+?\.go):(\d+)(?::(\d+))?: (.+)$/;
2
+ /**
3
+ * Regex to detect non-file build error lines. These are package-level or linker
4
+ * errors that don't match the standard file:line:col format.
5
+ * Matches: "package X is not in GOROOT", linker errors, build constraint errors, etc.
6
+ * Ignores: blank lines, "# package" headers (captured separately as context).
7
+ */
8
+ const GO_NON_FILE_ERROR_PATTERNS = [
9
+ /^package .+ is not in (?:GOROOT|GOPATH|std)/,
10
+ /^can(?:not|'t) (?:find|load) package/,
11
+ /^no required module provides package/,
12
+ /^build constraints exclude all Go files/,
13
+ /^imports .+ is not in/,
14
+ // Linker errors
15
+ /^(?:\/[^\s]+\/)?ld[.:]/,
16
+ /^(?:#\s+command-line-arguments|link:)/,
17
+ /undefined reference to/,
18
+ /multiple definition of/,
19
+ // CGO errors
20
+ /^cgo: /,
21
+ // Module errors
22
+ /^go: /,
23
+ ];
2
24
  /** Parses `go build` stderr output into structured error data with file locations. */
3
25
  export function parseGoBuildOutput(stdout, stderr, exitCode) {
4
26
  const output = stdout + "\n" + stderr;
5
27
  const lines = output.split("\n");
6
28
  const errors = [];
29
+ const rawErrors = [];
7
30
  for (const line of lines) {
8
- const match = line.match(GO_ERROR_RE);
31
+ const trimmed = line.trim();
32
+ if (!trimmed)
33
+ continue;
34
+ const match = trimmed.match(GO_ERROR_RE);
9
35
  if (match) {
10
36
  errors.push({
11
37
  file: match[1],
@@ -13,12 +39,22 @@ export function parseGoBuildOutput(stdout, stderr, exitCode) {
13
39
  column: match[3] ? parseInt(match[3], 10) : undefined,
14
40
  message: match[4],
15
41
  });
42
+ continue;
43
+ }
44
+ // Only capture non-file errors when the build actually failed
45
+ if (exitCode !== 0) {
46
+ const isNonFileError = GO_NON_FILE_ERROR_PATTERNS.some((re) => re.test(trimmed));
47
+ if (isNonFileError) {
48
+ rawErrors.push(trimmed);
49
+ }
16
50
  }
17
51
  }
52
+ const total = errors.length + rawErrors.length;
18
53
  return {
19
54
  success: exitCode === 0,
20
55
  errors,
21
- total: errors.length,
56
+ rawErrors: rawErrors.length > 0 ? rawErrors : undefined,
57
+ total,
22
58
  };
23
59
  }
24
60
  /**
@@ -28,7 +64,13 @@ export function parseGoBuildOutput(stdout, stderr, exitCode) {
28
64
  */
29
65
  export function parseGoTestJson(stdout, exitCode) {
30
66
  const lines = stdout.trim().split("\n").filter(Boolean);
67
+ // Collect output lines per test (keyed by package/test)
68
+ const testOutputMap = new Map();
69
+ // Collect output lines per package (for package-level output)
70
+ const pkgOutputMap = new Map();
31
71
  const testMap = new Map();
72
+ // Track package-level failures (where event.Test is undefined)
73
+ const pkgFailures = new Map();
32
74
  for (const line of lines) {
33
75
  let event;
34
76
  try {
@@ -37,18 +79,62 @@ export function parseGoTestJson(stdout, exitCode) {
37
79
  catch {
38
80
  continue;
39
81
  }
40
- if (!event.Test)
41
- continue;
42
- const key = `${event.Package}/${event.Test}`;
43
- if (event.Action === "pass" || event.Action === "fail" || event.Action === "skip") {
44
- testMap.set(key, {
45
- package: event.Package,
46
- name: event.Test,
47
- status: event.Action,
48
- elapsed: event.Elapsed,
49
- });
82
+ if (event.Test) {
83
+ const key = `${event.Package}/${event.Test}`;
84
+ // Collect output lines for tests (#51)
85
+ if (event.Action === "output" && event.Output) {
86
+ if (!testOutputMap.has(key)) {
87
+ testOutputMap.set(key, []);
88
+ }
89
+ testOutputMap.get(key).push(event.Output);
90
+ }
91
+ if (event.Action === "pass" || event.Action === "fail" || event.Action === "skip") {
92
+ testMap.set(key, {
93
+ package: event.Package,
94
+ name: event.Test,
95
+ status: event.Action,
96
+ elapsed: event.Elapsed,
97
+ });
98
+ }
99
+ }
100
+ else {
101
+ // Package-level events (no Test field) (#52)
102
+ if (event.Action === "output" && event.Output) {
103
+ if (!pkgOutputMap.has(event.Package)) {
104
+ pkgOutputMap.set(event.Package, []);
105
+ }
106
+ pkgOutputMap.get(event.Package).push(event.Output);
107
+ }
108
+ if (event.Action === "fail") {
109
+ pkgFailures.set(event.Package, {
110
+ package: event.Package,
111
+ });
112
+ }
50
113
  }
51
114
  }
115
+ // Attach output to failed tests (#51)
116
+ for (const [key, entry] of testMap) {
117
+ if (entry.status === "fail") {
118
+ const outputLines = testOutputMap.get(key);
119
+ if (outputLines && outputLines.length > 0) {
120
+ entry.output = outputLines.join("").trimEnd();
121
+ }
122
+ }
123
+ }
124
+ // Attach package-level output to package failures (#52)
125
+ for (const [pkg, failure] of pkgFailures) {
126
+ const outputLines = pkgOutputMap.get(pkg);
127
+ if (outputLines && outputLines.length > 0) {
128
+ failure.output = outputLines.join("").trimEnd();
129
+ }
130
+ }
131
+ // Filter out package failures that have corresponding test-level results
132
+ // (a package "fail" event with tests means the package summary, not a build failure)
133
+ const pkgWithTests = new Set();
134
+ for (const entry of testMap.values()) {
135
+ pkgWithTests.add(entry.package);
136
+ }
137
+ const packageFailures = Array.from(pkgFailures.values()).filter((f) => !pkgWithTests.has(f.package));
52
138
  const tests = Array.from(testMap.values());
53
139
  const passed = tests.filter((t) => t.status === "pass").length;
54
140
  const failed = tests.filter((t) => t.status === "fail").length;
@@ -56,14 +142,119 @@ export function parseGoTestJson(stdout, exitCode) {
56
142
  return {
57
143
  success: exitCode === 0,
58
144
  tests,
145
+ packageFailures: packageFailures.length > 0 ? packageFailures : undefined,
59
146
  total: tests.length,
60
147
  passed,
61
148
  failed,
62
149
  skipped,
63
150
  };
64
151
  }
65
- /** Parses `go vet` output into structured diagnostics with file locations and messages. */
66
- export function parseGoVetOutput(stdout, stderr) {
152
+ /**
153
+ * Parses `go vet -json` output into structured diagnostics.
154
+ *
155
+ * The `-json` flag produces output grouped by package. Each package entry is a JSON
156
+ * object keyed by package import path, containing analyzer results:
157
+ * ```json
158
+ * {
159
+ * "<package>": {
160
+ * "<analyzer>": {
161
+ * "posn": "file.go:10:5",
162
+ * "message": "..."
163
+ * }
164
+ * // or an array of diagnostics per analyzer
165
+ * }
166
+ * }
167
+ * ```
168
+ *
169
+ * Falls back to text parsing if JSON parsing fails (e.g., older Go versions).
170
+ */
171
+ export function parseGoVetOutput(stdout, stderr, exitCode) {
172
+ // Try JSON parsing first (go vet -json)
173
+ const jsonResult = tryParseGoVetJson(stdout, stderr);
174
+ if (jsonResult) {
175
+ return { ...jsonResult, success: exitCode === 0 };
176
+ }
177
+ // Fallback: text parsing
178
+ return parseGoVetText(stdout, stderr, exitCode);
179
+ }
180
+ /**
181
+ * Attempt to parse `go vet -json` structured output.
182
+ * Returns null if the output doesn't look like JSON.
183
+ *
184
+ * go vet -json emits one JSON object per package on stdout/stderr.
185
+ * The structure is: { "<package-path>": { "<analyzer>": <diagnostic-or-array> } }
186
+ * Each diagnostic has: { posn: "file:line:col", message: "..." }
187
+ */
188
+ function tryParseGoVetJson(stdout, stderr) {
189
+ // go vet -json writes JSON to stdout; stderr may have non-JSON messages
190
+ const combined = (stdout + "\n" + stderr).trim();
191
+ if (!combined || !combined.includes("{"))
192
+ return null;
193
+ const diagnostics = [];
194
+ // go vet -json outputs concatenated JSON objects (one per package)
195
+ const chunks = splitJsonObjects(combined);
196
+ if (chunks.length === 0)
197
+ return null;
198
+ let parsedAny = false;
199
+ for (const chunk of chunks) {
200
+ let pkgObj;
201
+ try {
202
+ pkgObj = JSON.parse(chunk);
203
+ }
204
+ catch {
205
+ continue;
206
+ }
207
+ // Each top-level key is a package path
208
+ for (const [, analyzerMap] of Object.entries(pkgObj)) {
209
+ if (typeof analyzerMap !== "object" || analyzerMap === null)
210
+ continue;
211
+ // Each key in the analyzerMap is an analyzer name
212
+ for (const [analyzerName, diagData] of Object.entries(analyzerMap)) {
213
+ const diagItems = Array.isArray(diagData) ? diagData : [diagData];
214
+ for (const item of diagItems) {
215
+ if (typeof item !== "object" || item === null)
216
+ continue;
217
+ const diag = item;
218
+ if (!diag.posn || !diag.message)
219
+ continue;
220
+ parsedAny = true;
221
+ const parsed = parseVetPosn(diag.posn);
222
+ diagnostics.push({
223
+ file: parsed.file,
224
+ line: parsed.line,
225
+ column: parsed.column,
226
+ message: diag.message,
227
+ analyzer: analyzerName,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+ if (!parsedAny)
234
+ return null;
235
+ return { diagnostics, total: diagnostics.length };
236
+ }
237
+ /** Parse a posn string like "file.go:10:5" or "file.go:10" into components. */
238
+ function parseVetPosn(posn) {
239
+ // posn format: "file.go:line:col" or "file.go:line"
240
+ const parts = posn.split(":");
241
+ if (parts.length >= 3) {
242
+ return {
243
+ file: parts.slice(0, -2).join(":"),
244
+ line: parseInt(parts[parts.length - 2], 10) || 0,
245
+ column: parseInt(parts[parts.length - 1], 10) || undefined,
246
+ };
247
+ }
248
+ if (parts.length === 2) {
249
+ return {
250
+ file: parts[0],
251
+ line: parseInt(parts[1], 10) || 0,
252
+ };
253
+ }
254
+ return { file: posn, line: 0 };
255
+ }
256
+ /** Fallback text parser for go vet output (non-JSON mode). */
257
+ function parseGoVetText(stdout, stderr, exitCode) {
67
258
  const output = stdout + "\n" + stderr;
68
259
  const lines = output.split("\n");
69
260
  const diagnostics = [];
@@ -78,7 +269,7 @@ export function parseGoVetOutput(stdout, stderr) {
78
269
  });
79
270
  }
80
271
  }
81
- return { diagnostics, total: diagnostics.length };
272
+ return { success: exitCode === 0, diagnostics, total: diagnostics.length };
82
273
  }
83
274
  /** Parses `go run` output into structured result with stdout, stderr, and exit code. */
84
275
  export function parseGoRunOutput(stdout, stderr, exitCode) {
@@ -89,13 +280,39 @@ export function parseGoRunOutput(stdout, stderr, exitCode) {
89
280
  success: exitCode === 0,
90
281
  };
91
282
  }
92
- /** Parses `go mod tidy` output into structured result with success status and summary. */
93
- export function parseGoModTidyOutput(stdout, stderr, exitCode) {
283
+ /**
284
+ * Parses `go mod tidy` output into structured result with success status, summary,
285
+ * and whether changes were made.
286
+ *
287
+ * @param goModHashBefore - MD5/hash of go.mod before tidy (optional, for change detection)
288
+ * @param goModHashAfter - MD5/hash of go.mod after tidy (optional, for change detection)
289
+ * @param goSumHashBefore - MD5/hash of go.sum before tidy (optional, for change detection)
290
+ * @param goSumHashAfter - MD5/hash of go.sum after tidy (optional, for change detection)
291
+ */
292
+ export function parseGoModTidyOutput(stdout, stderr, exitCode, goModHashBefore, goModHashAfter, goSumHashBefore, goSumHashAfter) {
94
293
  if (exitCode === 0) {
95
294
  const combined = (stdout + "\n" + stderr).trim();
295
+ // Determine whether changes were made
296
+ let madeChanges;
297
+ if (goModHashBefore !== undefined && goModHashAfter !== undefined) {
298
+ const modChanged = goModHashBefore !== goModHashAfter;
299
+ const sumChanged = goSumHashBefore !== undefined &&
300
+ goSumHashAfter !== undefined &&
301
+ goSumHashBefore !== goSumHashAfter;
302
+ madeChanges = modChanged || sumChanged;
303
+ }
304
+ else if (combined) {
305
+ // If we have output (e.g., "go: downloading..."), changes were likely made
306
+ madeChanges = true;
307
+ }
308
+ else {
309
+ // No output and no hashes: assume no changes
310
+ madeChanges = false;
311
+ }
96
312
  return {
97
313
  success: true,
98
314
  summary: combined || "go.mod and go.sum are already tidy.",
315
+ madeChanges,
99
316
  };
100
317
  }
101
318
  const combined = (stderr + "\n" + stdout).trim();
@@ -104,51 +321,147 @@ export function parseGoModTidyOutput(stdout, stderr, exitCode) {
104
321
  summary: combined || "go mod tidy failed.",
105
322
  };
106
323
  }
107
- /** Parses `gofmt -l` or `gofmt -w` output into structured result with file list. */
324
+ /**
325
+ * Regex for gofmt stderr parse errors.
326
+ * Matches patterns like: "file.go:10:5: expected ..." or "file.go:10: expected ..."
327
+ */
328
+ const GOFMT_ERROR_RE = /^(.+?):(\d+)(?::(\d+))?: (.+)$/;
329
+ /** Parses `gofmt -l` or `gofmt -l -w` output into structured result with file list. */
108
330
  export function parseGoFmtOutput(stdout, stderr, exitCode, checkMode) {
109
- // In check mode (-l), stdout lists files that need formatting (one per line).
110
- // In fix mode (-w), stdout is typically empty (files are rewritten in place).
331
+ // Both check mode (-l) and fix mode (-l -w) list changed files on stdout.
332
+ // The -l flag causes gofmt to print filenames, -w causes it to also rewrite them.
111
333
  // stderr may contain error messages.
112
- const output = checkMode ? stdout : stdout;
113
- const files = output
334
+ const files = stdout
114
335
  .split("\n")
115
336
  .map((f) => f.trim())
116
337
  .filter(Boolean);
117
338
  const hasErrors = exitCode !== 0 || (checkMode && files.length > 0);
339
+ // Parse stderr for parse errors (Gap #151)
340
+ const parseErrors = [];
341
+ if (stderr) {
342
+ const stderrLines = stderr.split("\n");
343
+ for (const line of stderrLines) {
344
+ const trimmed = line.trim();
345
+ if (!trimmed)
346
+ continue;
347
+ const match = trimmed.match(GOFMT_ERROR_RE);
348
+ if (match) {
349
+ parseErrors.push({
350
+ file: match[1],
351
+ line: parseInt(match[2], 10),
352
+ column: match[3] ? parseInt(match[3], 10) : undefined,
353
+ message: match[4],
354
+ });
355
+ }
356
+ }
357
+ }
118
358
  return {
119
359
  success: !hasErrors,
120
360
  filesChanged: files.length,
121
361
  files,
362
+ parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
122
363
  };
123
364
  }
365
+ /**
366
+ * Regex for go generate -v output: prints package name as it's processed.
367
+ * Format: "mypackage" (just package name on stderr when -v is used)
368
+ *
369
+ * Regex for go generate -x output: prints the command being executed.
370
+ * Format on stderr: "cd /path/to/pkg; <command>" or just the command
371
+ *
372
+ * Combined regex for go generate verbose output lines indicating per-directive status.
373
+ * Patterns:
374
+ * - `<file>:<line>: running "<command>"...` (from -v, not always present)
375
+ * - `<file>.go:<line>: <command>` (from stderr with -x)
376
+ * - `cd /path; <command>` (from -x)
377
+ */
378
+ const GO_GENERATE_DIRECTIVE_RE = /^(.+\.go):(\d+): running "(.+?)"$/;
379
+ const GO_GENERATE_X_FILE_RE = /^(.+\.go):(\d+): (.+)$/;
124
380
  /** Parses `go generate` output into structured result with success status and output text. */
125
381
  export function parseGoGenerateOutput(stdout, stderr, exitCode) {
126
382
  const combined = (stdout + "\n" + stderr).trim();
383
+ // Parse per-directive output from -v/-x flags (Gap #152)
384
+ const directives = [];
385
+ const allLines = combined.split("\n");
386
+ for (const line of allLines) {
387
+ const trimmed = line.trim();
388
+ if (!trimmed)
389
+ continue;
390
+ // Match "file.go:10: running "command""
391
+ const verboseMatch = trimmed.match(GO_GENERATE_DIRECTIVE_RE);
392
+ if (verboseMatch) {
393
+ directives.push({
394
+ file: verboseMatch[1],
395
+ line: parseInt(verboseMatch[2], 10),
396
+ command: verboseMatch[3],
397
+ status: exitCode === 0 ? "completed" : "running",
398
+ });
399
+ continue;
400
+ }
401
+ // Match "file.go:10: <command>" from -x output
402
+ const xMatch = trimmed.match(GO_GENERATE_X_FILE_RE);
403
+ if (xMatch) {
404
+ // Avoid matching error messages that look like file:line: patterns
405
+ const command = xMatch[3];
406
+ // Skip lines that look like error messages (contain "exec:", "not found", etc.)
407
+ if (!command.startsWith("running ") &&
408
+ !command.includes("executable file not found") &&
409
+ !command.startsWith("bad flag syntax")) {
410
+ directives.push({
411
+ file: xMatch[1],
412
+ line: parseInt(xMatch[2], 10),
413
+ command: command,
414
+ status: exitCode === 0 ? "completed" : undefined,
415
+ });
416
+ }
417
+ }
418
+ }
127
419
  return {
128
420
  success: exitCode === 0,
129
421
  output: combined,
422
+ directives: directives.length > 0 ? directives : undefined,
130
423
  };
131
424
  }
132
- /** Parses `go env -json` output into structured result with environment variables and key fields. */
133
- export function parseGoEnvOutput(stdout) {
134
- const vars = JSON.parse(stdout || "{}");
425
+ /**
426
+ * Parses `go env -json` output into structured result with environment variables and key fields.
427
+ * When specific vars are queried, the result only contains those vars from the JSON output.
428
+ * The queriedVars parameter is used to ensure compact mode includes the queried variables.
429
+ */
430
+ export function parseGoEnvOutput(stdout, _queriedVars) {
431
+ let vars;
432
+ try {
433
+ vars = JSON.parse(stdout || "{}");
434
+ }
435
+ catch {
436
+ return {
437
+ success: false,
438
+ vars: undefined,
439
+ goroot: "",
440
+ gopath: "",
441
+ goversion: "",
442
+ goos: "",
443
+ goarch: "",
444
+ };
445
+ }
135
446
  return {
447
+ success: true,
136
448
  vars,
137
449
  goroot: vars.GOROOT ?? "",
138
450
  gopath: vars.GOPATH ?? "",
139
451
  goversion: vars.GOVERSION ?? "",
140
452
  goos: vars.GOOS ?? "",
141
453
  goarch: vars.GOARCH ?? "",
454
+ // Store queriedVars for compact mode usage (handled in formatter)
142
455
  };
143
456
  }
144
457
  /**
145
458
  * Parses `go list -json` output into structured result with package list and total count.
146
459
  * go list -json emits concatenated JSON objects (JSONL), one per package.
147
460
  */
148
- export function parseGoListOutput(stdout) {
461
+ export function parseGoListOutput(stdout, exitCode) {
149
462
  const packages = [];
150
463
  if (!stdout.trim()) {
151
- return { packages, total: 0 };
464
+ return { success: exitCode === 0, packages, total: 0 };
152
465
  }
153
466
  // go list -json outputs concatenated JSON objects separated by newlines.
154
467
  // Split by detecting `}\n{` boundary and parse each object individually.
@@ -156,18 +469,54 @@ export function parseGoListOutput(stdout) {
156
469
  for (const chunk of chunks) {
157
470
  try {
158
471
  const pkg = JSON.parse(chunk);
159
- packages.push({
472
+ const entry = {
160
473
  dir: pkg.Dir ?? "",
161
474
  importPath: pkg.ImportPath ?? "",
162
475
  name: pkg.Name ?? "",
163
476
  goFiles: pkg.GoFiles,
477
+ testGoFiles: pkg.TestGoFiles,
478
+ imports: pkg.Imports,
479
+ };
480
+ // Capture Error field (Gap #155)
481
+ if (pkg.Error && typeof pkg.Error === "object" && pkg.Error.Err) {
482
+ entry.error = { err: String(pkg.Error.Err) };
483
+ }
484
+ packages.push(entry);
485
+ }
486
+ catch {
487
+ // skip malformed JSON chunks
488
+ }
489
+ }
490
+ return { success: exitCode === 0, packages, total: packages.length };
491
+ }
492
+ /**
493
+ * Parses `go list -json -m` output into structured result with module list and total count.
494
+ * go list -json -m emits concatenated JSON objects, one per module.
495
+ */
496
+ export function parseGoListModulesOutput(stdout, exitCode) {
497
+ const modules = [];
498
+ if (!stdout.trim()) {
499
+ return { success: exitCode === 0, modules, total: 0 };
500
+ }
501
+ const chunks = splitJsonObjects(stdout);
502
+ for (const chunk of chunks) {
503
+ try {
504
+ const mod = JSON.parse(chunk);
505
+ modules.push({
506
+ path: mod.Path ?? "",
507
+ version: mod.Version || undefined,
508
+ dir: mod.Dir || undefined,
509
+ goMod: mod.GoMod || undefined,
510
+ goVersion: mod.GoVersion || undefined,
511
+ main: mod.Main || undefined,
512
+ indirect: mod.Indirect || undefined,
164
513
  });
165
514
  }
166
515
  catch {
167
516
  // skip malformed JSON chunks
168
517
  }
169
518
  }
170
- return { packages, total: packages.length };
519
+ return { success: exitCode === 0, modules, total: modules.length };
171
520
  }
172
521
  /** Splits concatenated JSON objects from go list -json output. */
173
522
  function splitJsonObjects(text) {
@@ -211,7 +560,7 @@ export function parseGolangciLintJson(stdout, _exitCode) {
211
560
  const issues = parsed.Issues ?? [];
212
561
  for (const issue of issues) {
213
562
  const severity = mapSeverity(issue.Severity);
214
- diagnostics.push({
563
+ const diag = {
215
564
  file: issue.Pos?.Filename ?? "",
216
565
  line: issue.Pos?.Line ?? 0,
217
566
  column: issue.Pos?.Column || undefined,
@@ -219,7 +568,35 @@ export function parseGolangciLintJson(stdout, _exitCode) {
219
568
  severity,
220
569
  message: issue.Text ?? "",
221
570
  sourceLine: issue.SourceLines?.[0] || undefined,
222
- });
571
+ };
572
+ // Capture Replacement/fix data (Gap #154)
573
+ if (issue.Replacement) {
574
+ const replacement = issue.Replacement;
575
+ if (replacement.NeedOnlyDelete || replacement.NewLines !== undefined) {
576
+ const fix = {
577
+ text: replacement.NeedOnlyDelete ? "" : (replacement.NewLines ?? []).join("\n"),
578
+ };
579
+ // Include range if inline fix info is present
580
+ if (replacement.Inline) {
581
+ fix.range = {
582
+ start: {
583
+ line: replacement.Inline.StartLine ?? issue.Pos?.Line ?? 0,
584
+ column: replacement.Inline.StartCol,
585
+ },
586
+ end: {
587
+ line: replacement.Inline.EndLine ?? issue.Pos?.Line ?? 0,
588
+ column: replacement.Inline.EndCol,
589
+ },
590
+ };
591
+ }
592
+ diag.fix = fix;
593
+ }
594
+ else if (typeof replacement.NewLines === "undefined" && !replacement.NeedOnlyDelete) {
595
+ // Some replacements only have NewLines without inline info
596
+ // If Replacement exists but has no expected structure, still try to capture it
597
+ }
598
+ }
599
+ diagnostics.push(diag);
223
600
  }
224
601
  const errors = diagnostics.filter((d) => d.severity === "error").length;
225
602
  const warnings = diagnostics.filter((d) => d.severity === "warning").length;
@@ -249,12 +626,92 @@ function mapSeverity(severity) {
249
626
  return "info";
250
627
  return "warning";
251
628
  }
252
- /** Parses `go get` output into structured result with success status and output text. */
253
- export function parseGoGetOutput(stdout, stderr, exitCode) {
629
+ /**
630
+ * Regex patterns for go get output lines that indicate version resolution.
631
+ * Matches lines like:
632
+ * go: upgraded golang.org/x/text v0.3.7 => v0.14.0
633
+ * go: added github.com/pkg/errors v0.9.1
634
+ * go: downgraded golang.org/x/net v0.10.0 => v0.8.0
635
+ */
636
+ const GO_GET_UPGRADED_RE = /^go: (?:upgraded|downgraded)\s+(\S+)\s+(\S+)\s+=>\s+(\S+)$/;
637
+ const GO_GET_ADDED_RE = /^go: added\s+(\S+)\s+(\S+)$/;
638
+ /**
639
+ * Regex for go get error lines per package.
640
+ * Examples:
641
+ * go: module github.com/nonexistent/pkg: no matching versions for query "latest"
642
+ * go: github.com/some/pkg@v1.0.0: verifying module: ...
643
+ */
644
+ const GO_GET_ERROR_RE = /^go: (?:module\s+)?(\S+?)(?:@(\S+))?:\s+(.+)$/;
645
+ /** Parses `go get` output into structured result with success status, output text, resolved packages, and per-package status. */
646
+ export function parseGoGetOutput(stdout, stderr, exitCode, requestedPackages) {
254
647
  const combined = (stdout + "\n" + stderr).trim();
648
+ const resolvedPackages = [];
649
+ // Per-package status tracking (Gap #153)
650
+ const packageStatuses = new Map();
651
+ const lines = combined.split("\n");
652
+ for (const line of lines) {
653
+ const trimmed = line.trim();
654
+ // Match "go: upgraded <pkg> <old> => <new>" or "go: downgraded <pkg> <old> => <new>"
655
+ const upgradeMatch = trimmed.match(GO_GET_UPGRADED_RE);
656
+ if (upgradeMatch) {
657
+ resolvedPackages.push({
658
+ package: upgradeMatch[1],
659
+ previousVersion: upgradeMatch[2],
660
+ newVersion: upgradeMatch[3],
661
+ });
662
+ packageStatuses.set(upgradeMatch[1], {
663
+ path: upgradeMatch[1],
664
+ version: upgradeMatch[3],
665
+ });
666
+ continue;
667
+ }
668
+ // Match "go: added <pkg> <version>"
669
+ const addedMatch = trimmed.match(GO_GET_ADDED_RE);
670
+ if (addedMatch) {
671
+ resolvedPackages.push({
672
+ package: addedMatch[1],
673
+ newVersion: addedMatch[2],
674
+ });
675
+ packageStatuses.set(addedMatch[1], {
676
+ path: addedMatch[1],
677
+ version: addedMatch[2],
678
+ });
679
+ continue;
680
+ }
681
+ // Match per-package error lines (Gap #153)
682
+ const errorMatch = trimmed.match(GO_GET_ERROR_RE);
683
+ if (errorMatch) {
684
+ const pkgPath = errorMatch[1];
685
+ const errMsg = errorMatch[3];
686
+ // Skip "downloading" lines which are progress, not errors
687
+ if (!errMsg.startsWith("downloading ")) {
688
+ packageStatuses.set(pkgPath, {
689
+ path: pkgPath,
690
+ version: errorMatch[2] || undefined,
691
+ error: errMsg,
692
+ });
693
+ }
694
+ }
695
+ }
696
+ // If we have requestedPackages but some aren't in statuses, add them as successful (no error, no explicit status)
697
+ if (requestedPackages) {
698
+ for (const reqPkg of requestedPackages) {
699
+ // Strip version suffix from request (e.g., "github.com/pkg/errors@latest" -> "github.com/pkg/errors")
700
+ const pkgPath = reqPkg.replace(/@.*$/, "");
701
+ if (!packageStatuses.has(pkgPath)) {
702
+ // If the overall command succeeded, mark as successful
703
+ if (exitCode === 0) {
704
+ packageStatuses.set(pkgPath, { path: pkgPath });
705
+ }
706
+ }
707
+ }
708
+ }
709
+ const packages = packageStatuses.size > 0 ? Array.from(packageStatuses.values()) : undefined;
255
710
  return {
256
711
  success: exitCode === 0,
257
712
  output: combined || undefined,
713
+ resolvedPackages: resolvedPackages.length > 0 ? resolvedPackages : undefined,
714
+ packages,
258
715
  };
259
716
  }
260
717
  //# sourceMappingURL=parsers.js.map