blob: 01387fd63bea04d9633d92a9d73e59c5d56b85de [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
)
type useTidyMode int
const clangTidyCrashSubstring = "PLEASE submit a bug report"
const (
tidyModeNone useTidyMode = iota
tidyModeAll
tidyModeTricium
)
func processClangTidyFlags(builder *commandBuilder) (cSrcFile string, clangTidyFlags []string, mode useTidyMode) {
builder.transformArgs(func(arg builderArg) string {
const prefix = "-clang-tidy-flag="
if !strings.HasPrefix(arg.value, prefix) {
return arg.value
}
clangTidyFlags = append(clangTidyFlags, arg.value[len(prefix):])
return ""
})
withTidy, _ := builder.env.getenv("WITH_TIDY")
if withTidy == "" {
return "", clangTidyFlags, tidyModeNone
}
srcFileSuffixes := []string{
".c",
".cc",
".cpp",
".C",
".cxx",
".c++",
}
cSrcFile = ""
srcSuffix := ""
lastArg := ""
for _, arg := range builder.args {
if lastArg != "-o" {
for _, suffix := range srcFileSuffixes {
if strings.HasSuffix(arg.value, suffix) {
srcSuffix = suffix
cSrcFile = arg.value
break
}
}
}
lastArg = arg.value
}
if cSrcFile == "" {
return "", clangTidyFlags, 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, clangTidyFlags, mode
}
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, extraTidyFlags []string, crashArtifactsDir 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=*`.
extraTidyFlags = append(extraTidyFlags, "-checks=*", "--export-fixes="+fixesFilePath)
clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, extraTidyFlags...)
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
}
type crashOutput struct {
CrashReproducerPath string `json:"crash_reproducer_path"`
Stdstreams string `json:"stdstreams"`
}
type metadata struct {
Args []string `json:"args"`
CrashOutput *crashOutput `json:"crash_output"`
Executable string `json:"executable"`
ExitCode int `json:"exit_code"`
LintTarget string `json:"lint_target"`
Stdstreams string `json:"stdstreams"`
Wd string `json:"wd"`
}
meta := &metadata{
Args: clangTidyCmd.Args,
CrashOutput: nil,
Executable: clangTidyCmd.Path,
ExitCode: exitCode,
LintTarget: cSrcFile,
Stdstreams: stdstreams.String(),
Wd: env.getwd(),
}
// Sometimes, clang-tidy crashes. Unfortunately, these don't get funnelled through the
// standard clang crash machinery. :(. Try to work with our own.
if crashArtifactsDir != "" && strings.Contains(meta.Stdstreams, clangTidyCrashSubstring) {
tidyCrashArtifacts := path.Join(crashArtifactsDir, "clang-tidy")
if err := os.MkdirAll(tidyCrashArtifacts, 0777); err != nil {
return fmt.Errorf("creating crash artifacts directory at %q: %v", tidyCrashArtifacts, err)
}
f, err := ioutil.TempFile(tidyCrashArtifacts, "crash-")
if err != nil {
return fmt.Errorf("making tempfile for crash output: %v", err)
}
f.Close()
reproCmd := &command{}
*reproCmd = *clangCmd
reproCmd.Args = append(reproCmd.Args, "-E", "-o", f.Name())
reproOut := &strings.Builder{}
_, err = wrapSubprocessErrorWithSourceLoc(reproCmd, env.run(reproCmd, nil, reproOut, reproOut))
if err != nil {
return fmt.Errorf("attempting to produce a clang-tidy crash reproducer: %v", err)
}
meta.CrashOutput = &crashOutput{
CrashReproducerPath: f.Name(),
Stdstreams: reproOut.String(),
}
}
f, err = os.Create(fixesMetadataPath)
if err != nil {
return fmt.Errorf("creating fixes metadata: %v", err)
}
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, extraTidyFlags []string) error {
extraTidyFlags = append(extraTidyFlags,
"-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-*",
}, ","))
clangTidyCmd, err := calcClangTidyInvocation(env, clangCmd, cSrcFile, extraTidyFlags...)
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
// was invoked with a source file argument.
exitCode, err := wrapSubprocessErrorWithSourceLoc(clangTidyCmd,
env.run(clangTidyCmd, nil, env.stdout(), env.stderr()))
if err == nil && exitCode != 0 {
// Note: We continue on purpose when clang-tidy fails
// to maintain compatibility with the previous wrapper.
fmt.Fprint(env.stderr(), "clang-tidy failed")
}
return err
}
func hasAtLeastOneSuffix(s string, suffixes []string) bool {
for _, suffix := range suffixes {
if strings.HasSuffix(s, suffix) {
return true
}
}
return false
}