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

Add Formatting Fail Flag for CI Use #923

Merged
merged 5 commits into from
Sep 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
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 --
joerdav marked this conversation as resolved.
Show resolved Hide resolved
package test

templ b() {
Expand All @@ -32,3 +32,23 @@ templ b() {
</p>
</div>
}
-- c.templ --
joerdav marked this conversation as resolved.
Show resolved Hide resolved
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