| /* |
| 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" |
| "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) |
| } |
| |
| // findTablesPath locates the specified table file starting from the process's |
| // current working directory. It searches upward through the directory tree |
| // until the file is found or the root of the workspace is reached. |
| func findTablesPath(file string) (string, error) { |
| if wspace.IsRegularFile(file) { |
| return file, nil |
| } |
| rootDir, _ := os.Getwd() |
| dirname, err := wspace.Find( |
| rootDir, |
| map[string]func(os.FileInfo) bool{ |
| file: func(fi os.FileInfo) bool { |
| return fi.Mode()&os.ModeType == 0 |
| }, |
| }, |
| ) |
| if err != nil { |
| return file, err |
| } |
| return filepath.Join(dirname, file), nil |
| } |
| |
| // 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 := io.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 != "" { |
| foundTablesPath, err := findTablesPath(c.TablesPath) |
| if err != nil { |
| return fmt.Errorf("failed to find %s for -tables: %w", c.TablesPath, err) |
| } |
| if err := tables.ParseAndUpdateJSONDefinitions(foundTablesPath, false); err != nil { |
| return fmt.Errorf("failed to parse %s for -tables: %w", foundTablesPath, err) |
| } |
| } |
| |
| if c.AddTablesPath != "" { |
| foundTablesPath, err := findTablesPath(c.AddTablesPath) |
| if err != nil { |
| return fmt.Errorf("failed to find %s for -add_tables: %w", c.AddTablesPath, err) |
| } |
| if err := tables.ParseAndUpdateJSONDefinitions(foundTablesPath, true); err != nil { |
| return fmt.Errorf("failed to parse %s for -add_tables: %w", foundTablesPath, 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 |
| } |