Skip to content

Commit ff29e12

Browse files
benyankejoerdav
andauthoredSep 18, 2024··
feat: Add -fail flag to fmt (#923)
Co-authored-by: Joe Davidson <joe.davidson.21111@gmail.com>
1 parent 88eed0f commit ff29e12

File tree

6 files changed

+131
-30
lines changed

6 files changed

+131
-30
lines changed
 

‎cmd/templ/fmtcmd/main.go

+40-21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
)
1818

1919
type Arguments struct {
20+
FailIfChanged bool
2021
ToStdout bool
2122
StdinFilepath string
2223
Files []string
@@ -26,9 +27,10 @@ type Arguments struct {
2627
func Run(log *slog.Logger, stdin io.Reader, stdout io.Writer, args Arguments) (err error) {
2728
// If no files are provided, read from stdin and write to stdout.
2829
if len(args.Files) == 0 {
29-
return format(writeToWriter(stdout), readFromReader(stdin, args.StdinFilepath), true)
30+
out, _ := format(writeToWriter(stdout), readFromReader(stdin, args.StdinFilepath), true)
31+
return out
3032
}
31-
process := func(fileName string) error {
33+
process := func(fileName string) (error, bool) {
3234
read := readFromFile(fileName)
3335
write := writeToFile
3436
if args.ToStdout {
@@ -38,22 +40,24 @@ func Run(log *slog.Logger, stdin io.Reader, stdout io.Writer, args Arguments) (e
3840
return format(write, read, writeIfUnchanged)
3941
}
4042
dir := args.Files[0]
41-
return NewFormatter(log, dir, process, args.WorkerCount).Run()
43+
return NewFormatter(log, dir, process, args.WorkerCount, args.FailIfChanged).Run()
4244
}
4345

4446
type Formatter struct {
45-
Log *slog.Logger
46-
Dir string
47-
Process func(fileName string) error
48-
WorkerCount int
47+
Log *slog.Logger
48+
Dir string
49+
Process func(fileName string) (error, bool)
50+
WorkerCount int
51+
FailIfChange bool
4952
}
5053

51-
func NewFormatter(log *slog.Logger, dir string, process func(fileName string) error, workerCount int) *Formatter {
54+
func NewFormatter(log *slog.Logger, dir string, process func(fileName string) (error, bool), workerCount int, failIfChange bool) *Formatter {
5255
f := &Formatter{
53-
Log: log,
54-
Dir: dir,
55-
Process: process,
56-
WorkerCount: workerCount,
56+
Log: log,
57+
Dir: dir,
58+
Process: process,
59+
WorkerCount: workerCount,
60+
FailIfChange: failIfChange,
5761
}
5862
if f.WorkerCount == 0 {
5963
f.WorkerCount = runtime.NumCPU()
@@ -62,12 +66,16 @@ func NewFormatter(log *slog.Logger, dir string, process func(fileName string) er
6266
}
6367

6468
func (f *Formatter) Run() (err error) {
69+
changesMade := 0
6570
start := time.Now()
6671
results := make(chan processor.Result)
6772
f.Log.Debug("Walking directory", slog.String("path", f.Dir))
6873
go processor.Process(f.Dir, f.Process, f.WorkerCount, results)
6974
var successCount, errorCount int
7075
for r := range results {
76+
if r.ChangesMade {
77+
changesMade += 1
78+
}
7179
if r.Error != nil {
7280
f.Log.Error(r.FileName, slog.Any("error", r.Error))
7381
errorCount++
@@ -76,10 +84,18 @@ func (f *Formatter) Run() (err error) {
7684
f.Log.Debug(r.FileName, slog.Duration("duration", r.Duration))
7785
successCount++
7886
}
79-
f.Log.Info("Format complete", slog.Int("count", successCount+errorCount), slog.Int("errors", errorCount), slog.Duration("duration", time.Since(start)))
87+
88+
if f.FailIfChange && changesMade > 0 {
89+
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)))
90+
return fmt.Errorf("templates were not formatted properly")
91+
}
92+
93+
f.Log.Info("Format Complete", slog.Int("count", successCount+errorCount), slog.Int("errors", errorCount), slog.Int("changed", changesMade), slog.Duration("duration", time.Since(start)))
94+
8095
if errorCount > 0 {
8196
return fmt.Errorf("formatting failed")
8297
}
98+
8399
return
84100
}
85101

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

125-
func format(write writer, read reader, writeIfUnchanged bool) (err error) {
141+
func format(write writer, read reader, writeIfUnchanged bool) (err error, fileChanged bool) {
126142
fileName, src, err := read()
127143
if err != nil {
128-
return err
144+
return err, false
129145
}
130146
t, err := parser.ParseString(src)
131147
if err != nil {
132-
return err
148+
return err, false
133149
}
134150
t.Filepath = fileName
135151
t, err = imports.Process(t)
136152
if err != nil {
137-
return err
153+
return err, false
138154
}
139155
w := new(bytes.Buffer)
140156
if err = t.Write(w); err != nil {
141-
return fmt.Errorf("formatting error: %w", err)
157+
return fmt.Errorf("formatting error: %w", err), false
142158
}
143-
if !writeIfUnchanged && src == w.String() {
144-
return nil
159+
160+
fileChanged = (src != w.String())
161+
162+
if !writeIfUnchanged && !fileChanged {
163+
return nil, fileChanged
145164
}
146-
return write(fileName, w.String())
165+
return write(fileName, w.String()), fileChanged
147166
}

‎cmd/templ/fmtcmd/main_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func TestFormat(t *testing.T) {
8484
Files: []string{
8585
tp.testFiles["a.templ"].name,
8686
},
87+
FailIfChanged: false,
8788
}); err != nil {
8889
t.Fatalf("failed to run format command: %v", err)
8990
}
@@ -101,6 +102,7 @@ func TestFormat(t *testing.T) {
101102
Files: []string{
102103
tp.testFiles["a.templ"].name,
103104
},
105+
FailIfChanged: false,
104106
}); err != nil {
105107
t.Fatalf("failed to run format command: %v", err)
106108
}
@@ -112,4 +114,50 @@ func TestFormat(t *testing.T) {
112114
t.Error(diff)
113115
}
114116
})
117+
118+
t.Run("fails when fail flag used and change occurs", func(t *testing.T) {
119+
tp, err := setupProjectDir()
120+
if err != nil {
121+
t.Fatalf("failed to setup project dir: %v", err)
122+
}
123+
defer tp.cleanup()
124+
if err = Run(log, nil, nil, Arguments{
125+
Files: []string{
126+
tp.testFiles["a.templ"].name,
127+
},
128+
FailIfChanged: true,
129+
}); err == nil {
130+
t.Fatal("command should have exited with an error and did not")
131+
}
132+
data, err := os.ReadFile(tp.testFiles["a.templ"].name)
133+
if err != nil {
134+
t.Fatalf("failed to read file: %v", err)
135+
}
136+
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, string(data)); diff != "" {
137+
t.Error(diff)
138+
}
139+
})
140+
141+
t.Run("passes when fail flag used and no change occurs", func(t *testing.T) {
142+
tp, err := setupProjectDir()
143+
if err != nil {
144+
t.Fatalf("failed to setup project dir: %v", err)
145+
}
146+
defer tp.cleanup()
147+
if err = Run(log, nil, nil, Arguments{
148+
Files: []string{
149+
tp.testFiles["c.templ"].name,
150+
},
151+
FailIfChanged: true,
152+
}); err != nil {
153+
t.Fatalf("failed to run format command: %v", err)
154+
}
155+
data, err := os.ReadFile(tp.testFiles["c.templ"].name)
156+
if err != nil {
157+
t.Fatalf("failed to read file: %v", err)
158+
}
159+
if diff := cmp.Diff(tp.testFiles["c.templ"].expected, string(data)); diff != "" {
160+
t.Error(diff)
161+
}
162+
})
115163
}

‎cmd/templ/fmtcmd/testdata.txtar

+21-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ templ b() {
2222
<div><p>B
2323
</p></div>
2424
}
25-
-- a.templ --
25+
-- b.templ --
2626
package test
2727

2828
templ b() {
@@ -32,3 +32,23 @@ templ b() {
3232
</p>
3333
</div>
3434
}
35+
-- c.templ --
36+
package test
37+
38+
templ c() {
39+
<div>
40+
<p>
41+
C
42+
</p>
43+
</div>
44+
}
45+
-- c.templ --
46+
package test
47+
48+
templ c() {
49+
<div>
50+
<p>
51+
C
52+
</p>
53+
</div>
54+
}

‎cmd/templ/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ Args:
298298
Set log verbosity level. (default "info", options: "debug", "info", "warn", "error")
299299
-w
300300
Number of workers to use when formatting code. (default runtime.NumCPUs).
301+
-fail
302+
Fails with exit code 1 if files are changed. (e.g. in CI)
301303
-help
302304
Print help and exit.
303305
`
@@ -308,6 +310,7 @@ func fmtCmd(stdin io.Reader, stdout, stderr io.Writer, args []string) (code int)
308310
workerCountFlag := cmd.Int("w", runtime.NumCPU(), "")
309311
verboseFlag := cmd.Bool("v", false, "")
310312
logLevelFlag := cmd.String("log-level", "info", "")
313+
failIfChanged := cmd.Bool("fail", false, "")
311314
stdoutFlag := cmd.Bool("stdout", false, "")
312315
stdinFilepath := cmd.String("stdin-filepath", "", "")
313316
err := cmd.Parse(args)
@@ -327,6 +330,7 @@ func fmtCmd(stdin io.Reader, stdout, stderr io.Writer, args []string) (code int)
327330
Files: cmd.Args(),
328331
WorkerCount: *workerCountFlag,
329332
StdinFilepath: *stdinFilepath,
333+
FailIfChanged: *failIfChanged,
330334
})
331335
if err != nil {
332336
return 1

‎cmd/templ/processor/processor.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
)
1111

1212
type Result struct {
13-
FileName string
14-
Duration time.Duration
15-
Error error
13+
FileName string
14+
Duration time.Duration
15+
Error error
16+
ChangesMade bool
1617
}
1718

18-
func Process(dir string, f func(fileName string) error, workerCount int, results chan<- Result) {
19+
func Process(dir string, f func(fileName string) (error, bool), workerCount int, results chan<- Result) {
1920
templates := make(chan string)
2021
go func() {
2122
defer close(templates)
@@ -56,7 +57,7 @@ func FindTemplates(srcPath string, output chan<- string) (err error) {
5657
})
5758
}
5859

59-
func ProcessChannel(templates <-chan string, dir string, f func(fileName string) error, workerCount int, results chan<- Result) {
60+
func ProcessChannel(templates <-chan string, dir string, f func(fileName string) (error, bool), workerCount int, results chan<- Result) {
6061
defer close(results)
6162
var wg sync.WaitGroup
6263
wg.Add(workerCount)
@@ -65,10 +66,12 @@ func ProcessChannel(templates <-chan string, dir string, f func(fileName string)
6566
defer wg.Done()
6667
for sourceFileName := range templates {
6768
start := time.Now()
69+
outErr, outChanged := f(sourceFileName)
6870
results <- Result{
69-
FileName: sourceFileName,
70-
Error: f(sourceFileName),
71-
Duration: time.Since(start),
71+
FileName: sourceFileName,
72+
Error: outErr,
73+
Duration: time.Since(start),
74+
ChangesMade: outChanged,
7275
}
7376
}
7477
}()

‎docs/docs/09-commands-and-tools/01-cli.md

+7
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ templ fmt .
8787
templ fmt
8888
```
8989

90+
Alternatively, you can run `fmt` in CI to ensure that invalidly formatted templatess do not pass CI. This will cause the command
91+
to exit with unix error-code `1` if any templates needed to be modified.
92+
93+
```
94+
templ fmt -fail .
95+
```
96+
9097
## Language Server for IDE integration
9198

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

0 commit comments

Comments
 (0)
Please sign in to comment.