treefmt/format/formatter.go
Brian McGee ce14ee828f
feat: simplify pipeline model
For each path we determine the list of formatters that are interested in formatting it. From there, we sort
the list of formatters first by priority (lower value, higher priority) and then by name (lexicographically).

With this information we create a batch key which is based on the unique sequence of formatters. When enough paths with the same sequence is ready we apply them in order to each formatter.

By default, with no special configuration, this model guarantees that a given path will only be processed by one formatter at a time.

If a user wishes to influence the order in which formatters are applied they can use the priority field.

Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-05-26 16:52:04 +01:00

143 lines
3.3 KiB
Go

package format
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"time"
"git.numtide.com/numtide/treefmt/walk"
"git.numtide.com/numtide/treefmt/config"
"github.com/charmbracelet/log"
"github.com/gobwas/glob"
)
// ErrCommandNotFound is returned when the Command for a Formatter is not available.
var ErrCommandNotFound = errors.New("formatter command not found in PATH")
// Formatter represents a command which should be applied to a filesystem.
type Formatter struct {
name string
config *config.Formatter
log *log.Logger
executable string // path to the executable described by Command
workingDir string
// internal compiled versions of Includes and Excludes.
includes []glob.Glob
excludes []glob.Glob
batch []string
}
// Executable returns the path to the executable defined by Command
func (f *Formatter) Executable() string {
return f.executable
}
func (f *Formatter) Name() string {
return f.name
}
func (f *Formatter) Priority() int {
return f.config.Priority
}
func (f *Formatter) Apply(ctx context.Context, tasks []*Task) error {
start := time.Now()
// construct args, starting with config
args := f.config.Options
// exit early if nothing to process
if len(tasks) == 0 {
return nil
}
// append paths to the args
for _, task := range tasks {
args = append(args, task.File.RelPath)
}
// execute the command
cmd := exec.CommandContext(ctx, f.executable, args...)
cmd.Dir = f.workingDir
// log out the command being executed
f.log.Debugf("executing: %s", cmd.String())
if out, err := cmd.CombinedOutput(); err != nil {
if len(out) > 0 {
_, _ = fmt.Fprintf(os.Stderr, "%s error:\n%s\n", f.name, out)
}
return fmt.Errorf("formatter '%s' with options '%v' failed to apply: %w", f.config.Command, f.config.Options, err)
}
//
f.log.Infof("%v files processed in %v", len(tasks), time.Now().Sub(start))
return nil
}
// Wants is used to test if a Formatter wants a path based on it's configured Includes and Excludes patterns.
// Returns true if the Formatter should be applied to path, false otherwise.
func (f *Formatter) Wants(file *walk.File) bool {
match := !PathMatches(file.RelPath, f.excludes) && PathMatches(file.RelPath, f.includes)
if match {
f.log.Debugf("match: %v", file)
}
return match
}
// NewFormatter is used to create a new Formatter.
func NewFormatter(
name string,
treeRoot string,
cfg *config.Formatter,
globalExcludes []glob.Glob,
) (*Formatter, error) {
var err error
f := Formatter{}
// capture config and the formatter's name
f.name = name
f.config = cfg
f.workingDir = treeRoot
// test if the formatter is available
executable, err := exec.LookPath(cfg.Command)
if errors.Is(err, exec.ErrNotFound) {
return nil, ErrCommandNotFound
} else if err != nil {
return nil, err
}
f.executable = executable
// initialise internal state
if cfg.Priority > 0 {
f.log = log.WithPrefix(fmt.Sprintf("format | %s[%d]", name, cfg.Priority))
} else {
f.log = log.WithPrefix(fmt.Sprintf("format | %s", name))
}
f.includes, err = CompileGlobs(cfg.Includes)
if err != nil {
return nil, fmt.Errorf("failed to compile formatter '%v' includes: %w", f.name, err)
}
f.excludes, err = CompileGlobs(cfg.Excludes)
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
}