| /* |
| Copyright 2016 Google LLC |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| https://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| // Buildifier, a tool to parse and format BUILD files. |
| package main |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "runtime" |
| |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/bazelbuild/buildtools/buildifier/config" |
| "github.com/bazelbuild/buildtools/buildifier/utils" |
| "github.com/bazelbuild/buildtools/differ" |
| "github.com/bazelbuild/buildtools/wspace" |
| ) |
| |
| var buildVersion = "redacted" |
| var buildScmRevision = "redacted" |
| |
| func usage() { |
| fmt.Fprintf(flag.CommandLine.Output(), `usage: buildifier [-d] [-v] [-r] [-config=path.json] [-diff_command=command] [-help] [-multi_diff] [-mode=mode] [-lint=lint_mode] [-path=path] [files...] |
| |
| Buildifier applies standard formatting to the named Starlark files. The mode |
| flag selects the processing: check, diff, fix, or print_if_changed. In check |
| mode, buildifier prints a list of files that need reformatting. In diff mode, |
| buildifier shows the diffs that it would make. It creates the diffs by running |
| a diff command, which can be specified using the -diff_command flag. You can |
| indicate that the diff command can show differences between more than two files |
| in the manner of tkdiff by specifying the -multi_diff flag. In fix mode, |
| buildifier updates the files that need reformatting and, if the -v flag is |
| given, prints their names to standard error. In print_if_changed mode, |
| buildifier shows the file contents it would write. The default mode is fix. -d |
| is an alias for -mode=diff. |
| |
| The lint flag selects the lint mode to be used: off, warn, fix. |
| In off mode, the linting is not performed. |
| In warn mode, buildifier prints warnings for common mistakes and suboptimal |
| coding practices that include links providing more context and fix suggestions. |
| In fix mode, buildifier updates the files with all warning resolutions produced |
| by automated fixes. |
| The default lint mode is off. |
| |
| If no files are listed, buildifier reads a Starlark file from standard |
| input. In fix mode, it writes the reformatted Starlark file to standard output, |
| even if no changes are necessary. |
| |
| Buildifier's reformatting depends in part on the path to the file relative |
| to the workspace directory. Normally buildifier deduces that path from the |
| file names given, but the path can be given explicitly with the -path |
| argument. This is especially useful when reformatting standard input, |
| or in scripts that reformat a temporary copy of a file. |
| |
| Return codes used by buildifier: |
| |
| 0: success, everything went well |
| 1: syntax errors in input |
| 2: usage errors: invoked incorrectly |
| 3: unexpected runtime errors: file I/O problems or internal bugs |
| 4: check mode failed (reformat is needed) |
| |
| Full list of flags with their defaults: |
| `) |
| flag.PrintDefaults() |
| |
| fmt.Fprintf(flag.CommandLine.Output(), ` |
| Buildifier can also be configured via a JSON file. The location of the file |
| is given by the -config flag, the BUILDIFIER_CONFIG environment variable, or |
| a file named '.buildifier.json' at the root of the workspace (e.g., in the same |
| directory as the WORKSPACE file). The PWD environment variable or process |
| working directory is used to help find the workspace root. If present, the file |
| is loaded into memory and becomes the base configuration that command line flags |
| override. A sample configuration file can be printed to stdout by running |
| buildifier -config=example. The config file feature can be disabled completely |
| with -config=off. |
| `) |
| } |
| |
| func main() { |
| c := config.New() |
| |
| flags := c.FlagSet("buildifier", flag.ExitOnError) |
| flag.CommandLine = flags |
| flag.Usage = usage |
| flags.Parse(os.Args[1:]) |
| args := flags.Args() |
| |
| if c.Help { |
| flag.CommandLine.SetOutput(os.Stdout) |
| usage() |
| fmt.Println() |
| os.Exit(0) |
| } |
| |
| if c.Version { |
| fmt.Printf("buildifier version: %s \n", buildVersion) |
| fmt.Printf("buildifier scm revision: %s \n", buildScmRevision) |
| os.Exit(0) |
| } |
| |
| if c.ConfigPath == "" { |
| c.ConfigPath = config.FindConfigPath("") |
| } |
| if c.ConfigPath != "" { |
| if c.ConfigPath == "example" { |
| fmt.Println(config.Example().String()) |
| os.Exit(0) |
| } |
| if c.ConfigPath != "off" { |
| if err := c.LoadFile(); err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) |
| os.Exit(2) |
| } |
| // re-parse with new possibly new defaults |
| flags = c.FlagSet("buildifier", flag.ExitOnError) |
| flag.CommandLine = flags |
| flag.Usage = usage |
| flags.Parse(os.Args[1:]) |
| } |
| } |
| |
| if err := c.Validate(args); err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) |
| os.Exit(2) |
| } |
| |
| // Pass down debug flags into build package |
| build.DisableRewrites = c.DisableRewrites |
| build.AllowSort = c.AllowSort |
| |
| differ, deprecationWarning := differ.Find() |
| if c.DiffCommand != "" { |
| differ.Cmd = c.DiffCommand |
| differ.MultiDiff = c.MultiDiff |
| } else { |
| if deprecationWarning && c.Mode == "diff" { |
| fmt.Fprintf(os.Stderr, "buildifier: selecting diff program with the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables is deprecated, use flags -diff_command and -multi_diff instead\n") |
| } |
| } |
| |
| b := buildifier{c, differ} |
| exitCode := b.run(args) |
| |
| os.Exit(exitCode) |
| } |
| |
| type buildifier struct { |
| config *config.Config |
| differ *differ.Differ |
| } |
| |
| func (b *buildifier) run(args []string) int { |
| tf := &utils.TempFile{} |
| defer tf.Clean() |
| |
| exitCode := 0 |
| var diagnostics *utils.Diagnostics |
| if len(args) == 0 || (len(args) == 1 && (args)[0] == "-") { |
| // Read from stdin, write to stdout. |
| data, err := io.ReadAll(os.Stdin) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: reading stdin: %v\n", err) |
| return 2 |
| } |
| if b.config.Mode == "fix" { |
| b.config.Mode = "pipe" |
| } |
| var fileDiagnostics *utils.FileDiagnostics |
| fileDiagnostics, exitCode = b.processFile("", data, false, tf) |
| diagnostics = utils.NewDiagnostics(fileDiagnostics) |
| } else { |
| files := args |
| if b.config.Recursive { |
| var err error |
| files, err = utils.ExpandDirectories(&args) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) |
| return 3 |
| } |
| } |
| diagnostics, exitCode = b.processFiles(files, tf) |
| } |
| |
| diagnosticsOutput := diagnostics.Format(b.config.Format, b.config.Verbose) |
| if b.config.Format != "" { |
| // Explicitly provided --format means the diagnostics are printed to stdout |
| fmt.Print(diagnosticsOutput) |
| // Exit code should be set to 0 so that other tools know they can safely parse the json |
| exitCode = 0 |
| } else { |
| // --format is not provided, stdout is reserved for file contents |
| fmt.Fprint(os.Stderr, diagnosticsOutput) |
| } |
| |
| if err := b.differ.Run(); err != nil { |
| fmt.Fprintf(os.Stderr, "%v\n", err) |
| return 2 |
| } |
| |
| return exitCode |
| } |
| |
| func (b *buildifier) processFiles(files []string, tf *utils.TempFile) (*utils.Diagnostics, int) { |
| // Decide how many file reads to run in parallel. |
| // At most 100, and at most one per 10 input files. |
| nworker := 100 |
| if n := (len(files) + 9) / 10; nworker > n { |
| nworker = n |
| } |
| runtime.GOMAXPROCS(nworker + 1) |
| |
| // Start nworker workers reading stripes of the input |
| // argument list and sending the resulting data on |
| // separate channels. file[k] is read by worker k%nworker |
| // and delivered on ch[k%nworker]. |
| type result struct { |
| file string |
| data []byte |
| err error |
| } |
| |
| ch := make([]chan result, nworker) |
| for i := 0; i < nworker; i++ { |
| ch[i] = make(chan result, 1) |
| go func(i int) { |
| for j := i; j < len(files); j += nworker { |
| file := files[j] |
| data, err := os.ReadFile(file) |
| ch[i] <- result{file, data, err} |
| } |
| }(i) |
| } |
| |
| exitCode := 0 |
| fileDiagnostics := []*utils.FileDiagnostics{} |
| |
| // Process files. The processing still runs in a single goroutine |
| // in sequence. Only the reading of the files has been parallelized. |
| // The goal is to optimize for runs where most files are already |
| // formatted correctly, so that reading is the bulk of the I/O. |
| for i, file := range files { |
| res := <-ch[i%nworker] |
| if res.file != file { |
| fmt.Fprintf(os.Stderr, "buildifier: internal phase error: got %s for %s", res.file, file) |
| os.Exit(3) |
| } |
| if res.err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %v\n", res.err) |
| exitCode = 3 |
| continue |
| } |
| fd, newExitCode := b.processFile(file, res.data, len(files) > 1, tf) |
| if fd != nil { |
| fileDiagnostics = append(fileDiagnostics, fd) |
| } |
| if newExitCode != 0 { |
| exitCode = newExitCode |
| } |
| } |
| return utils.NewDiagnostics(fileDiagnostics...), exitCode |
| } |
| |
| // processFile processes a single file containing data. |
| // It has been read from filename and should be written back if fixing. |
| func (b *buildifier) processFile(filename string, data []byte, displayFileNames bool, tf *utils.TempFile) (*utils.FileDiagnostics, int) { |
| var exitCode int |
| |
| displayFilename := filename |
| if b.config.WorkspaceRelativePath != "" { |
| displayFilename = b.config.WorkspaceRelativePath |
| } |
| |
| parser := utils.GetParser(b.config.InputType) |
| |
| f, err := parser(displayFilename, data) |
| if err != nil { |
| // Do not use buildifier: prefix on this error. |
| // Since it is a parse error, it begins with file:line: |
| // and we want that to be the first thing in the error. |
| fmt.Fprintf(os.Stderr, "%v\n", err) |
| if exitCode < 1 { |
| exitCode = 1 |
| } |
| return utils.InvalidFileDiagnostics(displayFilename), exitCode |
| } |
| |
| if absoluteFilename, err := filepath.Abs(displayFilename); err == nil { |
| f.WorkspaceRoot, f.Pkg, f.Label = wspace.SplitFilePath(absoluteFilename) |
| } |
| |
| warnings := utils.Lint(f, b.config.Lint, &b.config.LintWarnings, b.config.Verbose) |
| if len(warnings) > 0 { |
| exitCode = 4 |
| } |
| fileDiagnostics := utils.NewFileDiagnostics(f.DisplayPath(), warnings) |
| |
| ndata := build.Format(f) |
| |
| switch b.config.Mode { |
| case "check": |
| // check mode: print names of files that need formatting. |
| if !bytes.Equal(data, ndata) { |
| fileDiagnostics.Formatted = false |
| return fileDiagnostics, 4 |
| } |
| |
| case "diff": |
| // diff mode: run diff on old and new. |
| if bytes.Equal(data, ndata) { |
| return fileDiagnostics, exitCode |
| } |
| outfile, err := tf.WriteTemp(ndata) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) |
| return fileDiagnostics, 3 |
| } |
| infile := filename |
| if filename == "" { |
| // data was read from standard filename. |
| // Write it to a temporary file so diff can read it. |
| infile, err = tf.WriteTemp(data) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) |
| return fileDiagnostics, 3 |
| } |
| } |
| if displayFileNames { |
| fmt.Fprintf(os.Stderr, "%v:\n", f.DisplayPath()) |
| } |
| if err := b.differ.Show(infile, outfile); err != nil { |
| fmt.Fprintf(os.Stderr, "%v\n", err) |
| return fileDiagnostics, 4 |
| } |
| |
| case "pipe": |
| // pipe mode - reading from stdin, writing to stdout. |
| // ("pipe" is not from the command line; it is set above in main.) |
| os.Stdout.Write(ndata) |
| |
| case "fix": |
| // fix mode: update files in place as needed. |
| if bytes.Equal(data, ndata) { |
| return fileDiagnostics, exitCode |
| } |
| |
| err := os.WriteFile(filename, ndata, 0666) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) |
| return fileDiagnostics, 3 |
| } |
| |
| if b.config.Verbose { |
| fmt.Fprintf(os.Stderr, "fixed %s\n", f.DisplayPath()) |
| } |
| case "print_if_changed": |
| if bytes.Equal(data, ndata) { |
| return fileDiagnostics, exitCode |
| } |
| |
| if _, err := os.Stdout.Write(ndata); err != nil { |
| fmt.Fprintf(os.Stderr, "buildifier: error writing output: %v\n", err) |
| return fileDiagnostics, 3 |
| } |
| } |
| return fileDiagnostics, exitCode |
| } |