Files
releases/cmd/fmt.go
T
Loïc Rosso 3ede316faa feat: do not overwrite file on fmt without changes (#8223)
* feat: do not overwrite file on fmt without changes

When no changes are detected, avoid unnecessary rewrite.

It also help fmt CI tools not to see modified files like treefmt

Signed-off-by: Loïc Rosso <37541460+Loic-R@users.noreply.github.com>

* chore: add tests

Signed-off-by: Loïc Rosso <37541460+Loic-R@users.noreply.github.com>

* chore: fix tests by adding 2 sec sleep time

Signed-off-by: Loïc Rosso <37541460+Loic-R@users.noreply.github.com>

* chore: reduce sleep time, add permission check and better naming

Co-authored-by: Charlie Egan <charlie_egan@apple.com>
Signed-off-by: Loïc Rosso <37541460+Loic-R@users.noreply.github.com>

---------

Signed-off-by: Loïc Rosso <37541460+Loic-R@users.noreply.github.com>
Co-authored-by: Charlie Egan <charlie_egan@apple.com>
2026-01-20 16:54:26 +00:00

323 lines
8.9 KiB
Go

// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/spf13/cobra"
"github.com/open-policy-agent/opa/cmd/internal/env"
fileurl "github.com/open-policy-agent/opa/internal/file/url"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/format"
)
type fmtCommandParams struct {
overwrite bool
list bool
diff bool
fail bool
regoV1 bool
v0Compatible bool
v1Compatible bool
checkResult bool
dropV0Imports bool
capabilitiesFlag *capabilitiesFlag
}
func newFmtCommandParams() *fmtCommandParams {
return &fmtCommandParams{
capabilitiesFlag: newCapabilitiesFlag(),
}
}
func (p *fmtCommandParams) capabilities() *ast.Capabilities {
if p.capabilitiesFlag != nil && p.capabilitiesFlag.C != nil {
return p.capabilitiesFlag.C
}
return ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(p.regoVersion()))
}
func (p *fmtCommandParams) regoVersion() ast.RegoVersion {
// The '--rego-v1' flag takes precedence over the '--v1-compatible' flag.
if p.regoV1 {
return ast.RegoV0CompatV1
}
// The '--v0-compatible' flag takes precedence over the '--v1-compatible' flag.
if p.v0Compatible {
return ast.RegoV0
}
if p.v1Compatible {
return ast.RegoV1
}
return ast.DefaultRegoVersion
}
func opaFmt(args []string, fmtParams *fmtCommandParams) int {
if len(args) == 0 {
if err := formatStdin(fmtParams, os.Stdin, os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
for _, filename := range args {
var err error
filename, err = fileurl.Clean(filename)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
err = filepath.Walk(filename, func(path string, info os.FileInfo, err error) error {
return formatFile(fmtParams, os.Stdout, path, info, err)
})
if err != nil {
switch err := err.(type) {
case fmtError:
fmt.Fprintln(os.Stderr, err.msg)
return err.code
default:
fmt.Fprintln(os.Stderr, err.Error())
return 1
}
}
}
return 0
}
func formatFile(params *fmtCommandParams, out io.Writer, filename string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if filepath.Ext(filename) != ".rego" {
return nil
}
contents, err := os.ReadFile(filename)
if err != nil {
return newError("failed to open file: %v", err)
}
opts := format.Opts{
RegoVersion: params.regoVersion(),
DropV0Imports: params.dropV0Imports,
Capabilities: params.capabilities(),
}
if params.regoV1 {
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV0}
}
if params.v0Compatible {
// v0 takes precedence over v1
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV0}
} else if params.v1Compatible {
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV1}
}
formatted, err := format.SourceWithOpts(filename, contents, opts)
if err != nil {
return newError("failed to format Rego source file: %v", err)
}
if params.checkResult {
popts := ast.ParserOptions{RegoVersion: params.regoVersion()}
_, err := ast.ParseModuleWithOpts("formatted", string(formatted), popts)
if err != nil {
return newError("%s was successfully formatted, but the result is invalid: %v\n\nTo inspect the formatted Rego, you can turn off this check with --check-result=false.", filename, err)
}
}
changed := !bytes.Equal(contents, formatted)
if params.fail && !params.list && !params.diff {
if changed {
return newError("unexpected diff")
}
}
if params.list {
if changed {
fmt.Fprintln(out, filename)
if params.fail {
return newError("unexpected diff")
}
}
return nil
}
if params.diff {
if changed {
diffString := doDiff(contents, formatted)
if _, err := fmt.Fprintln(out, diffString); err != nil {
return newError("failed to print contents: %v", err)
}
if params.fail {
return newError("unexpected diff")
}
}
return nil
}
if params.overwrite {
if !changed {
return nil
}
outfile, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return newError("failed to open file for writing: %v", err)
}
defer outfile.Close()
out = outfile
}
_, err = out.Write(formatted)
if err != nil {
return newError("failed writing formatted contents: %v", err)
}
return nil
}
func formatStdin(params *fmtCommandParams, r io.Reader, w io.Writer) error {
contents, err := io.ReadAll(r)
if err != nil {
return err
}
opts := format.Opts{
RegoVersion: params.regoVersion(),
Capabilities: params.capabilities(),
}
if params.regoV1 {
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV0}
}
if params.v0Compatible {
// v0 takes precedence over v1
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV0}
} else if params.v1Compatible {
opts.ParserOptions = &ast.ParserOptions{RegoVersion: ast.RegoV1}
}
formatted, err := format.SourceWithOpts("stdin", contents, opts)
if err != nil {
return err
}
_, err = w.Write(formatted)
return err
}
func doDiff(a, b []byte) (diffString string) { // "a" is old, "b" is new
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(a), string(b), false)
return dmp.DiffPrettyText(diffs)
}
type fmtError struct {
msg string
code int
}
func (e fmtError) Error() string {
return fmt.Sprintf("%s (%d)", e.msg, e.code)
}
func newError(msg string, a ...any) fmtError {
return fmtError{
msg: fmt.Sprintf(msg, a...),
code: 2,
}
}
func initFmt(root *cobra.Command, _ string) {
cmd := root.Name()
fmtParams := newFmtCommandParams()
formatCommand := &cobra.Command{
Use: "fmt [path [...]]",
Short: "Format Rego source files",
Long: `Format Rego source files.
The 'fmt' command takes a Rego source file and outputs a reformatted version. If no file path
is provided - this tool will use stdin.
The format of the output is not defined specifically; whatever this tool outputs
is considered correct format (with the exception of bugs).
If the '-w' option is supplied, the 'fmt' command will overwrite the source file
instead of printing to stdout.
If the '-d' option is supplied, the 'fmt' command will output a diff between the
original and formatted source.
If the '-l' option is supplied, the 'fmt' command will output the names of files
that would change if formatted. The '-l' option will suppress any other output
to stdout from the 'fmt' command.
If the '--fail' option is supplied, the 'fmt' command will return a non zero exit
code if a file would be reformatted.
The 'fmt' command can be run in several compatibility modes for consuming and outputting
different Rego versions:
* ` + "`" + cmd + ` fmt` + "`" + `:
* v1 Rego is formatted to v1
* ` + "`" + `rego.v1` + "`" + `/` + "`" + `future.keywords` + "`" + ` imports are NOT removed
* ` + "`" + `rego.v1` + "`" + `/` + "`" + `future.keywords` + "`" + ` imports are NOT added if missing
* v0 rego is rejected
* ` + "`" + cmd + ` fmt --v0-compatible` + "`" + `:
* v0 Rego is formatted to v0
* v1 Rego is rejected
* ` + "`" + cmd + ` fmt --v0-v1` + "`" + `:
* v0 Rego is formatted to be compatible with v0 AND v1
* v1 Rego is rejected
* ` + "`" + cmd + ` fmt --v0-v1 --v1-compatible` + "`" + `:
* v1 Rego is formatted to be compatible with v0 AND v1
* v0 Rego is rejected
`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
return env.CmdFlags.CheckEnvironmentVariables(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceErrors = true
cmd.SilenceUsage = true
exit := opaFmt(args, fmtParams)
if exit != 0 {
return newExitError(exit)
}
return nil
},
}
formatCommand.Flags().BoolVarP(&fmtParams.overwrite, "write", "w", false, "overwrite the original source file")
formatCommand.Flags().BoolVarP(&fmtParams.list, "list", "l", false, "list all files who would change when formatted")
formatCommand.Flags().BoolVarP(&fmtParams.diff, "diff", "d", false, "only display a diff of the changes")
formatCommand.Flags().BoolVar(&fmtParams.fail, "fail", false, "non zero exit code on reformat")
addRegoV0V1FlagWithDescription(formatCommand.Flags(), &fmtParams.regoV1, false, "format module(s) to be compatible with both Rego v0 and v1")
addV0CompatibleFlag(formatCommand.Flags(), &fmtParams.v0Compatible, false)
addV1CompatibleFlag(formatCommand.Flags(), &fmtParams.v1Compatible, false)
formatCommand.Flags().BoolVar(&fmtParams.checkResult, "check-result", true, "assert that the formatted code is valid and can be successfully parsed")
formatCommand.Flags().BoolVar(&fmtParams.dropV0Imports, "drop-v0-imports", false, "drop v0 imports from the formatted code, such as 'rego.v1' and 'future.keywords'")
addCapabilitiesFlag(formatCommand.Flags(), fmtParams.capabilitiesFlag)
root.AddCommand(formatCommand)
}