Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixer: Regal fix command #653

Merged
merged 21 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions cmd/fix.go
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(&params.configFile, "config-file", "c", "",
"set path of configuration file")
fixCommand.Flags().StringVarP(&params.format, "format", "f", formatPretty,
"set output format (pretty is the only supported format)")
fixCommand.Flags().StringVarP(&params.outputFile, "output-file", "o", "",
"set file to use for fixing output, defaults to stdout")
fixCommand.Flags().BoolVar(&params.noColor, "no-color", false,
"Disable color output")
fixCommand.Flags().VarP(&params.rules, "rules", "r",
"set custom rules file(s). This flag can be repeated.")
fixCommand.Flags().DurationVar(&params.timeout, "timeout", 0,
"set timeout for fixing (default unlimited)")
fixCommand.Flags().BoolVar(&params.debug, "debug", false,
"enable debug logging (including print output from custom policy)")

fixCommand.Flags().VarP(&params.disable, "disable", "d",
"disable specific rule(s). This flag can be repeated.")
fixCommand.Flags().BoolVarP(&params.disableAll, "disable-all", "D", false,
"disable all rules")
fixCommand.Flags().VarP(&params.disableCategory, "disable-category", "",
"disable all rules in a category. This flag can be repeated.")

fixCommand.Flags().VarP(&params.enable, "enable", "e",
"enable specific rule(s). This flag can be repeated.")
fixCommand.Flags().BoolVarP(&params.enableAll, "enable-all", "E", false,
"enable all rules")
fixCommand.Flags().VarP(&params.enableCategory, "enable-category", "",
"enable all rules in a category. This flag can be repeated.")

fixCommand.Flags().VarP(&params.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
}
45 changes: 10 additions & 35 deletions cmd/lint.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -47,6 +46,14 @@ type lintCommandParams struct {
ignoreFiles repeatedStringFlag
}

func (p *lintCommandParams) getConfigFile() string {
return p.configFile
}

func (p *lintCommandParams) getTimeout() time.Duration {
return p.timeout
}

const stringType = "string"

type repeatedStringFlag struct {
Expand All @@ -70,7 +77,7 @@ func (f *repeatedStringFlag) Set(s string) error {
}

func init() {
params := lintCommandParams{}
params := &lintCommandParams{}

lintCommand := &cobra.Command{
Use: "lint <path> [path [...]]",
Expand Down Expand Up @@ -177,7 +184,7 @@ func init() {
}

//nolint:gocognit
func lint(args []string, params lintCommandParams) (report.Report, error) {
func lint(args []string, params *lintCommandParams) (report.Report, error) {
var err error

ctx, cancel := getLinterContext(params)
Expand Down Expand Up @@ -334,38 +341,6 @@ func getReporter(format string, outputWriter io.Writer) (reporter.Reporter, erro
}
}

func readUserConfig(params lintCommandParams, regalDir *os.File) (userConfig *os.File, err error) {
if params.configFile != "" {
userConfig, err = os.Open(params.configFile)
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
}

func getLinterContext(params lintCommandParams) (context.Context, func()) {
ctx := context.Background()

cancel := func() {}

if params.timeout != 0 {
ctx, cancel = context.WithTimeout(ctx, params.timeout)
}

return ctx, cancel
}

func getWriterForOutputFile(filename string) (io.Writer, error) {
if _, err := os.Stat(filename); err == nil {
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
Expand Down
54 changes: 54 additions & 0 deletions cmd/utils.go
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
}
Loading