1// Copyright 2019 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package main
6
7import (
8	"encoding/json"
9	"fmt"
10	"io/ioutil"
11	"os"
12	"path"
13	"path/filepath"
14	"strings"
15)
16
17type useTidyMode int
18
19const clangTidyCrashSubstring = "PLEASE submit a bug report"
20
21const (
22	tidyModeNone useTidyMode = iota
23	tidyModeAll
24	tidyModeTricium
25)
26
27func processClangTidyFlags(builder *commandBuilder) (cSrcFile string, clangTidyFlags []string, mode useTidyMode) {
28	builder.transformArgs(func(arg builderArg) string {
29		const prefix = "-clang-tidy-flag="
30		if !strings.HasPrefix(arg.value, prefix) {
31			return arg.value
32		}
33
34		clangTidyFlags = append(clangTidyFlags, arg.value[len(prefix):])
35		return ""
36	})
37
38	withTidy, _ := builder.env.getenv("WITH_TIDY")
39	if withTidy == "" {
40		return "", clangTidyFlags, tidyModeNone
41	}
42	srcFileSuffixes := []string{
43		".c",
44		".cc",
45		".cpp",
46		".C",
47		".cxx",
48		".c++",
49	}
50	cSrcFile = ""
51	srcSuffix := ""
52	lastArg := ""
53	for _, arg := range builder.args {
54		if lastArg != "-o" {
55			for _, suffix := range srcFileSuffixes {
56				if strings.HasSuffix(arg.value, suffix) {
57					srcSuffix = suffix
58					cSrcFile = arg.value
59					break
60				}
61			}
62		}
63		lastArg = arg.value
64	}
65
66	if cSrcFile == "" {
67		return "", clangTidyFlags, tidyModeNone
68	}
69
70	if withTidy == "tricium" {
71		// Files generated from protobufs can result in _many_ clang-tidy complaints, and aren't
72		// worth linting in general. Don't.
73		if strings.HasSuffix(cSrcFile, ".pb"+srcSuffix) {
74			mode = tidyModeNone
75		} else {
76			mode = tidyModeTricium
77		}
78	} else {
79		mode = tidyModeAll
80	}
81	return cSrcFile, clangTidyFlags, mode
82}
83
84func calcClangTidyInvocation(env env, clangCmd *command, cSrcFile string, tidyFlags ...string) (*command, error) {
85	resourceDir, err := getClangResourceDir(env, clangCmd.Path)
86	if err != nil {
87		return nil, err
88	}
89
90	clangTidyPath := filepath.Join(filepath.Dir(clangCmd.Path), "clang-tidy")
91	args := append([]string{}, tidyFlags...)
92	args = append(args, cSrcFile, "--", "-resource-dir="+resourceDir)
93	args = append(args, clangCmd.Args...)
94	return &command{
95		Path:       clangTidyPath,
96		Args:       args,
97		EnvUpdates: clangCmd.EnvUpdates,
98	}, nil
99}
100
101func runClangTidyForTricium(env env, clangCmd *command, cSrcFile, fixesDir string, extraTidyFlags []string, crashArtifactsDir string) error {
102	if err := os.MkdirAll(fixesDir, 0777); err != nil {
103		return fmt.Errorf("creating fixes directory at %q: %v", fixesDir, err)
104	}
105
106	f, err := ioutil.TempFile(fixesDir, "lints-")
107	if err != nil {
108		return fmt.Errorf("making tempfile for tidy: %v", err)
109	}
110	f.Close()
111
112	// `f` is an 'anchor'; it ensures we won't create a similarly-named file in the future.
113	// Hence, we can't delete it.
114	fixesFilePath := f.Name() + ".yaml"
115	fixesMetadataPath := f.Name() + ".json"
116
117	// FIXME(gbiv): Remove `-checks=*` when testing is complete; we should defer to .clang-tidy
118	// files, which are both more expressive and more approachable than `-checks=*`.
119	extraTidyFlags = append(extraTidyFlags, "-checks=*", "--export-fixes="+fixesFilePath)
120	clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, extraTidyFlags...)
121	if err != nil {
122		return fmt.Errorf("calculating tidy invocation: %v", err)
123	}
124
125	stdstreams := &strings.Builder{}
126	// Note: We pass nil as stdin as we checked before that the compiler
127	// was invoked with a source file argument.
128	exitCode, err := wrapSubprocessErrorWithSourceLoc(clangTidyCmd,
129		env.run(clangTidyCmd, nil, stdstreams, stdstreams))
130	if err != nil {
131		return err
132	}
133
134	type crashOutput struct {
135		CrashReproducerPath string `json:"crash_reproducer_path"`
136		Stdstreams          string `json:"stdstreams"`
137	}
138
139	type metadata struct {
140		Args        []string     `json:"args"`
141		CrashOutput *crashOutput `json:"crash_output"`
142		Executable  string       `json:"executable"`
143		ExitCode    int          `json:"exit_code"`
144		LintTarget  string       `json:"lint_target"`
145		Stdstreams  string       `json:"stdstreams"`
146		Wd          string       `json:"wd"`
147	}
148
149	meta := &metadata{
150		Args:        clangTidyCmd.Args,
151		CrashOutput: nil,
152		Executable:  clangTidyCmd.Path,
153		ExitCode:    exitCode,
154		LintTarget:  cSrcFile,
155		Stdstreams:  stdstreams.String(),
156		Wd:          env.getwd(),
157	}
158
159	// Sometimes, clang-tidy crashes. Unfortunately, these don't get funnelled through the
160	// standard clang crash machinery. :(. Try to work with our own.
161	if crashArtifactsDir != "" && strings.Contains(meta.Stdstreams, clangTidyCrashSubstring) {
162		tidyCrashArtifacts := path.Join(crashArtifactsDir, "clang-tidy")
163		if err := os.MkdirAll(tidyCrashArtifacts, 0777); err != nil {
164			return fmt.Errorf("creating crash artifacts directory at %q: %v", tidyCrashArtifacts, err)
165		}
166
167		f, err := ioutil.TempFile(tidyCrashArtifacts, "crash-")
168		if err != nil {
169			return fmt.Errorf("making tempfile for crash output: %v", err)
170		}
171		f.Close()
172
173		reproCmd := &command{}
174		*reproCmd = *clangCmd
175		reproCmd.Args = append(reproCmd.Args, "-E", "-o", f.Name())
176
177		reproOut := &strings.Builder{}
178		_, err = wrapSubprocessErrorWithSourceLoc(reproCmd, env.run(reproCmd, nil, reproOut, reproOut))
179		if err != nil {
180			return fmt.Errorf("attempting to produce a clang-tidy crash reproducer: %v", err)
181		}
182		meta.CrashOutput = &crashOutput{
183			CrashReproducerPath: f.Name(),
184			Stdstreams:          reproOut.String(),
185		}
186	}
187
188	f, err = os.Create(fixesMetadataPath)
189	if err != nil {
190		return fmt.Errorf("creating fixes metadata: %v", err)
191	}
192
193	if err := json.NewEncoder(f).Encode(meta); err != nil {
194		return fmt.Errorf("writing fixes metadata: %v", err)
195	}
196
197	if err := f.Close(); err != nil {
198		return fmt.Errorf("finalizing fixes metadata: %v", err)
199	}
200	return nil
201}
202
203func runClangTidy(env env, clangCmd *command, cSrcFile string, extraTidyFlags []string) error {
204	extraTidyFlags = append(extraTidyFlags,
205		"-checks="+strings.Join([]string{
206			"*",
207			"-bugprone-narrowing-conversions",
208			"-cppcoreguidelines-*",
209			"-fuchsia-*",
210			"-google-readability*",
211			"-google-runtime-references",
212			"-hicpp-*",
213			"-llvm-*",
214			"-misc-non-private-member-variables-in-classes",
215			"-misc-unused-parameters",
216			"-modernize-*",
217			"-readability-*",
218		}, ","))
219	clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, extraTidyFlags...)
220	if err != nil {
221		return fmt.Errorf("calculating clang-tidy invocation: %v", err)
222	}
223
224	// Note: We pass nil as stdin as we checked before that the compiler
225	// was invoked with a source file argument.
226	exitCode, err := wrapSubprocessErrorWithSourceLoc(clangTidyCmd,
227		env.run(clangTidyCmd, nil, env.stdout(), env.stderr()))
228	if err == nil && exitCode != 0 {
229		// Note: We continue on purpose when clang-tidy fails
230		// to maintain compatibility with the previous wrapper.
231		fmt.Fprint(env.stderr(), "clang-tidy failed")
232	}
233	return err
234}
235
236func hasAtLeastOneSuffix(s string, suffixes []string) bool {
237	for _, suffix := range suffixes {
238		if strings.HasSuffix(s, suffix) {
239			return true
240		}
241	}
242	return false
243}
244