...

Source file src/github.com/bazelbuild/buildtools/buildifier/buildifier.go

Documentation: github.com/bazelbuild/buildtools/buildifier

     1  /*
     2  Copyright 2016 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      https://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Buildifier, a tool to parse and format BUILD files.
    18  package main
    19  
    20  import (
    21  	"bytes"
    22  	"flag"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"runtime"
    28  
    29  	"github.com/bazelbuild/buildtools/build"
    30  	"github.com/bazelbuild/buildtools/buildifier/config"
    31  	"github.com/bazelbuild/buildtools/buildifier/utils"
    32  	"github.com/bazelbuild/buildtools/differ"
    33  	"github.com/bazelbuild/buildtools/wspace"
    34  )
    35  
    36  var buildVersion = "redacted"
    37  var buildScmRevision = "redacted"
    38  
    39  func usage() {
    40  	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...]
    41  
    42  Buildifier applies standard formatting to the named Starlark files.  The mode
    43  flag selects the processing: check, diff, fix, or print_if_changed.  In check
    44  mode, buildifier prints a list of files that need reformatting.  In diff mode,
    45  buildifier shows the diffs that it would make.  It creates the diffs by running
    46  a diff command, which can be specified using the -diff_command flag. You can
    47  indicate that the diff command can show differences between more than two files
    48  in the manner of tkdiff by specifying the -multi_diff flag.  In fix mode,
    49  buildifier updates the files that need reformatting and, if the -v flag is
    50  given, prints their names to standard error.  In print_if_changed mode,
    51  buildifier shows the file contents it would write.  The default mode is fix. -d
    52  is an alias for -mode=diff.
    53  
    54  The lint flag selects the lint mode to be used: off, warn, fix.
    55  In off mode, the linting is not performed.
    56  In warn mode, buildifier prints warnings for common mistakes and suboptimal
    57  coding practices that include links providing more context and fix suggestions.
    58  In fix mode, buildifier updates the files with all warning resolutions produced
    59  by automated fixes.
    60  The default lint mode is off.
    61  
    62  If no files are listed, buildifier reads a Starlark file from standard
    63  input. In fix mode, it writes the reformatted Starlark file to standard output,
    64  even if no changes are necessary.
    65  
    66  Buildifier's reformatting depends in part on the path to the file relative
    67  to the workspace directory. Normally buildifier deduces that path from the
    68  file names given, but the path can be given explicitly with the -path
    69  argument. This is especially useful when reformatting standard input,
    70  or in scripts that reformat a temporary copy of a file.
    71  
    72  Return codes used by buildifier:
    73  
    74    0: success, everything went well
    75    1: syntax errors in input
    76    2: usage errors: invoked incorrectly
    77    3: unexpected runtime errors: file I/O problems or internal bugs
    78    4: check mode failed (reformat is needed)
    79  
    80  Full list of flags with their defaults:
    81  `)
    82  	flag.PrintDefaults()
    83  
    84  	fmt.Fprintf(flag.CommandLine.Output(), `
    85  Buildifier can be also be configured via a JSON file.  The location of the file
    86  is given by the -config flag, the BUILDIFIER_CONFIG environment variable, or
    87  a file named '.buildifier.json' at the root of the workspace (e.g., in the same
    88  directory as the WORKSPACE file).  The PWD environment variable or process
    89  working directory is used to help find the workspace root.  If present, the file
    90  is loaded into memory and becomes the base configuration that command line flags
    91  override.  A sample configuration file can be printed to stdout by running
    92  buildifier -config=example. The config file feature can be disabled completely
    93  with -config=off.
    94  `)
    95  }
    96  
    97  func main() {
    98  	c := config.New()
    99  
   100  	flags := c.FlagSet("buildifier", flag.ExitOnError)
   101  	flag.CommandLine = flags
   102  	flag.Usage = usage
   103  	flags.Parse(os.Args[1:])
   104  	args := flags.Args()
   105  
   106  	if c.Help {
   107  		flag.CommandLine.SetOutput(os.Stdout)
   108  		usage()
   109  		fmt.Println()
   110  		os.Exit(0)
   111  	}
   112  
   113  	if c.Version {
   114  		fmt.Printf("buildifier version: %s \n", buildVersion)
   115  		fmt.Printf("buildifier scm revision: %s \n", buildScmRevision)
   116  		os.Exit(0)
   117  	}
   118  
   119  	if c.ConfigPath == "" {
   120  		c.ConfigPath = config.FindConfigPath("")
   121  	}
   122  	if c.ConfigPath != "" {
   123  		if c.ConfigPath == "example" {
   124  			fmt.Println(config.Example().String())
   125  			os.Exit(0)
   126  		}
   127  		if c.ConfigPath != "off" {
   128  			if err := c.LoadFile(); err != nil {
   129  				fmt.Fprintf(os.Stderr, "buildifier: %s\n", err)
   130  				os.Exit(2)
   131  			}
   132  			// re-parse with new possibly new defaults
   133  			flags = c.FlagSet("buildifier", flag.ExitOnError)
   134  			flag.CommandLine = flags
   135  			flag.Usage = usage
   136  			flags.Parse(os.Args[1:])
   137  		}
   138  	}
   139  
   140  	if err := c.Validate(args); err != nil {
   141  		fmt.Fprintf(os.Stderr, "buildifier: %s\n", err)
   142  		os.Exit(2)
   143  	}
   144  
   145  	// Pass down debug flags into build package
   146  	build.DisableRewrites = c.DisableRewrites
   147  	build.AllowSort = c.AllowSort
   148  
   149  	differ, deprecationWarning := differ.Find()
   150  	if c.DiffCommand != "" {
   151  		differ.Cmd = c.DiffCommand
   152  		differ.MultiDiff = c.MultiDiff
   153  	} else {
   154  		if deprecationWarning && c.Mode == "diff" {
   155  			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")
   156  		}
   157  	}
   158  
   159  	b := buildifier{c, differ}
   160  	exitCode := b.run(args)
   161  
   162  	os.Exit(exitCode)
   163  }
   164  
   165  type buildifier struct {
   166  	config *config.Config
   167  	differ *differ.Differ
   168  }
   169  
   170  func (b *buildifier) run(args []string) int {
   171  	tf := &utils.TempFile{}
   172  	defer tf.Clean()
   173  
   174  	exitCode := 0
   175  	var diagnostics *utils.Diagnostics
   176  	if len(args) == 0 || (len(args) == 1 && (args)[0] == "-") {
   177  		// Read from stdin, write to stdout.
   178  		data, err := ioutil.ReadAll(os.Stdin)
   179  		if err != nil {
   180  			fmt.Fprintf(os.Stderr, "buildifier: reading stdin: %v\n", err)
   181  			return 2
   182  		}
   183  		if b.config.Mode == "fix" {
   184  			b.config.Mode = "pipe"
   185  		}
   186  		var fileDiagnostics *utils.FileDiagnostics
   187  		fileDiagnostics, exitCode = b.processFile("", data, false, tf)
   188  		diagnostics = utils.NewDiagnostics(fileDiagnostics)
   189  	} else {
   190  		files := args
   191  		if b.config.Recursive {
   192  			var err error
   193  			files, err = utils.ExpandDirectories(&args)
   194  			if err != nil {
   195  				fmt.Fprintf(os.Stderr, "buildifier: %v\n", err)
   196  				return 3
   197  			}
   198  		}
   199  		diagnostics, exitCode = b.processFiles(files, tf)
   200  	}
   201  
   202  	diagnosticsOutput := diagnostics.Format(b.config.Format, b.config.Verbose)
   203  	if b.config.Format != "" {
   204  		// Explicitly provided --format means the diagnostics are printed to stdout
   205  		fmt.Print(diagnosticsOutput)
   206  		// Exit code should be set to 0 so that other tools know they can safely parse the json
   207  		exitCode = 0
   208  	} else {
   209  		// --format is not provided, stdout is reserved for file contents
   210  		fmt.Fprint(os.Stderr, diagnosticsOutput)
   211  	}
   212  
   213  	if err := b.differ.Run(); err != nil {
   214  		fmt.Fprintf(os.Stderr, "%v\n", err)
   215  		return 2
   216  	}
   217  
   218  	return exitCode
   219  }
   220  
   221  func (b *buildifier) processFiles(files []string, tf *utils.TempFile) (*utils.Diagnostics, int) {
   222  	// Decide how many file reads to run in parallel.
   223  	// At most 100, and at most one per 10 input files.
   224  	nworker := 100
   225  	if n := (len(files) + 9) / 10; nworker > n {
   226  		nworker = n
   227  	}
   228  	runtime.GOMAXPROCS(nworker + 1)
   229  
   230  	// Start nworker workers reading stripes of the input
   231  	// argument list and sending the resulting data on
   232  	// separate channels. file[k] is read by worker k%nworker
   233  	// and delivered on ch[k%nworker].
   234  	type result struct {
   235  		file string
   236  		data []byte
   237  		err  error
   238  	}
   239  
   240  	ch := make([]chan result, nworker)
   241  	for i := 0; i < nworker; i++ {
   242  		ch[i] = make(chan result, 1)
   243  		go func(i int) {
   244  			for j := i; j < len(files); j += nworker {
   245  				file := files[j]
   246  				data, err := ioutil.ReadFile(file)
   247  				ch[i] <- result{file, data, err}
   248  			}
   249  		}(i)
   250  	}
   251  
   252  	exitCode := 0
   253  	fileDiagnostics := []*utils.FileDiagnostics{}
   254  
   255  	// Process files. The processing still runs in a single goroutine
   256  	// in sequence. Only the reading of the files has been parallelized.
   257  	// The goal is to optimize for runs where most files are already
   258  	// formatted correctly, so that reading is the bulk of the I/O.
   259  	for i, file := range files {
   260  		res := <-ch[i%nworker]
   261  		if res.file != file {
   262  			fmt.Fprintf(os.Stderr, "buildifier: internal phase error: got %s for %s", res.file, file)
   263  			os.Exit(3)
   264  		}
   265  		if res.err != nil {
   266  			fmt.Fprintf(os.Stderr, "buildifier: %v\n", res.err)
   267  			exitCode = 3
   268  			continue
   269  		}
   270  		fd, newExitCode := b.processFile(file, res.data, len(files) > 1, tf)
   271  		if fd != nil {
   272  			fileDiagnostics = append(fileDiagnostics, fd)
   273  		}
   274  		if newExitCode != 0 {
   275  			exitCode = newExitCode
   276  		}
   277  	}
   278  	return utils.NewDiagnostics(fileDiagnostics...), exitCode
   279  }
   280  
   281  // processFile processes a single file containing data.
   282  // It has been read from filename and should be written back if fixing.
   283  func (b *buildifier) processFile(filename string, data []byte, displayFileNames bool, tf *utils.TempFile) (*utils.FileDiagnostics, int) {
   284  	var exitCode int
   285  
   286  	displayFilename := filename
   287  	if b.config.WorkspaceRelativePath != "" {
   288  		displayFilename = b.config.WorkspaceRelativePath
   289  	}
   290  
   291  	parser := utils.GetParser(b.config.InputType)
   292  
   293  	f, err := parser(displayFilename, data)
   294  	if err != nil {
   295  		// Do not use buildifier: prefix on this error.
   296  		// Since it is a parse error, it begins with file:line:
   297  		// and we want that to be the first thing in the error.
   298  		fmt.Fprintf(os.Stderr, "%v\n", err)
   299  		if exitCode < 1 {
   300  			exitCode = 1
   301  		}
   302  		return utils.InvalidFileDiagnostics(displayFilename), exitCode
   303  	}
   304  
   305  	if absoluteFilename, err := filepath.Abs(displayFilename); err == nil {
   306  		f.WorkspaceRoot, f.Pkg, f.Label = wspace.SplitFilePath(absoluteFilename)
   307  	}
   308  
   309  	warnings := utils.Lint(f, b.config.Lint, &b.config.LintWarnings, b.config.Verbose)
   310  	if len(warnings) > 0 {
   311  		exitCode = 4
   312  	}
   313  	fileDiagnostics := utils.NewFileDiagnostics(f.DisplayPath(), warnings)
   314  
   315  	ndata := build.Format(f)
   316  
   317  	switch b.config.Mode {
   318  	case "check":
   319  		// check mode: print names of files that need formatting.
   320  		if !bytes.Equal(data, ndata) {
   321  			fileDiagnostics.Formatted = false
   322  			return fileDiagnostics, 4
   323  		}
   324  
   325  	case "diff":
   326  		// diff mode: run diff on old and new.
   327  		if bytes.Equal(data, ndata) {
   328  			return fileDiagnostics, exitCode
   329  		}
   330  		outfile, err := tf.WriteTemp(ndata)
   331  		if err != nil {
   332  			fmt.Fprintf(os.Stderr, "buildifier: %v\n", err)
   333  			return fileDiagnostics, 3
   334  		}
   335  		infile := filename
   336  		if filename == "" {
   337  			// data was read from standard filename.
   338  			// Write it to a temporary file so diff can read it.
   339  			infile, err = tf.WriteTemp(data)
   340  			if err != nil {
   341  				fmt.Fprintf(os.Stderr, "buildifier: %v\n", err)
   342  				return fileDiagnostics, 3
   343  			}
   344  		}
   345  		if displayFileNames {
   346  			fmt.Fprintf(os.Stderr, "%v:\n", f.DisplayPath())
   347  		}
   348  		if err := b.differ.Show(infile, outfile); err != nil {
   349  			fmt.Fprintf(os.Stderr, "%v\n", err)
   350  			return fileDiagnostics, 4
   351  		}
   352  
   353  	case "pipe":
   354  		// pipe mode - reading from stdin, writing to stdout.
   355  		// ("pipe" is not from the command line; it is set above in main.)
   356  		os.Stdout.Write(ndata)
   357  
   358  	case "fix":
   359  		// fix mode: update files in place as needed.
   360  		if bytes.Equal(data, ndata) {
   361  			return fileDiagnostics, exitCode
   362  		}
   363  
   364  		err := ioutil.WriteFile(filename, ndata, 0666)
   365  		if err != nil {
   366  			fmt.Fprintf(os.Stderr, "buildifier: %s\n", err)
   367  			return fileDiagnostics, 3
   368  		}
   369  
   370  		if b.config.Verbose {
   371  			fmt.Fprintf(os.Stderr, "fixed %s\n", f.DisplayPath())
   372  		}
   373  	case "print_if_changed":
   374  		if bytes.Equal(data, ndata) {
   375  			return fileDiagnostics, exitCode
   376  		}
   377  
   378  		if _, err := os.Stdout.Write(ndata); err != nil {
   379  			fmt.Fprintf(os.Stderr, "buildifier: error writing output: %v\n", err)
   380  			return fileDiagnostics, 3
   381  		}
   382  	}
   383  	return fileDiagnostics, exitCode
   384  }
   385  

View as plain text