1// Copyright 2016 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// +build ignore
6
7// The vet/all command runs go vet on the standard library and commands.
8// It compares the output against a set of whitelists
9// maintained in the whitelist directory.
10//
11// This program attempts to build packages from golang.org/x/tools,
12// which must be in your GOPATH.
13package main
14
15import (
16	"bufio"
17	"bytes"
18	"flag"
19	"fmt"
20	"go/build"
21	"go/types"
22	"internal/testenv"
23	"io"
24	"io/ioutil"
25	"log"
26	"os"
27	"os/exec"
28	"path/filepath"
29	"runtime"
30	"strings"
31	"sync/atomic"
32)
33
34var (
35	flagPlatforms = flag.String("p", "", "platform(s) to use e.g. linux/amd64,darwin/386")
36	flagAll       = flag.Bool("all", false, "run all platforms")
37	flagNoLines   = flag.Bool("n", false, "don't print line numbers")
38)
39
40var cmdGoPath string
41var failed uint32 // updated atomically
42
43func main() {
44	log.SetPrefix("vet/all: ")
45	log.SetFlags(0)
46
47	var err error
48	cmdGoPath, err = testenv.GoTool()
49	if err != nil {
50		log.Print("could not find cmd/go; skipping")
51		// We're on a platform that can't run cmd/go.
52		// We want this script to be able to run as part of all.bash,
53		// so return cleanly rather than with exit code 1.
54		return
55	}
56
57	flag.Parse()
58	switch {
59	case *flagAll && *flagPlatforms != "":
60		log.Print("-all and -p flags are incompatible")
61		flag.Usage()
62		os.Exit(2)
63	case *flagPlatforms != "":
64		vetPlatforms(parseFlagPlatforms())
65	case *flagAll:
66		vetPlatforms(allPlatforms())
67	default:
68		hostPlatform.vet()
69	}
70	if atomic.LoadUint32(&failed) != 0 {
71		os.Exit(1)
72	}
73}
74
75var hostPlatform = platform{os: build.Default.GOOS, arch: build.Default.GOARCH}
76
77func allPlatforms() []platform {
78	var pp []platform
79	cmd := exec.Command(cmdGoPath, "tool", "dist", "list")
80	out, err := cmd.Output()
81	if err != nil {
82		log.Fatal(err)
83	}
84	lines := bytes.Split(out, []byte{'\n'})
85	for _, line := range lines {
86		if len(line) == 0 {
87			continue
88		}
89		pp = append(pp, parsePlatform(string(line)))
90	}
91	return pp
92}
93
94func parseFlagPlatforms() []platform {
95	var pp []platform
96	components := strings.Split(*flagPlatforms, ",")
97	for _, c := range components {
98		pp = append(pp, parsePlatform(c))
99	}
100	return pp
101}
102
103func parsePlatform(s string) platform {
104	vv := strings.Split(s, "/")
105	if len(vv) != 2 {
106		log.Fatalf("could not parse platform %s, must be of form goos/goarch", s)
107	}
108	return platform{os: vv[0], arch: vv[1]}
109}
110
111type whitelist map[string]int
112
113// load adds entries from the whitelist file, if present, for os/arch to w.
114func (w whitelist) load(goos string, goarch string) {
115	sz := types.SizesFor("gc", goarch)
116	if sz == nil {
117		log.Fatalf("unknown type sizes for arch %q", goarch)
118	}
119	archbits := 8 * sz.Sizeof(types.Typ[types.UnsafePointer])
120
121	// Look up whether goarch has a shared arch suffix,
122	// such as mips64x for mips64 and mips64le.
123	archsuff := goarch
124	if x, ok := archAsmX[goarch]; ok {
125		archsuff = x
126	}
127
128	// Load whitelists.
129	filenames := []string{
130		"all.txt",
131		goos + ".txt",
132		goarch + ".txt",
133		goos + "_" + goarch + ".txt",
134		fmt.Sprintf("%dbit.txt", archbits),
135	}
136	if goarch != archsuff {
137		filenames = append(filenames,
138			archsuff+".txt",
139			goos+"_"+archsuff+".txt",
140		)
141	}
142
143	// We allow error message templates using GOOS and GOARCH.
144	if goos == "android" {
145		goos = "linux" // so many special cases :(
146	}
147
148	// Read whitelists and do template substitution.
149	replace := strings.NewReplacer("GOOS", goos, "GOARCH", goarch, "ARCHSUFF", archsuff)
150
151	for _, filename := range filenames {
152		path := filepath.Join("whitelist", filename)
153		f, err := os.Open(path)
154		if err != nil {
155			// Allow not-exist errors; not all combinations have whitelists.
156			if os.IsNotExist(err) {
157				continue
158			}
159			log.Fatal(err)
160		}
161		scan := bufio.NewScanner(f)
162		for scan.Scan() {
163			line := scan.Text()
164			if len(line) == 0 || strings.HasPrefix(line, "//") {
165				continue
166			}
167			w[replace.Replace(line)]++
168		}
169		if err := scan.Err(); err != nil {
170			log.Fatal(err)
171		}
172	}
173}
174
175type platform struct {
176	os   string
177	arch string
178}
179
180func (p platform) String() string {
181	return p.os + "/" + p.arch
182}
183
184// ignorePathPrefixes are file path prefixes that should be ignored wholesale.
185var ignorePathPrefixes = [...]string{
186	// These testdata dirs have lots of intentionally broken/bad code for tests.
187	"cmd/go/testdata/",
188	"cmd/vet/testdata/",
189	"go/printer/testdata/",
190}
191
192func vetPlatforms(pp []platform) {
193	for _, p := range pp {
194		p.vet()
195	}
196}
197
198func (p platform) vet() {
199	if p.os == "linux" && (p.arch == "riscv64" || p.arch == "sparc64") {
200		// TODO(tklauser): enable as soon as these ports have fully landed
201		fmt.Printf("skipping %s/%s\n", p.os, p.arch)
202		return
203	}
204
205	if p.os == "windows" && p.arch == "arm" {
206		// TODO(jordanrh1): enable as soon as the windows/arm port has fully landed
207		fmt.Println("skipping windows/arm")
208		return
209	}
210
211	if p.os == "aix" && p.arch == "ppc64" {
212		// TODO(aix): enable as soon as the aix/ppc64 port has fully landed
213		fmt.Println("skipping aix/ppc64")
214		return
215	}
216
217	var buf bytes.Buffer
218	fmt.Fprintf(&buf, "go run main.go -p %s\n", p)
219
220	// Load whitelist(s).
221	w := make(whitelist)
222	w.load(p.os, p.arch)
223
224	tmpdir, err := ioutil.TempDir("", "cmd-vet-all")
225	if err != nil {
226		log.Fatal(err)
227	}
228	defer os.RemoveAll(tmpdir)
229
230	// Build the go/packages-based vet command from the x/tools
231	// repo. It is considerably faster than "go vet", which rebuilds
232	// the standard library.
233	vetTool := filepath.Join(tmpdir, "vet")
234	cmd := exec.Command(cmdGoPath, "build", "-o", vetTool, "golang.org/x/tools/go/analysis/cmd/vet")
235	cmd.Dir = filepath.Join(runtime.GOROOT(), "src")
236	cmd.Stderr = os.Stderr
237	cmd.Stdout = os.Stderr
238	if err := cmd.Run(); err != nil {
239		log.Fatal(err)
240	}
241
242	// TODO: The unsafeptr checks are disabled for now,
243	// because there are so many false positives,
244	// and no clear way to improve vet to eliminate large chunks of them.
245	// And having them in the whitelists will just cause annoyance
246	// and churn when working on the runtime.
247	cmd = exec.Command(vetTool,
248		"-unsafeptr=0",
249		"-nilness=0", // expensive, uses SSA
250		"std",
251		"cmd/...",
252		"cmd/compile/internal/gc/testdata",
253	)
254	cmd.Dir = filepath.Join(runtime.GOROOT(), "src")
255	cmd.Env = append(os.Environ(), "GOOS="+p.os, "GOARCH="+p.arch, "CGO_ENABLED=0")
256	stderr, err := cmd.StderrPipe()
257	if err != nil {
258		log.Fatal(err)
259	}
260	if err := cmd.Start(); err != nil {
261		log.Fatal(err)
262	}
263
264	// Process vet output.
265	scan := bufio.NewScanner(stderr)
266	var parseFailed bool
267NextLine:
268	for scan.Scan() {
269		line := scan.Text()
270		if strings.HasPrefix(line, "vet: ") {
271			// Typecheck failure: Malformed syntax or multiple packages or the like.
272			// This will yield nicer error messages elsewhere, so ignore them here.
273
274			// This includes warnings from asmdecl of the form:
275			//   "vet: foo.s:16: [amd64] cannot check cross-package assembly function"
276			continue
277		}
278
279		if strings.HasPrefix(line, "panic: ") {
280			// Panic in vet. Don't filter anything, we want the complete output.
281			parseFailed = true
282			fmt.Fprintf(os.Stderr, "panic in vet (to reproduce: go run main.go -p %s):\n", p)
283			fmt.Fprintln(os.Stderr, line)
284			io.Copy(os.Stderr, stderr)
285			break
286		}
287		if strings.HasPrefix(line, "# ") {
288			// 'go vet' prefixes the output of each vet invocation by a comment:
289			//    # [package]
290			continue
291		}
292
293		// Parse line.
294		// Assume the part before the first ": "
295		// is the "file:line:col: " information.
296		// TODO(adonovan): parse vet -json output.
297		var file, lineno, msg string
298		if i := strings.Index(line, ": "); i >= 0 {
299			msg = line[i+len(": "):]
300
301			words := strings.Split(line[:i], ":")
302			switch len(words) {
303			case 3:
304				_ = words[2] // ignore column
305				fallthrough
306			case 2:
307				lineno = words[1]
308				fallthrough
309			case 1:
310				file = words[0]
311
312				// Make the file name relative to GOROOT/src.
313				if rel, err := filepath.Rel(cmd.Dir, file); err == nil {
314					file = rel
315				}
316			default:
317				// error: too many columns
318			}
319		}
320		if file == "" {
321			if !parseFailed {
322				parseFailed = true
323				fmt.Fprintf(os.Stderr, "failed to parse %s vet output:\n", p)
324			}
325			fmt.Fprintln(os.Stderr, line)
326			continue
327		}
328
329		msg = strings.TrimSpace(msg)
330
331		for _, ignore := range ignorePathPrefixes {
332			if strings.HasPrefix(file, filepath.FromSlash(ignore)) {
333				continue NextLine
334			}
335		}
336
337		key := file + ": " + msg
338		if w[key] == 0 {
339			// Vet error with no match in the whitelist. Print it.
340			if *flagNoLines {
341				fmt.Fprintf(&buf, "%s: %s\n", file, msg)
342			} else {
343				fmt.Fprintf(&buf, "%s:%s: %s\n", file, lineno, msg)
344			}
345			atomic.StoreUint32(&failed, 1)
346			continue
347		}
348		w[key]--
349	}
350	if parseFailed {
351		atomic.StoreUint32(&failed, 1)
352		return
353	}
354	if scan.Err() != nil {
355		log.Fatalf("failed to scan vet output: %v", scan.Err())
356	}
357	err = cmd.Wait()
358	// We expect vet to fail.
359	// Make sure it has failed appropriately, though (for example, not a PathError).
360	if _, ok := err.(*exec.ExitError); !ok {
361		log.Fatalf("unexpected go vet execution failure: %v", err)
362	}
363	printedHeader := false
364	if len(w) > 0 {
365		for k, v := range w {
366			if v != 0 {
367				if !printedHeader {
368					fmt.Fprintln(&buf, "unmatched whitelist entries:")
369					printedHeader = true
370				}
371				for i := 0; i < v; i++ {
372					fmt.Fprintln(&buf, k)
373				}
374				atomic.StoreUint32(&failed, 1)
375			}
376		}
377	}
378
379	os.Stdout.Write(buf.Bytes())
380}
381
382// archAsmX maps architectures to the suffix usually used for their assembly files,
383// if different than the arch name itself.
384var archAsmX = map[string]string{
385	"android":  "linux",
386	"mips64":   "mips64x",
387	"mips64le": "mips64x",
388	"mips":     "mipsx",
389	"mipsle":   "mipsx",
390	"ppc64":    "ppc64x",
391	"ppc64le":  "ppc64x",
392}
393