wrapper: add support for Tricium clang-tidy
This adds a special `WITH_TIDY` mode that's specifically for use with
Tricium. Crucially, this has us dump diagnostics as YAML, and stash a
fair amount of information about each clang-tidy invocation in the same
place where we dump YAML.
These bits are intended to be used by the script added in
I54ecc88d38faa4bfd502d632d3fd5c74734dabc0.
BUG=chromium:1035951
TEST=Ran on all platform2 packages `emerge`able on amd64-generic.
Change-Id: I63ef06dc6ddc016ebb6ba0c4a0cea8320fef7415
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2245785
Reviewed-by: Manoj Gupta <manojgupta@chromium.org>
Tested-by: George Burgess <gbiv@chromium.org>
diff --git a/compiler_wrapper/clang_tidy_flag.go b/compiler_wrapper/clang_tidy_flag.go
index d8bf3cb..edbbb41 100644
--- a/compiler_wrapper/clang_tidy_flag.go
+++ b/compiler_wrapper/clang_tidy_flag.go
@@ -5,15 +5,26 @@
package main
import (
+ "encoding/json"
"fmt"
+ "io/ioutil"
+ "os"
"path/filepath"
"strings"
)
-func processClangTidyFlags(builder *commandBuilder) (cSrcFile string, useClangTidy bool) {
+type useTidyMode int
+
+const (
+ tidyModeNone useTidyMode = iota
+ tidyModeAll
+ tidyModeTricium
+)
+
+func processClangTidyFlags(builder *commandBuilder) (cSrcFile string, mode useTidyMode) {
withTidy, _ := builder.env.getenv("WITH_TIDY")
if withTidy == "" {
- return "", false
+ return "", tidyModeNone
}
srcFileSuffixes := []string{
".c",
@@ -24,48 +35,138 @@
".c++",
}
cSrcFile = ""
+ srcSuffix := ""
lastArg := ""
for _, arg := range builder.args {
- if hasAtLeastOneSuffix(arg.value, srcFileSuffixes) && lastArg != "-o" {
- cSrcFile = arg.value
+ if lastArg != "-o" {
+ for _, suffix := range srcFileSuffixes {
+ if strings.HasSuffix(arg.value, suffix) {
+ srcSuffix = suffix
+ cSrcFile = arg.value
+ break
+ }
+ }
}
lastArg = arg.value
}
- useClangTidy = cSrcFile != ""
- return cSrcFile, useClangTidy
+
+ if cSrcFile == "" {
+ return "", tidyModeNone
+ }
+
+ if withTidy == "tricium" {
+ // Files generated from protobufs can result in _many_ clang-tidy complaints, and aren't
+ // worth linting in general. Don't.
+ if strings.HasSuffix(cSrcFile, ".pb"+srcSuffix) {
+ mode = tidyModeNone
+ } else {
+ mode = tidyModeTricium
+ }
+ } else {
+ mode = tidyModeAll
+ }
+ return cSrcFile, mode
}
-func runClangTidy(env env, clangCmd *command, cSrcFile string) error {
- defaultTidyChecks := strings.Join([]string{
- "*",
- "-bugprone-narrowing-conversions",
- "-cppcoreguidelines-*",
- "-fuchsia-*",
- "-google-readability*",
- "-google-runtime-references",
- "-hicpp-*",
- "-llvm-*",
- "-misc-non-private-member-variables-in-classes",
- "-misc-unused-parameters",
- "-modernize-*",
- "-readability-*",
- }, ",")
-
+func calcClangTidyInvocation(env env, clangCmd *command, cSrcFile string, tidyFlags ...string) (*command, error) {
resourceDir, err := getClangResourceDir(env, clangCmd.Path)
if err != nil {
+ return nil, err
+ }
+
+ clangTidyPath := filepath.Join(filepath.Dir(clangCmd.Path), "clang-tidy")
+ args := append([]string{}, tidyFlags...)
+ args = append(args, cSrcFile, "--", "-resource-dir="+resourceDir)
+ args = append(args, clangCmd.Args...)
+ return &command{
+ Path: clangTidyPath,
+ Args: args,
+ EnvUpdates: clangCmd.EnvUpdates,
+ }, nil
+}
+
+func runClangTidyForTricium(env env, clangCmd *command, cSrcFile, fixesDir string) error {
+ if err := os.MkdirAll(fixesDir, 0777); err != nil {
+ return fmt.Errorf("creating fixes directory at %q: %v", fixesDir, err)
+ }
+
+ f, err := ioutil.TempFile(fixesDir, "lints-")
+ if err != nil {
+ return fmt.Errorf("making tempfile for tidy: %v", err)
+ }
+ f.Close()
+
+ // `f` is an 'anchor'; it ensures we won't create a similarly-named file in the future.
+ // Hence, we can't delete it.
+ fixesFilePath := f.Name() + ".yaml"
+ fixesMetadataPath := f.Name() + ".json"
+
+ // FIXME(gbiv): Remove `-checks=*` when testing is complete; we should defer to .clang-tidy
+ // files, which are both more expressive and more approachable than `-checks=*`.
+ clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, "-checks=*", "--export-fixes="+fixesFilePath)
+ if err != nil {
+ return fmt.Errorf("calculating tidy invocation: %v", err)
+ }
+
+ stdstreams := &strings.Builder{}
+ // Note: We pass nil as stdin as we checked before that the compiler
+ // was invoked with a source file argument.
+ exitCode, err := wrapSubprocessErrorWithSourceLoc(clangTidyCmd,
+ env.run(clangTidyCmd, nil, stdstreams, stdstreams))
+ if err != nil {
return err
}
- clangTidyPath := filepath.Join(filepath.Dir(clangCmd.Path), "clang-tidy")
- clangTidyCmd := &command{
- Path: clangTidyPath,
- Args: append([]string{
- "-checks=" + defaultTidyChecks,
- cSrcFile,
- "--",
- "-resource-dir=" + resourceDir,
- }, clangCmd.Args...),
- EnvUpdates: clangCmd.EnvUpdates,
+ type metadata struct {
+ Args []string `json:"args"`
+ Executable string `json:"executable"`
+ ExitCode int `json:"exit_code"`
+ LintTarget string `json:"lint_target"`
+ Stdstreams string `json:"stdstreams"`
+ Wd string `json:"wd"`
+ }
+
+ f, err = os.Create(fixesMetadataPath)
+ if err != nil {
+ return fmt.Errorf("creating fixes metadata: %v", err)
+ }
+
+ meta := &metadata{
+ Args: clangTidyCmd.Args,
+ Executable: clangTidyCmd.Path,
+ ExitCode: exitCode,
+ LintTarget: cSrcFile,
+ Stdstreams: stdstreams.String(),
+ Wd: env.getwd(),
+ }
+ if err := json.NewEncoder(f).Encode(meta); err != nil {
+ return fmt.Errorf("writing fixes metadata: %v", err)
+ }
+
+ if err := f.Close(); err != nil {
+ return fmt.Errorf("finalizing fixes metadata: %v", err)
+ }
+ return nil
+}
+
+func runClangTidy(env env, clangCmd *command, cSrcFile string) error {
+ clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, "-checks="+
+ strings.Join([]string{
+ "*",
+ "-bugprone-narrowing-conversions",
+ "-cppcoreguidelines-*",
+ "-fuchsia-*",
+ "-google-readability*",
+ "-google-runtime-references",
+ "-hicpp-*",
+ "-llvm-*",
+ "-misc-non-private-member-variables-in-classes",
+ "-misc-unused-parameters",
+ "-modernize-*",
+ "-readability-*",
+ }, ","))
+ if err != nil {
+ return fmt.Errorf("calculating clang-tidy invocation: %v", err)
}
// Note: We pass nil as stdin as we checked before that the compiler
diff --git a/compiler_wrapper/clang_tidy_flag_test.go b/compiler_wrapper/clang_tidy_flag_test.go
index baf5219..d45d1bd 100644
--- a/compiler_wrapper/clang_tidy_flag_test.go
+++ b/compiler_wrapper/clang_tidy_flag_test.go
@@ -273,6 +273,66 @@
})
}
+func TestTriciumClangTidyIsProperlyDetectedFromEnv(t *testing.T) {
+ withClangTidyTestContext(t, func(ctx *testContext) {
+ ctx.env = []string{"WITH_TIDY=tricium"}
+ ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
+ switch ctx.cmdCount {
+ case 1:
+ if err := verifyPath(cmd, "usr/bin/clang"); err != nil {
+ t.Error(err)
+ }
+ return nil
+ case 2:
+ if err := verifyPath(cmd, "usr/bin/clang-tidy"); err != nil {
+ return err
+ }
+
+ hasFixesFile := false
+ for _, arg := range cmd.Args {
+ if path := strings.TrimPrefix(arg, "--export-fixes="); path != arg {
+ hasFixesFile = true
+ if !strings.HasPrefix(path, ctx.cfg.triciumNitsDir+"/") {
+ t.Errorf("fixes file was %q; expected it to be in %q", path, ctx.cfg.triciumNitsDir)
+ }
+ break
+ }
+ }
+
+ if !hasFixesFile {
+ t.Error("no fixes file was provided to a tricium invocation")
+ }
+
+ return nil
+ default:
+ return nil
+ }
+ }
+ cmd := ctx.must(callCompiler(ctx, ctx.cfg,
+ ctx.newCommand(clangX86_64, mainCc)))
+ if ctx.cmdCount != 3 {
+ t.Errorf("expected 3 calls. Got: %d", ctx.cmdCount)
+ }
+ if err := verifyPath(cmd, "usr/bin/clang"); err != nil {
+ t.Error(err)
+ }
+ })
+}
+
+func TestTriciumClangTidySkipsProtobufFiles(t *testing.T) {
+ withClangTidyTestContext(t, func(ctx *testContext) {
+ ctx.env = []string{"WITH_TIDY=tricium"}
+ cmd := ctx.must(callCompiler(ctx, ctx.cfg,
+ ctx.newCommand(clangX86_64, mainCc+".pb.cc")))
+ if ctx.cmdCount != 1 {
+ t.Errorf("expected tricium clang-tidy to not execute on a protobuf file")
+ }
+ if err := verifyPath(cmd, "usr/bin/clang"); err != nil {
+ t.Error(err)
+ }
+ })
+}
+
func withClangTidyTestContext(t *testing.T, work func(ctx *testContext)) {
withTestContext(t, func(ctx *testContext) {
ctx.env = []string{"WITH_TIDY=1"}
diff --git a/compiler_wrapper/compiler_wrapper.go b/compiler_wrapper/compiler_wrapper.go
index 4c3db14..14de210 100644
--- a/compiler_wrapper/compiler_wrapper.go
+++ b/compiler_wrapper/compiler_wrapper.go
@@ -90,16 +90,29 @@
return 0, newErrorwithSourceLocf("unsupported compiler: %s", mainBuilder.target.compiler)
}
} else if mainBuilder.target.compilerType == clangType {
- cSrcFile, useClangTidy := processClangTidyFlags(mainBuilder)
+ cSrcFile, tidyMode := processClangTidyFlags(mainBuilder)
err := prepareClangCommand(mainBuilder)
if err != nil {
return 0, err
}
allowCCache := true
- if useClangTidy {
+ if tidyMode != tidyModeNone {
allowCCache = false
clangCmdWithoutGomaAndCCache := mainBuilder.build()
- if err := runClangTidy(env, clangCmdWithoutGomaAndCCache, cSrcFile); err != nil {
+ var err error
+ switch tidyMode {
+ case tidyModeTricium:
+ if cfg.triciumNitsDir == "" {
+ return 0, newErrorwithSourceLocf("tricium linting was requested, but no nits directory is configured")
+ }
+ err = runClangTidyForTricium(env, clangCmdWithoutGomaAndCCache, cSrcFile, cfg.triciumNitsDir)
+ case tidyModeAll:
+ err = runClangTidy(env, clangCmdWithoutGomaAndCCache, cSrcFile)
+ default:
+ panic(fmt.Sprintf("Unknown tidy mode: %v", tidyMode))
+ }
+
+ if err != nil {
return 0, err
}
}
diff --git a/compiler_wrapper/config.go b/compiler_wrapper/config.go
index a122b5a..3c3668d 100644
--- a/compiler_wrapper/config.go
+++ b/compiler_wrapper/config.go
@@ -29,6 +29,8 @@
rootRelPath string
// Directory to store errors that were prevented with -Wno-error.
newWarningsDir string
+ // Directory to store nits in when using `WITH_TIDY=tricium`.
+ triciumNitsDir string
// Version. Only used for printing via -print-cmd.
version string
}
@@ -137,6 +139,7 @@
"-Wno-implicit-int-float-conversion",
},
newWarningsDir: "/tmp/fatal_clang_warnings",
+ triciumNitsDir: "/tmp/linting_output/clang-tidy",
}
// Flags to be added to non-hardened toolchain.
@@ -166,6 +169,7 @@
"-Wno-implicit-int-float-conversion",
},
newWarningsDir: "/tmp/fatal_clang_warnings",
+ triciumNitsDir: "/tmp/linting_output/clang-tidy",
}
// Flags to be added to host toolchain.
@@ -200,6 +204,7 @@
"-Wno-implicit-int-float-conversion",
},
newWarningsDir: "/tmp/fatal_clang_warnings",
+ triciumNitsDir: "/tmp/linting_output/clang-tidy",
}
var androidConfig = &config{
@@ -211,4 +216,5 @@
clangFlags: []string{},
clangPostFlags: []string{},
newWarningsDir: "",
+ triciumNitsDir: "",
}
diff --git a/compiler_wrapper/testutil_test.go b/compiler_wrapper/testutil_test.go
index 5d8ef92..d23a843 100644
--- a/compiler_wrapper/testutil_test.go
+++ b/compiler_wrapper/testutil_test.go
@@ -141,6 +141,7 @@
func (ctx *testContext) updateConfig(cfg *config) {
*ctx.cfg = *cfg
ctx.cfg.newWarningsDir = filepath.Join(ctx.tempDir, "fatal_clang_warnings")
+ ctx.cfg.triciumNitsDir = filepath.Join(ctx.tempDir, "tricium_nits")
}
func (ctx *testContext) newCommand(path string, args ...string) *command {