feat: improve unmatched logic

Separates global excludes processing from `Formatter.Wants`. This removes redundant processing of global excludes in each `Formatter.Wants` call.

If a file has been globally excluded, we do not emit an `on-unmatched` log message. This should help reduce as reported in #317.

Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
Brian McGee 2024-06-14 10:30:13 +01:00
parent 2ca613901d
commit 56d8561125
No known key found for this signature in database
GPG Key ID: D49016E76AD1E8C0
7 changed files with 55 additions and 41 deletions

View File

@ -3,11 +3,12 @@ package cli
import ( import (
"os" "os"
"github.com/gobwas/glob"
"git.numtide.com/numtide/treefmt/format" "git.numtide.com/numtide/treefmt/format"
"git.numtide.com/numtide/treefmt/walk" "git.numtide.com/numtide/treefmt/walk"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/gobwas/glob"
) )
func New() *Format { func New() *Format {
@ -36,8 +37,8 @@ type Format struct {
CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."` CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."`
excludes []glob.Glob formatters map[string]*format.Formatter
formatters map[string]*format.Formatter globalExcludes []glob.Glob
filesCh chan *walk.File filesCh chan *walk.File
processedCh chan *walk.File processedCh chan *walk.File

View File

@ -97,7 +97,7 @@ func (f *Format) Run() (err error) {
} }
// compile global exclude globs // compile global exclude globs
if f.excludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil { if f.globalExcludes, err = format.CompileGlobs(cfg.Global.Excludes); err != nil {
return fmt.Errorf("failed to compile global excludes: %w", err) return fmt.Errorf("failed to compile global excludes: %w", err)
} }
@ -105,7 +105,7 @@ func (f *Format) Run() (err error) {
f.formatters = make(map[string]*format.Formatter) f.formatters = make(map[string]*format.Formatter)
for name, formatterCfg := range cfg.Formatters { for name, formatterCfg := range cfg.Formatters {
formatter, err := format.NewFormatter(name, f.TreeRoot, formatterCfg, f.excludes) formatter, err := format.NewFormatter(name, f.TreeRoot, formatterCfg)
if errors.Is(err, format.ErrCommandNotFound) && f.AllowMissingFormatter { if errors.Is(err, format.ErrCommandNotFound) && f.AllowMissingFormatter {
log.Debugf("formatter command not found: %v", name) log.Debugf("formatter command not found: %v", name)
@ -390,7 +390,15 @@ func (f *Format) applyFormatters(ctx context.Context) func() error {
// iterate the files channel // iterate the files channel
for file := range f.filesCh { for file := range f.filesCh {
// determine a list of formatters that are interested in file // first check if this file has been globally excluded
if format.PathMatches(file.RelPath, f.globalExcludes) {
log.Debugf("path matched global excludes: %s", file.RelPath)
// mark it as processed and continue to the next
f.processedCh <- file
continue
}
// check if any formatters are interested in this file
var matches []*format.Formatter var matches []*format.Formatter
for _, formatter := range f.formatters { for _, formatter := range f.formatters {
if formatter.Wants(file) { if formatter.Wants(file) {
@ -398,11 +406,13 @@ func (f *Format) applyFormatters(ctx context.Context) func() error {
} }
} }
// see if any formatters matched
if len(matches) == 0 { if len(matches) == 0 {
if f.OnUnmatched == log.FatalLevel { if f.OnUnmatched == log.FatalLevel {
return fmt.Errorf("no formatter for path: %s", file.Path) return fmt.Errorf("no formatter for path: %s", file.RelPath)
} }
log.Logf(f.OnUnmatched, "no formatter for path: %s", file.Path) log.Logf(f.OnUnmatched, "no formatter for path: %s", file.RelPath)
// mark it as processed and continue to the next
f.processedCh <- file f.processedCh <- file
} else { } else {
// record the match // record the match

View File

@ -39,21 +39,22 @@ func TestOnUnmatched(t *testing.T) {
paths := []string{ paths := []string{
"go/go.mod", "go/go.mod",
"haskell/haskell.cabal", "haskell/haskell.cabal",
"haskell/treefmt.toml",
"html/scripts/.gitkeep", "html/scripts/.gitkeep",
"nixpkgs.toml",
"python/requirements.txt", "python/requirements.txt",
"rust/Cargo.toml", // these should not be reported as they're in the global excludes
"touch.toml", // - "nixpkgs.toml"
"treefmt.toml", // - "touch.toml"
// - "treefmt.toml"
// - "rust/Cargo.toml"
// - "haskell/treefmt.toml"
} }
out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal") out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal")
as.ErrorContains(err, fmt.Sprintf("no formatter for path: %s/%s", tempDir, paths[0])) as.ErrorContains(err, fmt.Sprintf("no formatter for path: %s", paths[0]))
checkOutput := func(level string, output []byte) { checkOutput := func(level string, output []byte) {
for _, p := range paths { for _, p := range paths {
as.Contains(string(output), fmt.Sprintf("%s format: no formatter for path: %s/%s", level, tempDir, p)) as.Contains(string(output), fmt.Sprintf("%s format: no formatter for path: %s", level, p))
} }
} }

View File

@ -14,6 +14,8 @@ func TestReadConfigFile(t *testing.T) {
as.NotNil(cfg) as.NotNil(cfg)
as.Equal([]string{"*.toml"}, cfg.Global.Excludes)
// python // python
python, ok := cfg.Formatters["python"] python, ok := cfg.Formatters["python"]
as.True(ok, "python formatter not found") as.True(ok, "python formatter not found")

View File

@ -100,7 +100,6 @@ func NewFormatter(
name string, name string,
treeRoot string, treeRoot string,
cfg *config.Formatter, cfg *config.Formatter,
globalExcludes []glob.Glob,
) (*Formatter, error) { ) (*Formatter, error) {
var err error var err error
@ -136,7 +135,6 @@ func NewFormatter(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err) return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err)
} }
f.excludes = append(f.excludes, globalExcludes...)
return &f, nil return &f, nil
} }

View File

@ -22,33 +22,32 @@
statix.enable = true; statix.enable = true;
}; };
settings.formatter = { settings = {
deadnix = { global.excludes = [
priority = 1; "LICENSE"
}; # let's not mess with the test folder
"test/**"
# unsupported extensions
"*.{gif,png,svg,tape,mts,lock,mod,sum,toml,env,envrc,gitignore}"
];
statix = { formatter = {
priority = 2; deadnix = {
}; priority = 1;
};
alejandra = { statix = {
priority = 3; priority = 2;
}; };
prettier = { alejandra = {
options = ["--tab-width" "4"]; priority = 3;
includes = [ };
"*.css"
"*.html" prettier = {
"*.js" options = ["--tab-width" "4"];
"*.json" includes = ["*.{css,html,js,json,jsx,md,mdx,scss,ts,yaml}"];
"*.jsx" };
"*.md"
"*.mdx"
"*.scss"
"*.ts"
"*.yaml"
];
}; };
}; };
}; };

View File

@ -1,5 +1,8 @@
# One CLI to format the code tree - https://git.numtide.com/numtide/treefmt # One CLI to format the code tree - https://git.numtide.com/numtide/treefmt
[global]
excludes = ["*.toml"]
[formatter.python] [formatter.python]
command = "black" command = "black"
includes = ["*.py"] includes = ["*.py"]