| // 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 |
| } |