Skip to content

Commit 1b517c6

Browse files
committed
feat: add --on-unmatched
By default, if a path does not match any formatter a log message at WARN level will be emitted. A user can change this by providing the `--on-unmatched` or `-u` flag and specifying a log level `debug,info,warn,error,fatal`. If fatal, the process will exit with an error on the first unmatched path encountered. Closes #302 Signed-off-by: Brian McGee <[email protected]>
1 parent 6cf9524 commit 1b517c6

File tree

7 files changed

+124
-7
lines changed

7 files changed

+124
-7
lines changed

cli/cli.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ type Format struct {
2222
Version bool `name:"version" short:"V" help:"Print version."`
2323
Init bool `name:"init" short:"i" help:"Create a new treefmt.toml."`
2424

25+
OnUnmatched log.Level `name:"on-unmatched" short:"u" default:"warn" help:"Log paths that did not match any formatters at the specified log level. Possible values are debug,info,warn,error,fatal."`
26+
2527
Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."`
2628
Stdin bool `help:"Format the context passed in via stdin."`
2729

2830
CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."`
2931
}
3032

31-
func ConfigureLogging() {
33+
func configureLogging() {
3234
log.SetReportTimestamp(false)
3335

3436
if Cli.Verbosity == 0 {

cli/format.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ var (
4040
)
4141

4242
func (f *Format) Run() (err error) {
43+
// set log level and other options
44+
configureLogging()
45+
4346
// cpu profiling
4447
if Cli.CpuProfile != "" {
4548
cpuProfile, err := os.Create(Cli.CpuProfile)
@@ -355,8 +358,10 @@ func applyFormatters(ctx context.Context) func() error {
355358
}
356359

357360
if len(matches) == 0 {
358-
// no match, so we send it direct to the processed channel
359-
log.Debugf("no match found: %s", file.Path)
361+
if Cli.OnUnmatched == log.FatalLevel {
362+
return fmt.Errorf("no formatter for path: %s", file.Path)
363+
}
364+
log.Logf(Cli.OnUnmatched, "no formatter for path: %s", file.Path)
360365
processedCh <- file
361366
} else {
362367
// record the match

cli/format_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"bufio"
5+
"fmt"
56
"os"
67
"os/exec"
78
"path"
@@ -21,6 +22,63 @@ import (
2122
"github.com/stretchr/testify/require"
2223
)
2324

25+
func TestOnUnmatched(t *testing.T) {
26+
as := require.New(t)
27+
28+
// capture current cwd, so we can replace it after the test is finished
29+
cwd, err := os.Getwd()
30+
as.NoError(err)
31+
32+
t.Cleanup(func() {
33+
// return to the previous working directory
34+
as.NoError(os.Chdir(cwd))
35+
})
36+
37+
tempDir := test.TempExamples(t)
38+
39+
paths := []string{
40+
"go/go.mod",
41+
"haskell/haskell.cabal",
42+
"haskell/treefmt.toml",
43+
"html/scripts/.gitkeep",
44+
"nixpkgs.toml",
45+
"python/requirements.txt",
46+
"rust/Cargo.toml",
47+
"touch.toml",
48+
"treefmt.toml",
49+
}
50+
51+
out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal")
52+
as.ErrorContains(err, fmt.Sprintf("no formatter for path: %s/%s", tempDir, paths[0]))
53+
54+
checkOutput := func(level string, output []byte) {
55+
for _, p := range paths {
56+
as.Contains(string(output), fmt.Sprintf("%s format: no formatter for path: %s/%s", level, tempDir, p))
57+
}
58+
}
59+
60+
// default is warn
61+
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c")
62+
as.NoError(err)
63+
checkOutput("WARN", out)
64+
65+
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn")
66+
as.NoError(err)
67+
checkOutput("WARN", out)
68+
69+
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error")
70+
as.NoError(err)
71+
checkOutput("ERRO", out)
72+
73+
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info")
74+
as.NoError(err)
75+
checkOutput("INFO", out)
76+
77+
out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv", "-u", "debug")
78+
as.NoError(err)
79+
checkOutput("DEBU", out)
80+
}
81+
2482
func TestCpuProfile(t *testing.T) {
2583
as := require.New(t)
2684
tempDir := test.TempExamples(t)

cli/helpers_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"path/filepath"
88
"testing"
99

10+
"github.com/charmbracelet/log"
11+
1012
"git.numtide.com/numtide/treefmt/stats"
1113

1214
"git.numtide.com/numtide/treefmt/test"
@@ -33,7 +35,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
3335
t.Helper()
3436

3537
// create a new kong context
36-
p := newKong(t, &Cli)
38+
p := newKong(t, &Cli, Options...)
3739
ctx, err := p.Parse(args)
3840
if err != nil {
3941
return nil, err
@@ -50,6 +52,8 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
5052
os.Stdout = tempOut
5153
os.Stderr = tempOut
5254

55+
log.SetOutput(tempOut)
56+
5357
// run the command
5458
if err = ctx.Run(); err != nil {
5559
return nil, err
@@ -68,6 +72,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
6872
// swap outputs back
6973
os.Stdout = stdout
7074
os.Stderr = stderr
75+
log.SetOutput(stderr)
7176

7277
return out, nil
7378
}

cli/mappers.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/alecthomas/kong"
8+
"github.com/charmbracelet/log"
9+
)
10+
11+
var Options []kong.Option
12+
13+
func init() {
14+
Options = []kong.Option{
15+
kong.TypeMapper(reflect.TypeOf(log.DebugLevel), logLevelDecoder()),
16+
}
17+
}
18+
19+
func logLevelDecoder() kong.MapperFunc {
20+
return func(ctx *kong.DecodeContext, target reflect.Value) error {
21+
t, err := ctx.Scan.PopValue("string")
22+
if err != nil {
23+
return err
24+
}
25+
var str string
26+
switch v := t.Value.(type) {
27+
case string:
28+
str = v
29+
default:
30+
return fmt.Errorf("expected a string but got %q (%T)", t, t.Value)
31+
}
32+
level, err := log.ParseLevel(str)
33+
if err != nil {
34+
return fmt.Errorf("failed to parse '%v' as log level: %w", level, err)
35+
}
36+
target.Set(reflect.ValueOf(level))
37+
return nil
38+
}
39+
}

docs/usage.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Flags:
2626
-v, --verbose Set the verbosity of logs e.g. -vv ($LOG_LEVEL).
2727
-V, --version Print version.
2828
-i, --init Create a new treefmt.toml.
29+
-u, --on-unmatched=warn Log paths that did not match any formatters at the specified log level. Possible values are debug,info,warn,error,fatal.
2930
--stdin Format the context passed in via stdin.
3031
--cpu-profile=STRING The file into which a cpu profile will be written.
3132
```
@@ -95,9 +96,17 @@ while `-vv` will also show `[DEBUG]` messages.
9596

9697
Create a new `treefmt.toml`.
9798

99+
### `-u --on-unmatched`
100+
101+
Log paths that did not match any formatters at the specified log level. Possible values are debug,info,warn,error,fatal.
102+
98103
### `--stdin`
99104

100-
Format the content passed in via stdin.
105+
Format the context passed in via stdin.
106+
107+
### `--cpu-profile`
108+
109+
The file into which a cpu profile will be written.
101110

102111
### `-V, --version`
103112

main.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ func main() {
3535
}
3636
}
3737

38-
ctx := kong.Parse(&cli.Cli)
39-
cli.ConfigureLogging()
38+
ctx := kong.Parse(&cli.Cli, cli.Options...)
4039
ctx.FatalIfErrorf(ctx.Run())
4140
}

0 commit comments

Comments
 (0)