1
2
3
4
5
6 package vet
7
8 import (
9 "archive/zip"
10 "bytes"
11 "context"
12 "encoding/json"
13 "errors"
14 "fmt"
15 "io"
16 "os"
17 "slices"
18 "strconv"
19 "strings"
20 "sync"
21
22 "cmd/go/internal/base"
23 "cmd/go/internal/cfg"
24 "cmd/go/internal/load"
25 "cmd/go/internal/modload"
26 "cmd/go/internal/trace"
27 "cmd/go/internal/work"
28 )
29
30 var CmdVet = &base.Command{
31 CustomFlags: true,
32 UsageLine: "go vet [build flags] [-vettool prog] [vet flags] [packages]",
33 Short: "report likely mistakes in packages",
34 Long: `
35 Vet runs the Go vet tool (cmd/vet) on the named packages
36 and reports diagnostics.
37
38 It supports these flags:
39
40 -c int
41 display offending line with this many lines of context (default -1)
42 -json
43 emit JSON output
44 -fix
45 instead of printing each diagnostic, apply its first fix (if any)
46 -diff
47 instead of applying each fix, print the patch as a unified diff;
48 exit with a non-zero status if the diff is not empty
49
50 The -vettool=prog flag selects a different analysis tool with
51 alternative or additional checks. For example, the 'shadow' analyzer
52 can be built and run using these commands:
53
54 go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
55 go vet -vettool=$(which shadow)
56
57 Alternative vet tools should be built atop golang.org/x/tools/go/analysis/unitchecker,
58 which handles the interaction with go vet.
59
60 The default vet tool is 'go tool vet' or cmd/vet.
61 For help on its checkers and their flags, run 'go tool vet help'.
62 For details of a specific checker such as 'printf', see 'go tool vet help printf'.
63
64 For more about specifying packages, see 'go help packages'.
65
66 The build flags supported by go vet are those that control package resolution
67 and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
68 For more about these flags, see 'go help build'.
69
70 See also: go fmt, go fix.
71 `,
72 }
73
74 var CmdFix = &base.Command{
75 CustomFlags: true,
76 UsageLine: "go fix [build flags] [-fixtool prog] [fix flags] [packages]",
77 Short: "apply fixes suggested by static checkers",
78 Long: `
79 Fix runs the Go fix tool (cmd/fix) on the named packages
80 and applies suggested fixes.
81
82 It supports these flags:
83
84 -diff
85 instead of applying each fix, print the patch as a unified diff;
86 exit with a non-zero status if the diff is not empty
87
88 The -fixtool=prog flag selects a different analysis tool with
89 alternative or additional fixers; see the documentation for go vet's
90 -vettool flag for details.
91
92 The default fix tool is 'go tool fix' or cmd/fix.
93 For help on its fixers and their flags, run 'go tool fix help'.
94 For details of a specific fixer such as 'hostport', see 'go tool fix help hostport'.
95
96 For more about specifying packages, see 'go help packages'.
97
98 The build flags supported by go fix are those that control package resolution
99 and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
100 For more about these flags, see 'go help build'.
101
102 See also: go fmt, go vet.
103 `,
104 }
105
106 func init() {
107
108 CmdVet.Run = run
109 CmdFix.Run = run
110
111 addFlags(CmdVet)
112 addFlags(CmdFix)
113 }
114
115 var (
116
117 vetFixFlag = CmdVet.Flag.Bool("fix", false, "apply the first fix (if any) for each diagnostic")
118
119
120
121 fixFixFlag = CmdFix.Flag.String("fix", "", "obsolete; no effect")
122 )
123
124
125
126 func run(ctx context.Context, cmd *base.Command, args []string) {
127 moduleLoaderState := modload.NewState()
128
129 toolFlags, pkgArgs := toolFlags(cmd, args)
130
131
132
133 moduleLoaderState.InitWorkfile()
134
135 if cfg.DebugTrace != "" {
136 var close func() error
137 var err error
138 ctx, close, err = trace.Start(ctx, cfg.DebugTrace)
139 if err != nil {
140 base.Fatalf("failed to start trace: %v", err)
141 }
142 defer func() {
143 if err := close(); err != nil {
144 base.Fatalf("failed to stop trace: %v", err)
145 }
146 }()
147 }
148
149 ctx, span := trace.StartSpan(ctx, fmt.Sprint("Running ", cmd.Name(), " command"))
150 defer span.Done()
151
152 work.BuildInit(moduleLoaderState)
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181 work.VetExplicit = len(toolFlags) > 0
182
183 applyFixes := false
184 if cmd.Name() == "fix" || *vetFixFlag {
185
186 if jsonFlag {
187 if diffFlag {
188 base.Fatalf("-json and -diff cannot be used together")
189 }
190 } else {
191 toolFlags = append(toolFlags, "-fix")
192 if diffFlag {
193 toolFlags = append(toolFlags, "-diff")
194
195
196
197 work.VetHandleStdout = copyAndDetectDiff
198 } else {
199 applyFixes = true
200 }
201 }
202 if contextFlag != -1 {
203 base.Fatalf("-c flag cannot be used when applying fixes")
204 }
205 } else {
206
207 if !jsonFlag {
208
209
210
211
212
213 toolFlags = append(toolFlags, "-json")
214 work.VetHandleStdout = printJSONDiagnostics
215 }
216 if diffFlag {
217 base.Fatalf("go vet -diff flag requires -fix")
218 }
219 }
220
221
222 if *fixFixFlag != "" {
223 fmt.Fprintf(os.Stderr, "go %s: the -fix=%s flag is obsolete and has no effect\n", cmd.Name(), *fixFixFlag)
224
225
226 if slices.Contains(strings.Split(*fixFixFlag, ","), "buildtag") {
227 fmt.Fprintf(os.Stderr, "go %s: to enable the buildtag check, use -buildtag\n", cmd.Name())
228 }
229 }
230
231 work.VetFlags = toolFlags
232
233 pkgOpts := load.PackageOpts{ModResolveTests: true}
234 pkgs := load.PackagesAndErrors(moduleLoaderState, ctx, pkgOpts, pkgArgs)
235 load.CheckPackageErrors(pkgs)
236 if len(pkgs) == 0 {
237 base.Fatalf("no packages to %s", cmd.Name())
238 }
239
240
241 b := work.NewBuilder("", moduleLoaderState.VendorDirOrEmpty)
242 defer func() {
243 if err := b.Close(); err != nil {
244 base.Fatal(err)
245 }
246 }()
247
248 root := &work.Action{Mode: "go " + cmd.Name()}
249
250 addVetAction := func(p *load.Package) {
251 act := b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, applyFixes, p)
252 root.Deps = append(root.Deps, act)
253 }
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270 for _, p := range pkgs {
271
272
273
274 if applyFixes {
275 if p.Standard && strings.HasPrefix(p.ImportPath, "vendor/") ||
276 p.Module != nil && !p.Module.Main {
277 continue
278 }
279 }
280 _, ptest, pxtest, perr := load.TestPackagesFor(moduleLoaderState, ctx, pkgOpts, p, nil)
281 if perr != nil {
282 base.Errorf("%v", perr.Error)
283 continue
284 }
285 if len(ptest.GoFiles) == 0 && len(ptest.CgoFiles) == 0 && pxtest == nil {
286 base.Errorf("go: can't %s %s: no Go files in %s", cmd.Name(), p.ImportPath, p.Dir)
287 continue
288 }
289 if len(ptest.GoFiles) > 0 || len(ptest.CgoFiles) > 0 {
290
291 addVetAction(ptest)
292 }
293 if pxtest != nil {
294 addVetAction(pxtest)
295 }
296 }
297 b.Do(ctx, root)
298
299
300
301
302
303
304
305
306 if applyFixes {
307 contents := make(map[string][]byte)
308
309 for _, act := range root.Deps {
310 if act.FixArchive != "" {
311 if err := readZip(act.FixArchive, contents); err != nil {
312 base.Errorf("reading archive of fixes: %v", err)
313 return
314 }
315 }
316 }
317
318 for filename, content := range contents {
319 if err := os.WriteFile(filename, content, 0644); err != nil {
320 base.Errorf("applying fix: %v", err)
321 }
322 }
323 }
324 }
325
326
327
328 func readZip(zipfile string, out map[string][]byte) error {
329 r, err := zip.OpenReader(zipfile)
330 if err != nil {
331 return err
332 }
333 defer r.Close()
334 for _, f := range r.File {
335 rc, err := f.Open()
336 if err != nil {
337 return err
338 }
339 content, err := io.ReadAll(rc)
340 rc.Close()
341 if err != nil {
342 return err
343 }
344 if prev, ok := out[f.Name]; ok && !bytes.Equal(prev, content) {
345 return fmt.Errorf("inconsistent fixes to file %v", f.Name)
346 }
347 out[f.Name] = content
348 }
349 return nil
350 }
351
352
353
354
355
356
357 func copyAndDetectDiff(r io.Reader) error {
358 stdouterrMu.Lock()
359 defer stdouterrMu.Unlock()
360 n, err := io.Copy(os.Stdout, r)
361 if err != nil {
362 return fmt.Errorf("copying diff output: %w", err)
363 }
364 if n > 0 {
365 base.SetExitStatus(1)
366 }
367 return nil
368 }
369
370
371
372
373 func printJSONDiagnostics(r io.Reader) error {
374 stdout, err := io.ReadAll(r)
375 if err != nil {
376 return err
377 }
378 if len(stdout) > 0 {
379
380
381 var tree jsonTree
382 if err := json.Unmarshal(stdout, &tree); err != nil {
383 return fmt.Errorf("parsing JSON: %v", err)
384 }
385 for _, units := range tree {
386 for analyzer, msg := range units {
387 if msg[0] == '[' {
388
389 var diags []jsonDiagnostic
390 if err := json.Unmarshal([]byte(msg), &diags); err != nil {
391 return fmt.Errorf("parsing JSON diagnostics: %v", err)
392 }
393 for _, diag := range diags {
394 base.SetExitStatus(1)
395 printJSONDiagnostic(analyzer, diag)
396 }
397 } else {
398
399 var e jsonError
400 if err := json.Unmarshal([]byte(msg), &e); err != nil {
401 return fmt.Errorf("parsing JSON error: %v", err)
402 }
403
404 base.SetExitStatus(1)
405 return errors.New(e.Err)
406 }
407 }
408 }
409 }
410 return nil
411 }
412
413 var stdouterrMu sync.Mutex
414
415 func printJSONDiagnostic(analyzer string, diag jsonDiagnostic) {
416 stdouterrMu.Lock()
417 defer stdouterrMu.Unlock()
418
419 type posn struct {
420 file string
421 line, col int
422 }
423 parsePosn := func(s string) (_ posn, _ bool) {
424 colon2 := strings.LastIndexByte(s, ':')
425 if colon2 < 0 {
426 return
427 }
428 colon1 := strings.LastIndexByte(s[:colon2], ':')
429 if colon1 < 0 {
430 return
431 }
432 line, err := strconv.Atoi(s[colon1+len(":") : colon2])
433 if err != nil {
434 return
435 }
436 col, err := strconv.Atoi(s[colon2+len(":"):])
437 if err != nil {
438 return
439 }
440 return posn{s[:colon1], line, col}, true
441 }
442
443 print := func(start, end, message string) {
444 if posn, ok := parsePosn(start); ok {
445
446
447
448
449 fmt.Fprintf(os.Stderr, "%s:%d:%d: %v\n", base.ShortPath(posn.file), posn.line, posn.col, message)
450 } else {
451 fmt.Fprintf(os.Stderr, "%s: %v\n", start, message)
452 }
453
454
455
456 if contextFlag >= 0 {
457 if end == "" {
458 end = start
459 }
460 var (
461 startPosn, ok1 = parsePosn(start)
462 endPosn, ok2 = parsePosn(end)
463 )
464 if ok1 && ok2 {
465
466 data, _ := os.ReadFile(startPosn.file)
467 lines := strings.Split(string(data), "\n")
468 for i := startPosn.line - contextFlag; i <= endPosn.line+contextFlag; i++ {
469 if 1 <= i && i <= len(lines) {
470 fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1])
471 }
472 }
473 }
474 }
475 }
476
477
478
479 _ = analyzer
480 print(diag.Posn, diag.End, diag.Message)
481 for _, rel := range diag.Related {
482 print(rel.Posn, rel.End, "\t"+rel.Message)
483 }
484 }
485
486
487
488
489
490
491
492 type jsonTree map[string]map[string]json.RawMessage
493
494 type jsonError struct {
495 Err string `json:"error"`
496 }
497
498
499
500
501 type jsonTextEdit struct {
502 Filename string `json:"filename"`
503 Start int `json:"start"`
504 End int `json:"end"`
505 New string `json:"new"`
506 }
507
508
509
510
511 type jsonSuggestedFix struct {
512 Message string `json:"message"`
513 Edits []jsonTextEdit `json:"edits"`
514 }
515
516
517 type jsonDiagnostic struct {
518 Category string `json:"category,omitempty"`
519 Posn string `json:"posn"`
520 End string `json:"end"`
521 Message string `json:"message"`
522 SuggestedFixes []jsonSuggestedFix `json:"suggested_fixes,omitempty"`
523 Related []jsonRelatedInformation `json:"related,omitempty"`
524 }
525
526
527
528 type jsonRelatedInformation struct {
529 Posn string `json:"posn"`
530 End string `json:"end"`
531 Message string `json:"message"`
532 }
533
View as plain text