@paretools/go 0.8.5 → 0.10.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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/formatters.d.ts +33 -9
  5. package/dist/lib/formatters.d.ts.map +1 -1
  6. package/dist/lib/formatters.js +291 -34
  7. package/dist/lib/formatters.js.map +1 -1
  8. package/dist/lib/go-runner.d.ts +1 -1
  9. package/dist/lib/go-runner.d.ts.map +1 -1
  10. package/dist/lib/go-runner.js +3 -3
  11. package/dist/lib/go-runner.js.map +1 -1
  12. package/dist/lib/parsers.d.ts +47 -12
  13. package/dist/lib/parsers.d.ts.map +1 -1
  14. package/dist/lib/parsers.js +653 -45
  15. package/dist/lib/parsers.js.map +1 -1
  16. package/dist/schemas/index.d.ts +218 -4
  17. package/dist/schemas/index.d.ts.map +1 -1
  18. package/dist/schemas/index.js +142 -5
  19. package/dist/schemas/index.js.map +1 -1
  20. package/dist/tools/build.d.ts.map +1 -1
  21. package/dist/tools/build.js +97 -12
  22. package/dist/tools/build.js.map +1 -1
  23. package/dist/tools/env.d.ts.map +1 -1
  24. package/dist/tools/env.js +15 -13
  25. package/dist/tools/env.js.map +1 -1
  26. package/dist/tools/fmt.d.ts.map +1 -1
  27. package/dist/tools/fmt.js +28 -12
  28. package/dist/tools/fmt.js.map +1 -1
  29. package/dist/tools/generate.d.ts.map +1 -1
  30. package/dist/tools/generate.js +77 -12
  31. package/dist/tools/generate.js.map +1 -1
  32. package/dist/tools/get.d.ts.map +1 -1
  33. package/dist/tools/get.js +91 -11
  34. package/dist/tools/get.js.map +1 -1
  35. package/dist/tools/golangci-lint.d.ts.map +1 -1
  36. package/dist/tools/golangci-lint.js +145 -13
  37. package/dist/tools/golangci-lint.js.map +1 -1
  38. package/dist/tools/list.d.ts.map +1 -1
  39. package/dist/tools/list.js +77 -13
  40. package/dist/tools/list.js.map +1 -1
  41. package/dist/tools/mod-tidy.d.ts.map +1 -1
  42. package/dist/tools/mod-tidy.js +119 -12
  43. package/dist/tools/mod-tidy.js.map +1 -1
  44. package/dist/tools/run.d.ts.map +1 -1
  45. package/dist/tools/run.js +108 -16
  46. package/dist/tools/run.js.map +1 -1
  47. package/dist/tools/test.d.ts.map +1 -1
  48. package/dist/tools/test.js +112 -11
  49. package/dist/tools/test.js.map +1 -1
  50. package/dist/tools/vet.d.ts.map +1 -1
  51. package/dist/tools/vet.js +55 -13
  52. package/dist/tools/vet.js.map +1 -1
  53. 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,64 @@ 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
+ const parent = deriveParentTestName(event.Test);
93
+ testMap.set(key, {
94
+ package: event.Package,
95
+ name: event.Test,
96
+ ...(parent ? { parent } : {}),
97
+ status: event.Action,
98
+ elapsed: event.Elapsed,
99
+ });
100
+ }
50
101
  }
102
+ else {
103
+ // Package-level events (no Test field) (#52)
104
+ if (event.Action === "output" && event.Output) {
105
+ if (!pkgOutputMap.has(event.Package)) {
106
+ pkgOutputMap.set(event.Package, []);
107
+ }
108
+ pkgOutputMap.get(event.Package).push(event.Output);
109
+ }
110
+ if (event.Action === "fail") {
111
+ pkgFailures.set(event.Package, {
112
+ package: event.Package,
113
+ });
114
+ }
115
+ }
116
+ }
117
+ // Attach output to failed tests (#51)
118
+ for (const [key, entry] of testMap) {
119
+ if (entry.status === "fail") {
120
+ const outputLines = testOutputMap.get(key);
121
+ if (outputLines && outputLines.length > 0) {
122
+ entry.output = outputLines.join("").trimEnd();
123
+ }
124
+ }
125
+ }
126
+ // Attach package-level output to package failures (#52)
127
+ for (const [pkg, failure] of pkgFailures) {
128
+ const outputLines = pkgOutputMap.get(pkg);
129
+ if (outputLines && outputLines.length > 0) {
130
+ failure.output = outputLines.join("").trimEnd();
131
+ }
132
+ }
133
+ // Filter out package failures that have corresponding test-level results
134
+ // (a package "fail" event with tests means the package summary, not a build failure)
135
+ const pkgWithTests = new Set();
136
+ for (const entry of testMap.values()) {
137
+ pkgWithTests.add(entry.package);
51
138
  }
139
+ const packageFailures = Array.from(pkgFailures.values()).filter((f) => !pkgWithTests.has(f.package));
52
140
  const tests = Array.from(testMap.values());
53
141
  const passed = tests.filter((t) => t.status === "pass").length;
54
142
  const failed = tests.filter((t) => t.status === "fail").length;
@@ -56,19 +144,146 @@ export function parseGoTestJson(stdout, exitCode) {
56
144
  return {
57
145
  success: exitCode === 0,
58
146
  tests,
147
+ packageFailures: packageFailures.length > 0 ? packageFailures : undefined,
59
148
  total: tests.length,
60
149
  passed,
61
150
  failed,
62
151
  skipped,
63
152
  };
64
153
  }
65
- /** Parses `go vet` output into structured diagnostics with file locations and messages. */
66
- export function parseGoVetOutput(stdout, stderr) {
154
+ function deriveParentTestName(testName) {
155
+ const idx = testName.lastIndexOf("/");
156
+ if (idx <= 0)
157
+ return undefined;
158
+ return testName.slice(0, idx);
159
+ }
160
+ /**
161
+ * Parses `go vet -json` output into structured diagnostics.
162
+ *
163
+ * The `-json` flag produces output grouped by package. Each package entry is a JSON
164
+ * object keyed by package import path, containing analyzer results:
165
+ * ```json
166
+ * {
167
+ * "<package>": {
168
+ * "<analyzer>": {
169
+ * "posn": "file.go:10:5",
170
+ * "message": "..."
171
+ * }
172
+ * // or an array of diagnostics per analyzer
173
+ * }
174
+ * }
175
+ * ```
176
+ *
177
+ * Falls back to text parsing if JSON parsing fails (e.g., older Go versions).
178
+ */
179
+ export function parseGoVetOutput(stdout, stderr, exitCode) {
180
+ const compilationErrors = extractVetCompilationErrors(stdout, stderr);
181
+ // Try JSON parsing first (go vet -json)
182
+ const jsonResult = tryParseGoVetJson(stdout, stderr);
183
+ if (jsonResult) {
184
+ return {
185
+ ...jsonResult,
186
+ success: exitCode === 0,
187
+ compilationErrors: compilationErrors.length > 0 ? compilationErrors : undefined,
188
+ total: jsonResult.total,
189
+ };
190
+ }
191
+ // Fallback: text parsing
192
+ return parseGoVetText(stdout, stderr, exitCode, compilationErrors);
193
+ }
194
+ /**
195
+ * Attempt to parse `go vet -json` structured output.
196
+ * Returns null if the output doesn't look like JSON.
197
+ *
198
+ * go vet -json emits one JSON object per package on stdout/stderr.
199
+ * The structure is: { "<package-path>": { "<analyzer>": <diagnostic-or-array> } }
200
+ * Each diagnostic has: { posn: "file:line:col", message: "..." }
201
+ */
202
+ function tryParseGoVetJson(stdout, stderr) {
203
+ // go vet -json writes JSON to stdout; stderr may have non-JSON messages
204
+ const combined = (stdout + "\n" + stderr).trim();
205
+ if (!combined || !combined.includes("{"))
206
+ return null;
207
+ const diagnostics = [];
208
+ // go vet -json outputs concatenated JSON objects (one per package)
209
+ const chunks = splitJsonObjects(combined);
210
+ if (chunks.length === 0)
211
+ return null;
212
+ let parsedAny = false;
213
+ for (const chunk of chunks) {
214
+ let pkgObj;
215
+ try {
216
+ pkgObj = JSON.parse(chunk);
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ // Each top-level key is a package path
222
+ for (const [, analyzerMap] of Object.entries(pkgObj)) {
223
+ if (typeof analyzerMap !== "object" || analyzerMap === null)
224
+ continue;
225
+ // Each key in the analyzerMap is an analyzer name
226
+ for (const [analyzerName, diagData] of Object.entries(analyzerMap)) {
227
+ const diagItems = Array.isArray(diagData) ? diagData : [diagData];
228
+ for (const item of diagItems) {
229
+ if (typeof item !== "object" || item === null)
230
+ continue;
231
+ const diag = item;
232
+ if (!diag.posn || !diag.message)
233
+ continue;
234
+ parsedAny = true;
235
+ const parsed = parseVetPosn(diag.posn);
236
+ diagnostics.push({
237
+ file: parsed.file,
238
+ line: parsed.line,
239
+ column: parsed.column,
240
+ message: diag.message,
241
+ analyzer: analyzerName,
242
+ });
243
+ }
244
+ }
245
+ }
246
+ }
247
+ if (!parsedAny)
248
+ return null;
249
+ return { diagnostics, total: diagnostics.length };
250
+ }
251
+ /** Parse a posn string like "file.go:10:5" or "file.go:10" into components. */
252
+ function parseVetPosn(posn) {
253
+ // posn format: "file.go:line:col" or "file.go:line"
254
+ const parts = posn.split(":");
255
+ if (parts.length >= 3) {
256
+ return {
257
+ file: parts.slice(0, -2).join(":"),
258
+ line: parseInt(parts[parts.length - 2], 10) || 0,
259
+ column: parseInt(parts[parts.length - 1], 10) || undefined,
260
+ };
261
+ }
262
+ if (parts.length === 2) {
263
+ return {
264
+ file: parts[0],
265
+ line: parseInt(parts[1], 10) || 0,
266
+ };
267
+ }
268
+ return { file: posn, line: 0 };
269
+ }
270
+ /** Fallback text parser for go vet output (non-JSON mode). */
271
+ function parseGoVetText(stdout, stderr, exitCode, knownCompilationErrors = []) {
67
272
  const output = stdout + "\n" + stderr;
68
273
  const lines = output.split("\n");
69
274
  const diagnostics = [];
275
+ const compilationErrors = [...knownCompilationErrors];
70
276
  for (const line of lines) {
71
- const match = line.match(GO_ERROR_RE);
277
+ const trimmed = line.trim();
278
+ if (!trimmed)
279
+ continue;
280
+ if (isCompilationErrorLine(trimmed)) {
281
+ if (!compilationErrors.includes(trimmed)) {
282
+ compilationErrors.push(trimmed);
283
+ }
284
+ continue;
285
+ }
286
+ const match = trimmed.match(GO_ERROR_RE);
72
287
  if (match) {
73
288
  diagnostics.push({
74
289
  file: match[1],
@@ -78,77 +293,279 @@ export function parseGoVetOutput(stdout, stderr) {
78
293
  });
79
294
  }
80
295
  }
81
- return { diagnostics, total: diagnostics.length };
296
+ return {
297
+ success: exitCode === 0,
298
+ diagnostics,
299
+ compilationErrors: compilationErrors.length > 0 ? compilationErrors : undefined,
300
+ total: diagnostics.length,
301
+ };
302
+ }
303
+ function extractVetCompilationErrors(stdout, stderr) {
304
+ const lines = (stdout + "\n" + stderr)
305
+ .split("\n")
306
+ .map((l) => l.trim())
307
+ .filter(Boolean);
308
+ const seen = new Set();
309
+ const errors = [];
310
+ for (const line of lines) {
311
+ if (isCompilationErrorLine(line) && !seen.has(line)) {
312
+ seen.add(line);
313
+ errors.push(line);
314
+ }
315
+ }
316
+ return errors;
317
+ }
318
+ function isCompilationErrorLine(line) {
319
+ return (line.startsWith("# ") ||
320
+ line.includes("undefined:") ||
321
+ line.includes("could not import") ||
322
+ line.includes("build constraints exclude") ||
323
+ (line.includes("package ") && line.includes(" is not in ")) ||
324
+ line.includes("cannot find package"));
82
325
  }
83
326
  /** Parses `go run` output into structured result with stdout, stderr, and exit code. */
84
- export function parseGoRunOutput(stdout, stderr, exitCode) {
327
+ export function parseGoRunOutput(stdout, stderr, exitCode, timedOut = false, signal) {
85
328
  return {
86
329
  exitCode,
87
330
  stdout: stdout.trimEnd(),
88
331
  stderr: stderr.trimEnd(),
89
- success: exitCode === 0,
332
+ ...(timedOut ? { timedOut: true } : {}),
333
+ ...(signal ? { signal } : {}),
334
+ success: exitCode === 0 && !timedOut,
90
335
  };
91
336
  }
92
- /** Parses `go mod tidy` output into structured result with success status and summary. */
93
- export function parseGoModTidyOutput(stdout, stderr, exitCode) {
337
+ /**
338
+ * Parses `go mod tidy` output into structured result with success status, summary,
339
+ * and whether changes were made.
340
+ *
341
+ * @param goModHashBefore - MD5/hash of go.mod before tidy (optional, for change detection)
342
+ * @param goModHashAfter - MD5/hash of go.mod after tidy (optional, for change detection)
343
+ * @param goSumHashBefore - MD5/hash of go.sum before tidy (optional, for change detection)
344
+ * @param goSumHashAfter - MD5/hash of go.sum after tidy (optional, for change detection)
345
+ */
346
+ export function parseGoModTidyOutput(stdout, stderr, exitCode, goModHashBefore, goModHashAfter, goSumHashBefore, goSumHashAfter, addedModules = [], removedModules = []) {
94
347
  if (exitCode === 0) {
95
348
  const combined = (stdout + "\n" + stderr).trim();
349
+ // Determine whether changes were made
350
+ let madeChanges;
351
+ if (goModHashBefore !== undefined && goModHashAfter !== undefined) {
352
+ const modChanged = goModHashBefore !== goModHashAfter;
353
+ const sumChanged = goSumHashBefore !== undefined &&
354
+ goSumHashAfter !== undefined &&
355
+ goSumHashBefore !== goSumHashAfter;
356
+ madeChanges = modChanged || sumChanged;
357
+ }
358
+ else if (combined) {
359
+ // If we have output (e.g., "go: downloading..."), changes were likely made
360
+ madeChanges = true;
361
+ }
362
+ else {
363
+ // No output and no hashes: assume no changes
364
+ madeChanges = false;
365
+ }
96
366
  return {
97
367
  success: true,
98
368
  summary: combined || "go.mod and go.sum are already tidy.",
369
+ ...(addedModules.length > 0 ? { addedModules } : {}),
370
+ ...(removedModules.length > 0 ? { removedModules } : {}),
371
+ madeChanges,
99
372
  };
100
373
  }
101
374
  const combined = (stderr + "\n" + stdout).trim();
375
+ const errorType = categorizeGoModTidyError(combined);
102
376
  return {
103
377
  success: false,
104
378
  summary: combined || "go mod tidy failed.",
379
+ errorType,
105
380
  };
106
381
  }
107
- /** Parses `gofmt -l` or `gofmt -w` output into structured result with file list. */
382
+ function categorizeGoModTidyError(message) {
383
+ const lower = message.toLowerCase();
384
+ if (lower.includes("dial tcp") ||
385
+ lower.includes("i/o timeout") ||
386
+ lower.includes("connection refused") ||
387
+ lower.includes("no such host")) {
388
+ return "network";
389
+ }
390
+ if (lower.includes("checksum mismatch") || lower.includes("sumdb")) {
391
+ return "checksum";
392
+ }
393
+ if (lower.includes("syntax error") || lower.includes("invalid go version")) {
394
+ return "syntax";
395
+ }
396
+ return "unknown";
397
+ }
398
+ /**
399
+ * Regex for gofmt stderr parse errors.
400
+ * Matches patterns like: "file.go:10:5: expected ..." or "file.go:10: expected ..."
401
+ */
402
+ const GOFMT_ERROR_RE = /^(.+?):(\d+)(?::(\d+))?: (.+)$/;
403
+ /** Parses `gofmt -l` or `gofmt -l -w` output into structured result with file list. */
108
404
  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).
111
- // stderr may contain error messages.
112
- const output = checkMode ? stdout : stdout;
113
- const files = output
405
+ // Both check mode (-l) and fix mode (-l -w) list changed files on stdout.
406
+ // With -d enabled, stdout may contain unified diffs.
407
+ const changes = parseGofmtChanges(stdout);
408
+ const filesFromDiff = changes.map((c) => c.file);
409
+ const filesFromList = stdout
114
410
  .split("\n")
115
411
  .map((f) => f.trim())
116
- .filter(Boolean);
412
+ .filter((f) => !!f && !f.startsWith("diff ") && !f.startsWith("--- ") && !f.startsWith("+++ "));
413
+ const files = filesFromDiff.length > 0 ? Array.from(new Set(filesFromDiff)) : filesFromList;
117
414
  const hasErrors = exitCode !== 0 || (checkMode && files.length > 0);
415
+ // Parse stderr for parse errors (Gap #151)
416
+ const parseErrors = [];
417
+ if (stderr) {
418
+ const stderrLines = stderr.split("\n");
419
+ for (const line of stderrLines) {
420
+ const trimmed = line.trim();
421
+ if (!trimmed)
422
+ continue;
423
+ const match = trimmed.match(GOFMT_ERROR_RE);
424
+ if (match) {
425
+ parseErrors.push({
426
+ file: match[1],
427
+ line: parseInt(match[2], 10),
428
+ column: match[3] ? parseInt(match[3], 10) : undefined,
429
+ message: match[4],
430
+ });
431
+ }
432
+ }
433
+ }
118
434
  return {
119
435
  success: !hasErrors,
120
436
  filesChanged: files.length,
121
437
  files,
438
+ changes: changes.length > 0 ? changes : undefined,
439
+ parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
122
440
  };
123
441
  }
442
+ function parseGofmtChanges(stdout) {
443
+ if (!stdout.includes("\ndiff ") && !stdout.startsWith("diff ")) {
444
+ return [];
445
+ }
446
+ const blocks = stdout
447
+ .split(/^diff /m)
448
+ .map((b) => b.trim())
449
+ .filter(Boolean);
450
+ const changes = [];
451
+ for (const block of blocks) {
452
+ const diffText = `diff ${block}`;
453
+ const firstLine = diffText.split("\n")[0] ?? "";
454
+ const fileMatch = firstLine.match(/^diff\s+\S+\s+(\S+)/);
455
+ const file = fileMatch?.[1];
456
+ if (!file)
457
+ continue;
458
+ changes.push({ file, diff: diffText });
459
+ }
460
+ return changes;
461
+ }
462
+ /**
463
+ * Regex for go generate -v output: prints package name as it's processed.
464
+ * Format: "mypackage" (just package name on stderr when -v is used)
465
+ *
466
+ * Regex for go generate -x output: prints the command being executed.
467
+ * Format on stderr: "cd /path/to/pkg; <command>" or just the command
468
+ *
469
+ * Combined regex for go generate verbose output lines indicating per-directive status.
470
+ * Patterns:
471
+ * - `<file>:<line>: running "<command>"...` (from -v, not always present)
472
+ * - `<file>.go:<line>: <command>` (from stderr with -x)
473
+ * - `cd /path; <command>` (from -x)
474
+ */
475
+ const GO_GENERATE_DIRECTIVE_RE = /^(.+\.go):(\d+): running "(.+?)"$/;
476
+ const GO_GENERATE_X_FILE_RE = /^(.+\.go):(\d+): (.+)$/;
124
477
  /** Parses `go generate` output into structured result with success status and output text. */
125
- export function parseGoGenerateOutput(stdout, stderr, exitCode) {
478
+ export function parseGoGenerateOutput(stdout, stderr, exitCode, timedOut = false) {
126
479
  const combined = (stdout + "\n" + stderr).trim();
480
+ // Parse per-directive output from -v/-x flags (Gap #152)
481
+ const directives = [];
482
+ const allLines = combined.split("\n");
483
+ for (const line of allLines) {
484
+ const trimmed = line.trim();
485
+ if (!trimmed)
486
+ continue;
487
+ // Match "file.go:10: running "command""
488
+ const verboseMatch = trimmed.match(GO_GENERATE_DIRECTIVE_RE);
489
+ if (verboseMatch) {
490
+ directives.push({
491
+ file: verboseMatch[1],
492
+ line: parseInt(verboseMatch[2], 10),
493
+ command: verboseMatch[3],
494
+ status: exitCode === 0 ? "completed" : "running",
495
+ });
496
+ continue;
497
+ }
498
+ // Match "file.go:10: <command>" from -x output
499
+ const xMatch = trimmed.match(GO_GENERATE_X_FILE_RE);
500
+ if (xMatch) {
501
+ // Avoid matching error messages that look like file:line: patterns
502
+ const command = xMatch[3];
503
+ // Skip lines that look like error messages (contain "exec:", "not found", etc.)
504
+ if (!command.startsWith("running ") &&
505
+ !command.includes("executable file not found") &&
506
+ !command.startsWith("bad flag syntax")) {
507
+ directives.push({
508
+ file: xMatch[1],
509
+ line: parseInt(xMatch[2], 10),
510
+ command: command,
511
+ status: exitCode === 0 ? "completed" : undefined,
512
+ });
513
+ }
514
+ }
515
+ }
127
516
  return {
128
- success: exitCode === 0,
517
+ success: exitCode === 0 && !timedOut,
129
518
  output: combined,
519
+ ...(timedOut ? { timedOut: true } : {}),
520
+ directives: directives.length > 0 ? directives : undefined,
130
521
  };
131
522
  }
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 || "{}");
523
+ /**
524
+ * Parses `go env -json` output into structured result with environment variables and key fields.
525
+ * When specific vars are queried, the result only contains those vars from the JSON output.
526
+ * The queriedVars parameter is used to ensure compact mode includes the queried variables.
527
+ */
528
+ export function parseGoEnvOutput(stdout, _queriedVars) {
529
+ let allVars;
530
+ try {
531
+ allVars = JSON.parse(stdout || "{}");
532
+ }
533
+ catch {
534
+ return {
535
+ success: false,
536
+ vars: undefined,
537
+ goroot: "",
538
+ gopath: "",
539
+ goversion: "",
540
+ goos: "",
541
+ goarch: "",
542
+ };
543
+ }
544
+ const queriedVars = _queriedVars ?? [];
545
+ const includeVars = _queriedVars === undefined ? true : queriedVars.length > 0;
546
+ const vars = includeVars ? allVars : undefined;
547
+ const cgoRaw = allVars.CGO_ENABLED;
548
+ const cgoEnabled = cgoRaw !== undefined ? cgoRaw === "1" : undefined;
135
549
  return {
550
+ success: true,
136
551
  vars,
137
- goroot: vars.GOROOT ?? "",
138
- gopath: vars.GOPATH ?? "",
139
- goversion: vars.GOVERSION ?? "",
140
- goos: vars.GOOS ?? "",
141
- goarch: vars.GOARCH ?? "",
552
+ goroot: allVars.GOROOT ?? "",
553
+ gopath: allVars.GOPATH ?? "",
554
+ goversion: allVars.GOVERSION ?? "",
555
+ goos: allVars.GOOS ?? "",
556
+ goarch: allVars.GOARCH ?? "",
557
+ ...(cgoEnabled !== undefined ? { cgoEnabled } : {}),
558
+ // Store queriedVars for compact mode usage (handled in formatter)
142
559
  };
143
560
  }
144
561
  /**
145
562
  * Parses `go list -json` output into structured result with package list and total count.
146
563
  * go list -json emits concatenated JSON objects (JSONL), one per package.
147
564
  */
148
- export function parseGoListOutput(stdout) {
565
+ export function parseGoListOutput(stdout, exitCode) {
149
566
  const packages = [];
150
567
  if (!stdout.trim()) {
151
- return { packages, total: 0 };
568
+ return { success: exitCode === 0, packages, total: 0 };
152
569
  }
153
570
  // go list -json outputs concatenated JSON objects separated by newlines.
154
571
  // Split by detecting `}\n{` boundary and parse each object individually.
@@ -156,18 +573,62 @@ export function parseGoListOutput(stdout) {
156
573
  for (const chunk of chunks) {
157
574
  try {
158
575
  const pkg = JSON.parse(chunk);
159
- packages.push({
576
+ const entry = {
160
577
  dir: pkg.Dir ?? "",
161
578
  importPath: pkg.ImportPath ?? "",
162
579
  name: pkg.Name ?? "",
163
580
  goFiles: pkg.GoFiles,
581
+ testGoFiles: pkg.TestGoFiles,
582
+ imports: pkg.Imports,
583
+ standard: typeof pkg.Standard === "boolean" ? pkg.Standard : undefined,
584
+ stale: typeof pkg.Stale === "boolean" ? pkg.Stale : undefined,
585
+ module: pkg.Module && typeof pkg.Module === "object"
586
+ ? {
587
+ path: typeof pkg.Module.Path === "string" ? pkg.Module.Path : undefined,
588
+ version: typeof pkg.Module.Version === "string" ? pkg.Module.Version : undefined,
589
+ }
590
+ : undefined,
591
+ };
592
+ // Capture Error field (Gap #155)
593
+ if (pkg.Error && typeof pkg.Error === "object" && pkg.Error.Err) {
594
+ entry.error = { err: String(pkg.Error.Err) };
595
+ }
596
+ packages.push(entry);
597
+ }
598
+ catch {
599
+ // skip malformed JSON chunks
600
+ }
601
+ }
602
+ return { success: exitCode === 0, packages, total: packages.length };
603
+ }
604
+ /**
605
+ * Parses `go list -json -m` output into structured result with module list and total count.
606
+ * go list -json -m emits concatenated JSON objects, one per module.
607
+ */
608
+ export function parseGoListModulesOutput(stdout, exitCode) {
609
+ const modules = [];
610
+ if (!stdout.trim()) {
611
+ return { success: exitCode === 0, modules, total: 0 };
612
+ }
613
+ const chunks = splitJsonObjects(stdout);
614
+ for (const chunk of chunks) {
615
+ try {
616
+ const mod = JSON.parse(chunk);
617
+ modules.push({
618
+ path: mod.Path ?? "",
619
+ version: mod.Version || undefined,
620
+ dir: mod.Dir || undefined,
621
+ goMod: mod.GoMod || undefined,
622
+ goVersion: mod.GoVersion || undefined,
623
+ main: mod.Main || undefined,
624
+ indirect: mod.Indirect || undefined,
164
625
  });
165
626
  }
166
627
  catch {
167
628
  // skip malformed JSON chunks
168
629
  }
169
630
  }
170
- return { packages, total: packages.length };
631
+ return { success: exitCode === 0, modules, total: modules.length };
171
632
  }
172
633
  /** Splits concatenated JSON objects from go list -json output. */
173
634
  function splitJsonObjects(text) {
@@ -211,15 +672,44 @@ export function parseGolangciLintJson(stdout, _exitCode) {
211
672
  const issues = parsed.Issues ?? [];
212
673
  for (const issue of issues) {
213
674
  const severity = mapSeverity(issue.Severity);
214
- diagnostics.push({
675
+ const diag = {
215
676
  file: issue.Pos?.Filename ?? "",
216
677
  line: issue.Pos?.Line ?? 0,
217
678
  column: issue.Pos?.Column || undefined,
218
679
  linter: issue.FromLinter ?? "",
680
+ category: classifyLinter(issue.FromLinter ?? ""),
219
681
  severity,
220
682
  message: issue.Text ?? "",
221
683
  sourceLine: issue.SourceLines?.[0] || undefined,
222
- });
684
+ };
685
+ // Capture Replacement/fix data (Gap #154)
686
+ if (issue.Replacement) {
687
+ const replacement = issue.Replacement;
688
+ if (replacement.NeedOnlyDelete || replacement.NewLines !== undefined) {
689
+ const fix = {
690
+ text: replacement.NeedOnlyDelete ? "" : (replacement.NewLines ?? []).join("\n"),
691
+ };
692
+ // Include range if inline fix info is present
693
+ if (replacement.Inline) {
694
+ fix.range = {
695
+ start: {
696
+ line: replacement.Inline.StartLine ?? issue.Pos?.Line ?? 0,
697
+ column: replacement.Inline.StartCol,
698
+ },
699
+ end: {
700
+ line: replacement.Inline.EndLine ?? issue.Pos?.Line ?? 0,
701
+ column: replacement.Inline.EndCol,
702
+ },
703
+ };
704
+ }
705
+ diag.fix = fix;
706
+ }
707
+ else if (typeof replacement.NewLines === "undefined" && !replacement.NeedOnlyDelete) {
708
+ // Some replacements only have NewLines without inline info
709
+ // If Replacement exists but has no expected structure, still try to capture it
710
+ }
711
+ }
712
+ diagnostics.push(diag);
223
713
  }
224
714
  const errors = diagnostics.filter((d) => d.severity === "error").length;
225
715
  const warnings = diagnostics.filter((d) => d.severity === "warning").length;
@@ -249,12 +739,130 @@ function mapSeverity(severity) {
249
739
  return "info";
250
740
  return "warning";
251
741
  }
252
- /** Parses `go get` output into structured result with success status and output text. */
253
- export function parseGoGetOutput(stdout, stderr, exitCode) {
742
+ function classifyLinter(linter) {
743
+ const name = linter.toLowerCase();
744
+ if (["gosec"].includes(name))
745
+ return "security";
746
+ if (["govet", "staticcheck", "typecheck", "errcheck", "ineffassign", "nilerr"].includes(name)) {
747
+ return "bug-risk";
748
+ }
749
+ if (["gocyclo", "cyclop", "funlen", "nestif", "maintidx"].includes(name))
750
+ return "complexity";
751
+ if (["prealloc", "perfsprint", "makezero", "unconvert"].includes(name))
752
+ return "performance";
753
+ if (["gofmt", "goimports", "revive", "stylecheck", "whitespace", "misspell"].includes(name)) {
754
+ return "style";
755
+ }
756
+ return "other";
757
+ }
758
+ /**
759
+ * Regex patterns for go get output lines that indicate version resolution.
760
+ * Matches lines like:
761
+ * go: upgraded golang.org/x/text v0.3.7 => v0.14.0
762
+ * go: added github.com/pkg/errors v0.9.1
763
+ * go: downgraded golang.org/x/net v0.10.0 => v0.8.0
764
+ */
765
+ const GO_GET_UPGRADED_RE = /^go: (?:upgraded|downgraded)\s+(\S+)\s+(\S+)\s+=>\s+(\S+)$/;
766
+ const GO_GET_ADDED_RE = /^go: added\s+(\S+)\s+(\S+)$/;
767
+ /**
768
+ * Regex for go get error lines per package.
769
+ * Examples:
770
+ * go: module github.com/nonexistent/pkg: no matching versions for query "latest"
771
+ * go: github.com/some/pkg@v1.0.0: verifying module: ...
772
+ */
773
+ const GO_GET_ERROR_RE = /^go: (?:module\s+)?(\S+?)(?:@(\S+))?:\s+(.+)$/;
774
+ /** Parses `go get` output into structured result with success status, output text, resolved packages, and per-package status. */
775
+ export function parseGoGetOutput(stdout, stderr, exitCode, requestedPackages) {
254
776
  const combined = (stdout + "\n" + stderr).trim();
777
+ const resolvedPackages = [];
778
+ // Per-package status tracking (Gap #153)
779
+ const packageStatuses = new Map();
780
+ const lines = combined.split("\n");
781
+ for (const line of lines) {
782
+ const trimmed = line.trim();
783
+ // Match "go: upgraded <pkg> <old> => <new>" or "go: downgraded <pkg> <old> => <new>"
784
+ const upgradeMatch = trimmed.match(GO_GET_UPGRADED_RE);
785
+ if (upgradeMatch) {
786
+ resolvedPackages.push({
787
+ package: upgradeMatch[1],
788
+ previousVersion: upgradeMatch[2],
789
+ newVersion: upgradeMatch[3],
790
+ });
791
+ packageStatuses.set(upgradeMatch[1], {
792
+ path: upgradeMatch[1],
793
+ version: upgradeMatch[3],
794
+ });
795
+ continue;
796
+ }
797
+ // Match "go: added <pkg> <version>"
798
+ const addedMatch = trimmed.match(GO_GET_ADDED_RE);
799
+ if (addedMatch) {
800
+ resolvedPackages.push({
801
+ package: addedMatch[1],
802
+ newVersion: addedMatch[2],
803
+ });
804
+ packageStatuses.set(addedMatch[1], {
805
+ path: addedMatch[1],
806
+ version: addedMatch[2],
807
+ });
808
+ continue;
809
+ }
810
+ // Match per-package error lines (Gap #153)
811
+ const errorMatch = trimmed.match(GO_GET_ERROR_RE);
812
+ if (errorMatch) {
813
+ const pkgPath = errorMatch[1];
814
+ const errMsg = errorMatch[3];
815
+ // Skip "downloading" lines which are progress, not errors
816
+ if (!errMsg.startsWith("downloading ")) {
817
+ const errorType = categorizeGoGetError(errMsg);
818
+ packageStatuses.set(pkgPath, {
819
+ path: pkgPath,
820
+ version: errorMatch[2] || undefined,
821
+ error: errMsg,
822
+ errorType,
823
+ });
824
+ }
825
+ }
826
+ }
827
+ // If we have requestedPackages but some aren't in statuses, add them as successful (no error, no explicit status)
828
+ if (requestedPackages) {
829
+ for (const reqPkg of requestedPackages) {
830
+ // Strip version suffix from request (e.g., "github.com/pkg/errors@latest" -> "github.com/pkg/errors")
831
+ const pkgPath = reqPkg.replace(/@.*$/, "");
832
+ if (!packageStatuses.has(pkgPath)) {
833
+ // If the overall command succeeded, mark as successful
834
+ if (exitCode === 0) {
835
+ packageStatuses.set(pkgPath, { path: pkgPath });
836
+ }
837
+ }
838
+ }
839
+ }
840
+ const packages = packageStatuses.size > 0 ? Array.from(packageStatuses.values()) : undefined;
255
841
  return {
256
842
  success: exitCode === 0,
257
843
  output: combined || undefined,
844
+ resolvedPackages: resolvedPackages.length > 0 ? resolvedPackages : undefined,
845
+ packages,
258
846
  };
259
847
  }
848
+ function categorizeGoGetError(message) {
849
+ const lower = message.toLowerCase();
850
+ if (lower.includes("timeout") || lower.includes("context deadline exceeded")) {
851
+ return "timeout";
852
+ }
853
+ if (lower.includes("no such host") || lower.includes("lookup ")) {
854
+ return "dns";
855
+ }
856
+ if (lower.includes("authentication required") ||
857
+ lower.includes("permission denied") ||
858
+ lower.includes("access denied") ||
859
+ lower.includes("401") ||
860
+ lower.includes("403")) {
861
+ return "authentication";
862
+ }
863
+ if (lower.includes("dial tcp") || lower.includes("connection refused") || lower.includes("tls")) {
864
+ return "network";
865
+ }
866
+ return "unknown";
867
+ }
260
868
  //# sourceMappingURL=parsers.js.map