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 (
"os"
"github.com/gobwas/glob"
"git.numtide.com/numtide/treefmt/format"
"git.numtide.com/numtide/treefmt/walk"
"github.com/alecthomas/kong"
"github.com/charmbracelet/log"
"github.com/gobwas/glob"
)
func New() *Format {
@ -36,8 +37,8 @@ type Format struct {
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
processedCh chan *walk.File

View File

@ -97,7 +97,7 @@ func (f *Format) Run() (err error) {
}
// 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)
}
@ -105,7 +105,7 @@ func (f *Format) Run() (err error) {
f.formatters = make(map[string]*format.Formatter)
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 {
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
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
for _, formatter := range f.formatters {
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 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
} else {
// record the match

View File

@ -39,21 +39,22 @@ func TestOnUnmatched(t *testing.T) {
paths := []string{
"go/go.mod",
"haskell/haskell.cabal",
"haskell/treefmt.toml",
"html/scripts/.gitkeep",
"nixpkgs.toml",
"python/requirements.txt",
"rust/Cargo.toml",
"touch.toml",
"treefmt.toml",
// these should not be reported as they're in the global excludes
// - "nixpkgs.toml"
// - "touch.toml"
// - "treefmt.toml"
// - "rust/Cargo.toml"
// - "haskell/treefmt.toml"
}
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) {
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.Equal([]string{"*.toml"}, cfg.Global.Excludes)
// python
python, ok := cfg.Formatters["python"]
as.True(ok, "python formatter not found")

View File

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

View File

@ -22,33 +22,32 @@
statix.enable = true;
};
settings.formatter = {
deadnix = {
priority = 1;
};
settings = {
global.excludes = [
"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 = {
priority = 2;
};
formatter = {
deadnix = {
priority = 1;
};
alejandra = {
priority = 3;
};
statix = {
priority = 2;
};
prettier = {
options = ["--tab-width" "4"];
includes = [
"*.css"
"*.html"
"*.js"
"*.json"
"*.jsx"
"*.md"
"*.mdx"
"*.scss"
"*.ts"
"*.yaml"
];
alejandra = {
priority = 3;
};
prettier = {
options = ["--tab-width" "4"];
includes = ["*.{css,html,js,json,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
[global]
excludes = ["*.toml"]
[formatter.python]
command = "black"
includes = ["*.py"]