Skip to content

Commit

Permalink
feat: Add -fail flag to fmt (#923)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Davidson <[email protected]>
  • Loading branch information
benyanke and joerdav authored Sep 18, 2024
1 parent 88eed0f commit ff29e12
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 30 deletions.
61 changes: 40 additions & 21 deletions cmd/templ/fmtcmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
)

type Arguments struct {
FailIfChanged bool
ToStdout bool
StdinFilepath string
Files []string
Expand All @@ -26,9 +27,10 @@ type Arguments struct {
func Run(log *slog.Logger, stdin io.Reader, stdout io.Writer, args Arguments) (err error) {
// If no files are provided, read from stdin and write to stdout.
if len(args.Files) == 0 {
return format(writeToWriter(stdout), readFromReader(stdin, args.StdinFilepath), true)
out, _ := format(writeToWriter(stdout), readFromReader(stdin, args.StdinFilepath), true)
return out
}
process := func(fileName string) error {
process := func(fileName string) (error, bool) {
read := readFromFile(fileName)
write := writeToFile
if args.ToStdout {
Expand All @@ -38,22 +40,24 @@ func Run(log *slog.Logger, stdin io.Reader, stdout io.Writer, args Arguments) (e
return format(write, read, writeIfUnchanged)
}
dir := args.Files[0]
return NewFormatter(log, dir, process, args.WorkerCount).Run()
return NewFormatter(log, dir, process, args.WorkerCount, args.FailIfChanged).Run()
}

type Formatter struct {
Log *slog.Logger
Dir string
Process func(fileName string) error
WorkerCount int
Log *slog.Logger
Dir string
Process func(fileName string) (error, bool)
WorkerCount int
FailIfChange bool
}

func NewFormatter(log *slog.Logger, dir string, process func(fileName string) error, workerCount int) *Formatter {
func NewFormatter(log *slog.Logger, dir string, process func(fileName string) (error, bool), workerCount int, failIfChange bool) *Formatter {
f := &Formatter{
Log: log,
Dir: dir,
Process: process,
WorkerCount: workerCount,
Log: log,
Dir: dir,
Process: process,
WorkerCount: workerCount,
FailIfChange: failIfChange,
}
if f.WorkerCount == 0 {
f.WorkerCount = runtime.NumCPU()
Expand All @@ -62,12 +66,16 @@ func NewFormatter(log *slog.Logger, dir string, process func(fileName string) er
}

func (f *Formatter) Run() (err error) {
changesMade := 0
start := time.Now()
results := make(chan processor.Result)
f.Log.Debug("Walking directory", slog.String("path", f.Dir))
go processor.Process(f.Dir, f.Process, f.WorkerCount, results)
var successCount, errorCount int
for r := range results {
if r.ChangesMade {
changesMade += 1
}
if r.Error != nil {
f.Log.Error(r.FileName, slog.Any("error", r.Error))
errorCount++
Expand All @@ -76,10 +84,18 @@ func (f *Formatter) Run() (err error) {
f.Log.Debug(r.FileName, slog.Duration("duration", r.Duration))
successCount++
}
f.Log.Info("Format complete", slog.Int("count", successCount+errorCount), slog.Int("errors", errorCount), slog.Duration("duration", time.Since(start)))

if f.FailIfChange && changesMade > 0 {
f.Log.Error("Templates were valid but not properly formatted", slog.Int("count", successCount+errorCount), slog.Int("changed", changesMade), slog.Int("errors", errorCount), slog.Duration("duration", time.Since(start)))
return fmt.Errorf("templates were not formatted properly")
}

f.Log.Info("Format Complete", slog.Int("count", successCount+errorCount), slog.Int("errors", errorCount), slog.Int("changed", changesMade), slog.Duration("duration", time.Since(start)))

if errorCount > 0 {
return fmt.Errorf("formatting failed")
}

return
}

Expand Down Expand Up @@ -122,26 +138,29 @@ func writeToFile(fileName, tgt string) error {
return atomic.WriteFile(fileName, bytes.NewBufferString(tgt))
}

func format(write writer, read reader, writeIfUnchanged bool) (err error) {
func format(write writer, read reader, writeIfUnchanged bool) (err error, fileChanged bool) {
fileName, src, err := read()
if err != nil {
return err
return err, false
}
t, err := parser.ParseString(src)
if err != nil {
return err
return err, false
}
t.Filepath = fileName
t, err = imports.Process(t)
if err != nil {
return err
return err, false
}
w := new(bytes.Buffer)
if err = t.Write(w); err != nil {
return fmt.Errorf("formatting error: %w", err)
return fmt.Errorf("formatting error: %w", err), false
}
if !writeIfUnchanged && src == w.String() {
return nil

fileChanged = (src != w.String())

if !writeIfUnchanged && !fileChanged {
return nil, fileChanged
}
return write(fileName, w.String())
return write(fileName, w.String()), fileChanged
}
48 changes: 48 additions & 0 deletions cmd/templ/fmtcmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func TestFormat(t *testing.T) {
Files: []string{
tp.testFiles["a.templ"].name,
},
FailIfChanged: false,
}); err != nil {
t.Fatalf("failed to run format command: %v", err)
}
Expand All @@ -101,6 +102,7 @@ func TestFormat(t *testing.T) {
Files: []string{
tp.testFiles["a.templ"].name,
},
FailIfChanged: false,
}); err != nil {
t.Fatalf("failed to run format command: %v", err)
}
Expand All @@ -112,4 +114,50 @@ func TestFormat(t *testing.T) {
t.Error(diff)
}
})

t.Run("fails when fail flag used and change occurs", func(t *testing.T) {
tp, err := setupProjectDir()
if err != nil {
t.Fatalf("failed to setup project dir: %v", err)
}
defer tp.cleanup()
if err = Run(log, nil, nil, Arguments{
Files: []string{
tp.testFiles["a.templ"].name,
},
FailIfChanged: true,
}); err == nil {
t.Fatal("command should have exited with an error and did not")
}
data, err := os.ReadFile(tp.testFiles["a.templ"].name)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, string(data)); diff != "" {
t.Error(diff)
}
})

t.Run("passes when fail flag used and no change occurs", func(t *testing.T) {
tp, err := setupProjectDir()
if err != nil {
t.Fatalf("failed to setup project dir: %v", err)
}
defer tp.cleanup()
if err = Run(log, nil, nil, Arguments{
Files: []string{
tp.testFiles["c.templ"].name,
},
FailIfChanged: true,
}); err != nil {
t.Fatalf("failed to run format command: %v", err)
}
data, err := os.ReadFile(tp.testFiles["c.templ"].name)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
if diff := cmp.Diff(tp.testFiles["c.templ"].expected, string(data)); diff != "" {
t.Error(diff)
}
})
}
22 changes: 21 additions & 1 deletion cmd/templ/fmtcmd/testdata.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ templ b() {
<div><p>B
</p></div>
}
-- a.templ --
-- b.templ --
package test

templ b() {
Expand All @@ -32,3 +32,23 @@ templ b() {
</p>
</div>
}
-- c.templ --
package test

templ c() {
<div>
<p>
C
</p>
</div>
}
-- c.templ --
package test

templ c() {
<div>
<p>
C
</p>
</div>
}
4 changes: 4 additions & 0 deletions cmd/templ/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ Args:
Set log verbosity level. (default "info", options: "debug", "info", "warn", "error")
-w
Number of workers to use when formatting code. (default runtime.NumCPUs).
-fail
Fails with exit code 1 if files are changed. (e.g. in CI)
-help
Print help and exit.
`
Expand All @@ -308,6 +310,7 @@ func fmtCmd(stdin io.Reader, stdout, stderr io.Writer, args []string) (code int)
workerCountFlag := cmd.Int("w", runtime.NumCPU(), "")
verboseFlag := cmd.Bool("v", false, "")
logLevelFlag := cmd.String("log-level", "info", "")
failIfChanged := cmd.Bool("fail", false, "")
stdoutFlag := cmd.Bool("stdout", false, "")
stdinFilepath := cmd.String("stdin-filepath", "", "")
err := cmd.Parse(args)
Expand All @@ -327,6 +330,7 @@ func fmtCmd(stdin io.Reader, stdout, stderr io.Writer, args []string) (code int)
Files: cmd.Args(),
WorkerCount: *workerCountFlag,
StdinFilepath: *stdinFilepath,
FailIfChanged: *failIfChanged,
})
if err != nil {
return 1
Expand Down
19 changes: 11 additions & 8 deletions cmd/templ/processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import (
)

type Result struct {
FileName string
Duration time.Duration
Error error
FileName string
Duration time.Duration
Error error
ChangesMade bool
}

func Process(dir string, f func(fileName string) error, workerCount int, results chan<- Result) {
func Process(dir string, f func(fileName string) (error, bool), workerCount int, results chan<- Result) {
templates := make(chan string)
go func() {
defer close(templates)
Expand Down Expand Up @@ -56,7 +57,7 @@ func FindTemplates(srcPath string, output chan<- string) (err error) {
})
}

func ProcessChannel(templates <-chan string, dir string, f func(fileName string) error, workerCount int, results chan<- Result) {
func ProcessChannel(templates <-chan string, dir string, f func(fileName string) (error, bool), workerCount int, results chan<- Result) {
defer close(results)
var wg sync.WaitGroup
wg.Add(workerCount)
Expand All @@ -65,10 +66,12 @@ func ProcessChannel(templates <-chan string, dir string, f func(fileName string)
defer wg.Done()
for sourceFileName := range templates {
start := time.Now()
outErr, outChanged := f(sourceFileName)
results <- Result{
FileName: sourceFileName,
Error: f(sourceFileName),
Duration: time.Since(start),
FileName: sourceFileName,
Error: outErr,
Duration: time.Since(start),
ChangesMade: outChanged,
}
}
}()
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/09-commands-and-tools/01-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ templ fmt .
templ fmt
```

Alternatively, you can run `fmt` in CI to ensure that invalidly formatted templatess do not pass CI. This will cause the command
to exit with unix error-code `1` if any templates needed to be modified.

```
templ fmt -fail .
```

## Language Server for IDE integration

`templ lsp` provides a Language Server Protocol (LSP) implementation to support IDE integrations.
Expand Down

0 comments on commit ff29e12

Please sign in to comment.