| // Copyright 2021 The Fuchsia 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 ( |
| "context" |
| "encoding/json" |
| "errors" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "os/signal" |
| "syscall" |
| |
| "go.fuchsia.dev/fuchsia/tools/build" |
| "go.fuchsia.dev/fuchsia/tools/lib/color" |
| "go.fuchsia.dev/fuchsia/tools/lib/jsonutil" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| "go.fuchsia.dev/fuchsia/tools/lib/streams" |
| "go.fuchsia.dev/fuchsia/tools/staticanalysis" |
| "go.fuchsia.dev/fuchsia/tools/staticanalysis/analyzers/clippy" |
| "go.fuchsia.dev/fuchsia/tools/staticanalysis/analyzers/codelinks" |
| ) |
| |
| // fileToAnalyze is the expected schema of each element in the file specified by |
| // `-files-json`, indicating the files to analyze. |
| type fileToAnalyze struct { |
| // Path is the file's path relative to the checkout root. |
| Path string `json:"path"` |
| } |
| |
| func getAnalyzers(checkoutDir, buildDir string) ([]staticanalysis.Analyzer, error) { |
| modules, err := build.NewModules(buildDir) |
| if err != nil { |
| return nil, err |
| } |
| |
| var res []staticanalysis.Analyzer |
| |
| clippy, err := clippy.New(checkoutDir, modules) |
| if err != nil { |
| return nil, err |
| } |
| res = append(res, clippy) |
| |
| res = append(res, codelinks.New(checkoutDir)) |
| |
| return res, nil |
| } |
| |
| func runAnalyzers(ctx context.Context, analyzers []staticanalysis.Analyzer, files []string) ([]*staticanalysis.Finding, error) { |
| // Empty slice rather than nil so it gets marshaled to an empty JSON array |
| // instead of null. |
| allFindings := []*staticanalysis.Finding{} |
| |
| var errs []error |
| |
| for _, analyzer := range analyzers { |
| for _, f := range files { |
| findings, err := analyzer.Analyze(ctx, f) |
| if err != nil { |
| logger.Errorf(ctx, err.Error()) |
| errs = append(errs, err) |
| // It's possible for an analyzer to produce findings *and* an |
| // error in which case we still want to emit the findings (but |
| // produce a non-zero retcode). |
| } |
| for _, finding := range findings { |
| if err := finding.Normalize(); err != nil { |
| logger.Errorf(ctx, err.Error()) |
| errs = append(errs, err) |
| } |
| } |
| allFindings = append(allFindings, findings...) |
| } |
| } |
| |
| if len(errs) > 0 { |
| return allFindings, errs[0] |
| } |
| return allFindings, nil |
| } |
| |
| func mainImpl(ctx context.Context) error { |
| ctx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) |
| defer cancel() |
| |
| var flags struct { |
| checkoutDir string |
| buildDir string |
| filesJSON string |
| outputJSON string |
| } |
| |
| flag.StringVar(&flags.buildDir, "build-dir", "", "Fuchsia build directory.") |
| flag.StringVar(&flags.checkoutDir, "checkout-dir", "", "Fuchsia checkout directory.") |
| flag.StringVar(&flags.filesJSON, "files-json", "", "JSON manifest of files to analyze. If unset, reads from positional arguments.") |
| flag.StringVar(&flags.outputJSON, "output-json", "", "File to write findings to. If unspecified, findings will be written to stdout.") |
| flag.Parse() |
| |
| if flags.buildDir == "" { |
| return errors.New("-build-dir is required") |
| } |
| if flags.checkoutDir == "" { |
| return errors.New("-checkout-dir is required") |
| } |
| |
| var files []string |
| if flags.filesJSON != "" { |
| var filesToAnalyze []fileToAnalyze |
| if err := jsonutil.ReadFromFile(flags.filesJSON, &filesToAnalyze); err != nil { |
| return fmt.Errorf("failed to read -files-json: %w", err) |
| } |
| if flag.NArg() > 0 { |
| return errors.New("positional arguments not accepted when -files-json is specified") |
| } |
| for _, f := range filesToAnalyze { |
| files = append(files, f.Path) |
| } |
| } else if flag.NArg() > 0 { |
| files = flag.Args() |
| } else { |
| logger.Infof(ctx, "No files specified. Either use -files-json or pass paths as positional arguments.") |
| return nil |
| } |
| |
| analyzers, err := getAnalyzers(flags.checkoutDir, flags.buildDir) |
| if err != nil { |
| return err |
| } |
| |
| findings, finalErr := runAnalyzers(ctx, analyzers, files) |
| // Attempt to report findings even if some analyzers failed, since others |
| // might have passed. |
| if err := writeFindings(ctx, findings, flags.outputJSON); err != nil { |
| if finalErr == nil { |
| finalErr = err |
| } |
| } |
| return finalErr |
| } |
| |
| func writeFindings(ctx context.Context, findings []*staticanalysis.Finding, outputJSON string) error { |
| output := streams.Stdout(ctx) |
| if outputJSON != "" { |
| f, err := os.Create(outputJSON) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| output = f |
| } |
| enc := json.NewEncoder(output) |
| enc.SetIndent("", " ") |
| if err := enc.Encode(findings); err != nil { |
| return fmt.Errorf("failed to write findings: %w", err) |
| } |
| return nil |
| } |
| |
| func main() { |
| const logFlags = log.Ltime | log.Lmicroseconds | log.Lshortfile |
| log := logger.NewLogger(logger.DebugLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "staticanalysis ") |
| log.SetFlags(logFlags) |
| ctx := logger.WithLogger(context.Background(), log) |
| |
| if err := mainImpl(ctx); err != nil { |
| logger.Fatalf(ctx, err.Error()) |
| os.Exit(1) |
| } |
| } |