-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
`regal fix` is a new command that is capable of fixing some linter errors: - opa-fmt - use-rego-v1 - no-whitespace-comment - use-assignment-operator Fixes are applied when the input files fail with linter errors. User config is supported. The same fix logic is also used from the language server for code actions.
- Loading branch information
1 parent
8c9b76b
commit dfd9ee2
Showing
22 changed files
with
1,631 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
package cmd | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/fatih/color" | ||
"github.com/spf13/cobra" | ||
"gopkg.in/yaml.v3" | ||
|
||
rio "github.com/styrainc/regal/internal/io" | ||
"github.com/styrainc/regal/pkg/config" | ||
"github.com/styrainc/regal/pkg/fixer" | ||
"github.com/styrainc/regal/pkg/fixer/fileprovider" | ||
"github.com/styrainc/regal/pkg/fixer/fixes" | ||
"github.com/styrainc/regal/pkg/linter" | ||
) | ||
|
||
// fixCommandParams is similar to the lint params, but with some fields such as profiling removed. | ||
// It is intended that it is compatible with the same command line flags as the lint command to | ||
// control the behavior of lint rules used. | ||
type fixCommandParams struct { | ||
configFile string | ||
debug bool | ||
disable repeatedStringFlag | ||
disableAll bool | ||
disableCategory repeatedStringFlag | ||
enable repeatedStringFlag | ||
enableAll bool | ||
enableCategory repeatedStringFlag | ||
format string | ||
ignoreFiles repeatedStringFlag | ||
noColor bool | ||
outputFile string | ||
rules repeatedStringFlag | ||
timeout time.Duration | ||
} | ||
|
||
func (p *fixCommandParams) getConfigFile() string { | ||
return p.configFile | ||
} | ||
|
||
func (p *fixCommandParams) getTimeout() time.Duration { | ||
return p.timeout | ||
} | ||
|
||
func init() { | ||
params := &fixCommandParams{} | ||
|
||
fixCommand := &cobra.Command{ | ||
Use: "fix <path> [path [...]]", | ||
Short: "Fix Rego source files", | ||
Long: `Fix Rego source files with linter violations.`, | ||
|
||
PreRunE: func(_ *cobra.Command, args []string) error { | ||
if len(args) == 0 { | ||
return errors.New("at least one file or directory must be provided for fixing") | ||
} | ||
|
||
return nil | ||
}, | ||
|
||
RunE: wrapProfiling(func(args []string) error { | ||
err := fix(args, params) | ||
if err != nil { | ||
log.SetOutput(os.Stderr) | ||
log.Println(err) | ||
|
||
return exit(1) | ||
} | ||
|
||
return nil | ||
}), | ||
} | ||
|
||
fixCommand.Flags().StringVarP(¶ms.configFile, "config-file", "c", "", | ||
"set path of configuration file") | ||
fixCommand.Flags().StringVarP(¶ms.format, "format", "f", formatPretty, | ||
"set output format (pretty is the only supported format)") | ||
fixCommand.Flags().StringVarP(¶ms.outputFile, "output-file", "o", "", | ||
"set file to use for fixing output, defaults to stdout") | ||
fixCommand.Flags().BoolVar(¶ms.noColor, "no-color", false, | ||
"Disable color output") | ||
fixCommand.Flags().VarP(¶ms.rules, "rules", "r", | ||
"set custom rules file(s). This flag can be repeated.") | ||
fixCommand.Flags().DurationVar(¶ms.timeout, "timeout", 0, | ||
"set timeout for fixing (default unlimited)") | ||
fixCommand.Flags().BoolVar(¶ms.debug, "debug", false, | ||
"enable debug logging (including print output from custom policy)") | ||
|
||
fixCommand.Flags().VarP(¶ms.disable, "disable", "d", | ||
"disable specific rule(s). This flag can be repeated.") | ||
fixCommand.Flags().BoolVarP(¶ms.disableAll, "disable-all", "D", false, | ||
"disable all rules") | ||
fixCommand.Flags().VarP(¶ms.disableCategory, "disable-category", "", | ||
"disable all rules in a category. This flag can be repeated.") | ||
|
||
fixCommand.Flags().VarP(¶ms.enable, "enable", "e", | ||
"enable specific rule(s). This flag can be repeated.") | ||
fixCommand.Flags().BoolVarP(¶ms.enableAll, "enable-all", "E", false, | ||
"enable all rules") | ||
fixCommand.Flags().VarP(¶ms.enableCategory, "enable-category", "", | ||
"enable all rules in a category. This flag can be repeated.") | ||
|
||
fixCommand.Flags().VarP(¶ms.ignoreFiles, "ignore-files", "", | ||
"ignore all files matching a glob-pattern. This flag can be repeated.") | ||
|
||
addPprofFlag(fixCommand.Flags()) | ||
|
||
RootCommand.AddCommand(fixCommand) | ||
} | ||
|
||
func fix(args []string, params *fixCommandParams) error { | ||
var err error | ||
|
||
ctx, cancel := getLinterContext(params) | ||
defer cancel() | ||
|
||
if params.noColor { | ||
color.NoColor = true | ||
} | ||
|
||
// if an outputFile has been set, open it for writing or create it | ||
var outputWriter io.Writer | ||
|
||
outputWriter = os.Stdout | ||
if params.outputFile != "" { | ||
outputWriter, err = getWriterForOutputFile(params.outputFile) | ||
if err != nil { | ||
return fmt.Errorf("failed to open output file before use %w", err) | ||
} | ||
} | ||
|
||
var regalDir *os.File | ||
|
||
var customRulesDir string | ||
|
||
var configSearchPath string | ||
|
||
cwd, _ := os.Getwd() | ||
|
||
if len(args) == 1 { | ||
configSearchPath = args[0] | ||
if !strings.HasPrefix(args[0], "/") { | ||
configSearchPath = filepath.Join(cwd, args[0]) | ||
} | ||
} else { | ||
configSearchPath, _ = os.Getwd() | ||
} | ||
|
||
if configSearchPath == "" { | ||
log.Println("failed to determine relevant directory for config file search - won't search for custom config or rules") | ||
} else { | ||
regalDir, err = config.FindRegalDirectory(configSearchPath) | ||
if err == nil { | ||
customRulesPath := filepath.Join(regalDir.Name(), rio.PathSeparator, "rules") | ||
if _, err = os.Stat(customRulesPath); err == nil { | ||
customRulesDir = customRulesPath | ||
} | ||
} | ||
} | ||
|
||
l := linter.NewLinter(). | ||
WithDisableAll(params.disableAll). | ||
WithDisabledCategories(params.disableCategory.v...). | ||
WithDisabledRules(params.disable.v...). | ||
WithEnableAll(params.enableAll). | ||
WithEnabledCategories(params.enableCategory.v...). | ||
WithEnabledRules(params.enable.v...). | ||
WithDebugMode(params.debug) | ||
|
||
if customRulesDir != "" { | ||
l = l.WithCustomRules([]string{customRulesDir}) | ||
} | ||
|
||
if params.rules.isSet { | ||
l = l.WithCustomRules(params.rules.v) | ||
} | ||
|
||
if params.ignoreFiles.isSet { | ||
l = l.WithIgnore(params.ignoreFiles.v) | ||
} | ||
|
||
var userConfig config.Config | ||
|
||
userConfigFile, err := readUserConfig(params, regalDir) | ||
|
||
switch { | ||
case err == nil: | ||
defer rio.CloseFileIgnore(userConfigFile) | ||
|
||
if params.debug { | ||
log.Printf("found user config file: %s", userConfigFile.Name()) | ||
} | ||
|
||
if err := yaml.NewDecoder(userConfigFile).Decode(&userConfig); err != nil { | ||
if regalDir != nil { | ||
return fmt.Errorf("failed to decode user config from %s: %w", regalDir.Name(), err) | ||
} | ||
|
||
return fmt.Errorf("failed to decode user config: %w", err) | ||
} | ||
|
||
l = l.WithUserConfig(userConfig) | ||
case err != nil && params.configFile != "": | ||
return fmt.Errorf("user-provided config file not found: %w", err) | ||
case params.debug: | ||
log.Println("no user-provided config file found, will use the default config") | ||
} | ||
|
||
f := fixer.NewFixer() | ||
f.RegisterFixes(fixes.NewDefaultFixes()...) | ||
|
||
fileProvider := fileprovider.NewFSFileProvider(args...) | ||
|
||
fixReport, err := f.Fix(ctx, &l, fileProvider) | ||
if err != nil { | ||
return fmt.Errorf("failed to fix: %w", err) | ||
} | ||
|
||
r, err := fixer.ReporterForFormat(params.format, outputWriter) | ||
if err != nil { | ||
return fmt.Errorf("failed to create reporter for format %s: %w", params.format, err) | ||
} | ||
|
||
err = r.Report(fixReport) | ||
if err != nil { | ||
return fmt.Errorf("failed to output fix report: %w", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package cmd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"time" | ||
|
||
"github.com/styrainc/regal/pkg/config" | ||
) | ||
|
||
// configFileParams supports extracting the config file path from various command | ||
// param types. This allows readUserConfig to be shared. | ||
type configFileParams interface { | ||
getConfigFile() string | ||
} | ||
|
||
func readUserConfig(params configFileParams, regalDir *os.File) (userConfig *os.File, err error) { | ||
if cfgFile := params.getConfigFile(); cfgFile != "" { | ||
userConfig, err = os.Open(cfgFile) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to open config file %w", err) | ||
} | ||
} else { | ||
searchPath, _ := os.Getwd() | ||
if regalDir != nil { | ||
searchPath = regalDir.Name() | ||
} | ||
|
||
if searchPath != "" { | ||
userConfig, err = config.FindConfig(searchPath) | ||
} | ||
} | ||
|
||
return userConfig, err //nolint:wrapcheck | ||
} | ||
|
||
// timeoutParams supports extracting the timeout duration from various command | ||
// param types. This allows getLinterContext to be shared. | ||
type timeoutParams interface { | ||
getTimeout() time.Duration | ||
} | ||
|
||
func getLinterContext(params timeoutParams) (context.Context, func()) { | ||
ctx := context.Background() | ||
|
||
cancel := func() {} | ||
|
||
if to := params.getTimeout(); to != 0 { | ||
ctx, cancel = context.WithTimeout(ctx, to) | ||
} | ||
|
||
return ctx, cancel | ||
} |
Oops, something went wrong.