/* Copyright 2022 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. */ // Package config provides configuration objects for buildifier package config import ( "encoding/json" "flag" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "strings" "github.com/bazelbuild/buildtools/tables" "github.com/bazelbuild/buildtools/warn" "github.com/bazelbuild/buildtools/wspace" ) const buildifierJSONFilename = ".buildifier.json" // New constructs a Config with default values. func New() *Config { return &Config{ InputType: "auto", } } // FindConfigPath locates the nearest buildifier configuration file. First // tries the value of the BUILDIFIER_CONFIG environment variable. If no // environment variable is defined, The configuration file will be resolved // starting from the process cwd and searching up the file tree until a config // file is (or isn't) found. func FindConfigPath(rootDir string) string { if filename, ok := os.LookupEnv("BUILDIFIER_CONFIG"); ok { return filename } if rootDir == "" { rootDir, _ = os.Getwd() // best-effort, ignore error } dirname, err := wspace.Find( rootDir, map[string]func(os.FileInfo) bool{ buildifierJSONFilename: func(fi os.FileInfo) bool { return fi.Mode()&os.ModeType == 0 }, }, ) if err != nil { return "" } return filepath.Join(dirname, buildifierJSONFilename) } // Config is used to configure buildifier type Config struct { // InputType determines the input file type: build (for BUILD files), bzl // (for .bzl files), workspace (for WORKSPACE files), default (for generic // Starlark files), module (for MODULE.bazel files) // or auto (default, based on the filename) InputType string `json:"type,omitempty"` // Format sets the diagnostics format: text or json (default text) Format string `json:"format,omitempty"` // Mode determines the formatting mode: check, diff, or fix (default fix) Mode string `json:"mode,omitempty"` // DiffMode is an alias for DiffMode bool `json:"diffMode,omitempty"` // Lint determines the lint mode: off, warn, or fix (default off) Lint string `json:"lint,omitempty"` // Warnings is a comma-separated list of warning identifiers used in the lint mode or "all" Warnings string `json:"warnings,omitempty"` // WarningsList is a list of warnings (alternative to comma-separated warnings string) WarningsList []string `json:"warningsList,omitempty"` // Recursive instructs buildifier to find starlark files recursively Recursive bool `json:"recursive,omitempty"` // Verbose instructs buildifier to output verbose diagnostics Verbose bool `json:"verbose,omitempty"` // DiffCommand is the command to run when the formatting mode is diff // (default uses the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY // environment variables to create the diff command) DiffCommand string `json:"diffCommand,omitempty"` // MultiDiff means the command specified by the -diff_command flag can diff // multiple files in the style of tkdiff (default false) MultiDiff bool `json:"multiDiff,omitempty"` // TablesPath is the path to JSON file with custom table definitions that // will replace the built-in tables TablesPath string `json:"tables,omitempty"` // AddTablesPath path to JSON file with custom table definitions which will be merged with the built-in tables AddTablesPath string `json:"addTables,omitempty"` // WorkspaceRelativePath - assume BUILD file has this path relative to the workspace directory WorkspaceRelativePath string `json:"path,omitempty"` // DisableRewrites configures the list of buildifier rewrites to disable DisableRewrites ArrayFlags `json:"buildifier_disable,omitempty"` // AllowSort specifies additional sort contexts to treat as safe AllowSort ArrayFlags `json:"allowsort,omitempty"` // Help is true if the -h flag is set Help bool `json:"-"` // Version is true if the -v flag is set Version bool `json:"-"` // ConfigPath is the path to this config ConfigPath string `json:"-"` // LintWarnings is the final validated list of Lint/Fix warnings LintWarnings []string `json:"-"` } // LoadFile unmarshals JSON file from the ConfigPath field. func (c *Config) LoadFile() error { file, err := os.Open(c.ConfigPath) if err != nil { return err } defer file.Close() return c.LoadReader(file) } // LoadReader unmarshals JSON data from the given reader. func (c *Config) LoadReader(in io.Reader) error { data, err := ioutil.ReadAll(in) if err != nil { return fmt.Errorf("reading config: %w", err) } if err := json.Unmarshal(data, c); err != nil { return err } return nil } // FlagSet returns a flag.FlagSet that can be used to override the config. func (c *Config) FlagSet(name string, errorHandling flag.ErrorHandling) *flag.FlagSet { flags := flag.NewFlagSet(name, errorHandling) flags.BoolVar(&c.Help, "help", false, "print usage information") flags.BoolVar(&c.Version, "version", false, "print the version of buildifier") flags.BoolVar(&c.Verbose, "v", c.Verbose, "print verbose information to standard error") flags.BoolVar(&c.DiffMode, "d", c.DiffMode, "alias for -mode=diff") flags.BoolVar(&c.Recursive, "r", c.Recursive, "find starlark files recursively") flags.BoolVar(&c.MultiDiff, "multi_diff", c.MultiDiff, "the command specified by the -diff_command flag can diff multiple files in the style of tkdiff (default false)") flags.StringVar(&c.Mode, "mode", c.Mode, "formatting mode: check, diff, or fix (default fix)") flags.StringVar(&c.Format, "format", c.Format, "diagnostics format: text or json (default text)") flags.StringVar(&c.DiffCommand, "diff_command", c.DiffCommand, "command to run when the formatting mode is diff (default uses the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables to create the diff command)") flags.StringVar(&c.Lint, "lint", c.Lint, "lint mode: off, warn, or fix (default off)") flags.StringVar(&c.Warnings, "warnings", c.Warnings, "comma-separated warnings used in the lint mode or \"all\"") flags.StringVar(&c.WorkspaceRelativePath, "path", c.WorkspaceRelativePath, "assume BUILD file has this path relative to the workspace directory") flags.StringVar(&c.TablesPath, "tables", c.TablesPath, "path to JSON file with custom table definitions which will replace the built-in tables") flags.StringVar(&c.AddTablesPath, "add_tables", c.AddTablesPath, "path to JSON file with custom table definitions which will be merged with the built-in tables") flags.StringVar(&c.InputType, "type", c.InputType, "Input file type: build (for BUILD files), bzl (for .bzl files), workspace (for WORKSPACE files), module (for MODULE.bazel files), default (for generic Starlark files) or auto (default, based on the filename)") flags.StringVar(&c.ConfigPath, "config", "", "path to .buildifier.json config file") flags.Var(&c.AllowSort, "allowsort", "additional sort contexts to treat as safe") flags.Var(&c.DisableRewrites, "buildifier_disable", "list of buildifier rewrites to disable") return flags } // Validate checks that the input type, format, and lint modes are correctly // set. It computes the final set of warnings used for linting. The tables // package is configured as a side-effect. func (c *Config) Validate(args []string) error { if err := ValidateInputType(&c.InputType); err != nil { return err } if err := ValidateFormat(&c.Format, &c.Mode); err != nil { return err } if err := ValidateModes(&c.Mode, &c.Lint, &c.DiffMode); err != nil { return err } // If the path flag is set, must only be formatting a single file. // It doesn't make sense for multiple files to have the same path. if (c.WorkspaceRelativePath != "" || c.Mode == "print_if_changed") && len(args) > 1 { return fmt.Errorf("can only format one file when using -path flag or -mode=print_if_changed") } if c.TablesPath != "" { if err := tables.ParseAndUpdateJSONDefinitions(c.TablesPath, false); err != nil { return fmt.Errorf("failed to parse %s for -tables: %w", c.TablesPath, err) } } if c.AddTablesPath != "" { if err := tables.ParseAndUpdateJSONDefinitions(c.AddTablesPath, true); err != nil { return fmt.Errorf("failed to parse %s for -add_tables: %w", c.AddTablesPath, err) } } warningsList := c.WarningsList if c.Warnings != "" { warningsList = append(warningsList, c.Warnings) } warnings := strings.Join(warningsList, ",") lintWarnings, err := ValidateWarnings(&warnings, &warn.AllWarnings, &warn.DefaultWarnings) if err != nil { return err // TODO(pcj) return nil? } c.LintWarnings = lintWarnings return nil } // String renders the config as a formatted JSON string and satisfies the // Stringer interface. func (c *Config) String() string { data, err := json.MarshalIndent(c, "", " ") if err != nil { log.Panicf("config marshal json: %v", err) } return string(data) } // ArrayFlags is a string slice that satisfies the flag.Value interface type ArrayFlags []string // String implements part of the flag.Value interface func (i *ArrayFlags) String() string { return strings.Join(*i, ",") } // Set implements part of the flag.Value interface func (i *ArrayFlags) Set(value string) error { *i = append(*i, value) return nil } // Example creates a sample configuration file for the -config=example flag. func Example() *Config { c := New() c.InputType = "auto" c.Mode = "fix" c.Lint = "fix" c.WarningsList = warn.AllWarnings return c }