Skip to content

Commit 176a18f

Browse files
committed
feat: include/exclude by detected MIME type
Intended for cases like scripts, which may lack extensions indicating their file/language type.
1 parent 23c7fee commit 176a18f

File tree

18 files changed

+457
-138
lines changed

18 files changed

+457
-138
lines changed

cmd/init/init.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ options = []
6161
includes = [ "*.<language-extension>" ]
6262
# Glob patterns of files to exclude
6363
excludes = []
64+
# MIME types of files to include
65+
allowed-mimetypes = [ "text/x-mylanguage" ]
66+
# MIME types of files to exclude
67+
disallowed-mimetypes = [ "application/json" ]
6468
# Controls the order of application when multiple formatters match the same file
6569
# Lower the number, the higher the precedence
6670
# Default is 0

config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Config struct {
2929
CI bool `mapstructure:"ci" toml:"-"` // not allowed in config
3030
ClearCache bool `mapstructure:"clear-cache" toml:"-"` // not allowed in config
3131
CPUProfile string `mapstructure:"cpu-profile" toml:"cpu-profile,omitempty"`
32+
DisallowedMimetypes []string `mapstructure:"disallowed-mimetypes" toml:"excludes,omitempty"`
3233
Excludes []string `mapstructure:"excludes" toml:"excludes,omitempty"`
3334
FailOnChange bool `mapstructure:"fail-on-change" toml:"fail-on-change,omitempty"`
3435
Formatters []string `mapstructure:"formatters" toml:"formatters,omitempty"`
@@ -56,6 +57,11 @@ type Formatter struct {
5657
Command string `mapstructure:"command" toml:"command"`
5758
// Options are an optional list of args to be passed to Command.
5859
Options []string `mapstructure:"options,omitempty" toml:"options,omitempty"`
60+
// AllowedMimetypes is an optional list of MIME types used to determine whether this Formatter
61+
// should be applied against a path.
62+
AllowedMimetypes []string `mapstructure:"allowed-mimetypes,omitempty" toml:"includes,omitempty"`
63+
// DisallowedMimetypes is an optional list of MIME types used to exclude certain files from this Formatter.
64+
DisallowedMimetypes []string `mapstructure:"disallowed-mimetypes,omitempty" toml:"includes,omitempty"`
5965
// Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path.
6066
Includes []string `mapstructure:"includes,omitempty" toml:"includes,omitempty"`
6167
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.
@@ -86,6 +92,10 @@ func SetFlags(fs *pflag.FlagSet) {
8692
"cpu-profile", "",
8793
"The file into which a cpu profile will be written. (env $TREEFMT_CPU_PROFILE)",
8894
)
95+
fs.StringSlice(
96+
"disallowed-mimetypes", nil,
97+
"Exclude files matching the specified MIME types. (env $TREEFMT_DISALLOWED_MIMETYPES)",
98+
)
8999
fs.StringSlice(
90100
"excludes", nil,
91101
"Exclude files or directories matching the specified globs. (env $TREEFMT_EXCLUDES)",

docs/site/getting-started/configure.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[MIME types]: https://github.com/gabriel-vasile/mimetype/blob/17b303270b920bc619feadef12cad28d70fcb6e0/supported_mimes.md
2+
13
# Configure
24

35
`treefmt`'s behaviour can be influenced in one of three ways:
@@ -127,6 +129,28 @@ The file into which a [pprof](https://github.com/google/pprof) cpu profile will
127129
cpu-profile = "./cpu.pprof"
128130
```
129131

132+
### `disallowed-mimetypes`
133+
134+
An optional list of [MIME types][] used to exclude files from all formatters.
135+
136+
=== "Flag"
137+
138+
```console
139+
treefmt --disallowed-mimetypes application/json,image/png
140+
```
141+
142+
=== "Env"
143+
144+
```console
145+
TREEFMT_DISALLOWED_MIMETYPES="application/json,image/png" treefmt
146+
```
147+
148+
=== "Config"
149+
150+
```toml
151+
disallowed-mimetypes = ["application/json", "image/png"]
152+
```
153+
130154
### `excludes`
131155

132156
An optional list of [glob patterns](#glob-patterns-format) used to exclude files from all formatters.
@@ -450,6 +474,11 @@ command = "deadnix"
450474
options = ["-e"]
451475
includes = ["*.nix"]
452476
priority = 2
477+
478+
[formatter.shfmt]
479+
command = "shfmt"
480+
options = ["-i", "2", "-w"]
481+
allowed-mimetypes = ["text/x-shellscript"]
453482
```
454483

455484
### `command`
@@ -462,19 +491,27 @@ An optional list of args to be passed to `command`.
462491

463492
### `includes`
464493

465-
A list of [glob patterns](#glob-patterns-format) used to determine whether the formatter should be applied against a given path.
494+
An optional list of [glob patterns](#glob-patterns-format) used to determine whether the formatter should be applied against a given path.
466495

467496
### `excludes`
468497

469498
An optional list of [glob patterns](#glob-patterns-format) used to exclude certain files from this formatter.
470499

500+
### `allowed-mimetypes`
501+
502+
An optional list of [MIME types][] used to determine whether the formatter should be applied against a given path.
503+
504+
### `disallowed-mimetypes`
505+
506+
An optional list of [MIME types][] used to exclude certain files from this formatter.
507+
471508
### `priority`
472509

473510
Influences the order of execution. Greater precedence is given to lower numbers, with the default being `0`.
474511

475512
## Same file, multiple formatters?
476513

477-
For each file, `treefmt` determines a list of formatters based on the configured `includes` / `excludes` rules. This list is
514+
For each file, `treefmt` determines a list of formatters based on the configured `includes` / `excludes` and `allowed-mimetypes` / `disallowed-mimetypes` rules. This list is
478515
then sorted, first by priority (lower the value, higher the precedence) and secondly by formatter name (lexicographically).
479516

480517
The resultant sequence of formatters is used to create a batch key, and similarly matched files get added to that batch
@@ -486,6 +523,10 @@ Another consequence is that formatting is deterministic for a given file and a g
486523
By setting the priority fields appropriately, you can control the order in which those formatters are applied for any
487524
files they _both happen to match on_.
488525

526+
## Required settings
527+
528+
Each formatter must have at least one entry in the `includes` list **or** the `allowed-mimetypes` list. This is the minimal requirement for `treefmt` to identify which files are subject to a given formatter.
529+
489530
## Glob patterns format
490531

491532
This is a variant of the Unix glob pattern. It supports all the usual

docs/site/guides/unmatched-formatters.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ $ treefmt --on-unmatched warn
3636
### Enforcing Strict Matching
3737

3838
Another stricter policy approach is to fail the run if any unmatched files are found.
39-
This can be paired with an `excludes` list to ignore specific files:
39+
This can be paired with `excludes` and/or `disallowed-mimetypes` lists to ignore specific files:
4040

4141
`treefmt.toml`:
4242

@@ -49,4 +49,10 @@ excludes = [
4949
"LICENCE",
5050
"go.sum",
5151
]
52+
53+
# List MIME types to explicity ignore
54+
disallowed-mimetypes = [
55+
"image/x-portable-pixmap",
56+
"text/rtf",
57+
]
5258
```

format/composite.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"slices"
1010

1111
"github.com/charmbracelet/log"
12-
"github.com/gobwas/glob"
1312
"github.com/numtide/treefmt/v2/config"
1413
"github.com/numtide/treefmt/v2/stats"
1514
"github.com/numtide/treefmt/v2/walk"
@@ -25,9 +24,9 @@ var ErrFormattingFailures = errors.New("formatting failures detected")
2524
// CompositeFormatter handles the application of multiple Formatter instances based on global excludes and individual
2625
// formatter configuration.
2726
type CompositeFormatter struct {
28-
cfg *config.Config
29-
stats *stats.Stats
30-
globalExcludes []glob.Glob
27+
cfg *config.Config
28+
stats *stats.Stats
29+
globalMatcher *CompositeMatcher
3130

3231
unmatchedLevel log.Level
3332

@@ -36,9 +35,9 @@ type CompositeFormatter struct {
3635
}
3736

3837
// match filters the file against global excludes and returns a list of formatters that want to process the file.
39-
func (c *CompositeFormatter) match(file *walk.File) (bool, []*Formatter) {
38+
func (c *CompositeFormatter) match(file *walk.File, cache MatcherCache) (bool, []*Formatter) {
4039
// first check if this file has been globally excluded
41-
if pathMatches(file.RelPath, c.globalExcludes) {
40+
if !c.globalMatcher.Wants(file.RelPath, cache) {
4241
log.Debugf("path matched global excludes: %s", file.RelPath)
4342

4443
return true, nil
@@ -49,7 +48,7 @@ func (c *CompositeFormatter) match(file *walk.File) (bool, []*Formatter) {
4948

5049
// iterate the formatters, recording which are interested in this file
5150
for _, formatter := range c.formatters {
52-
if formatter.Wants(file) {
51+
if formatter.Wants(file, cache) {
5352
matches = append(matches, formatter)
5453
}
5554
}
@@ -61,9 +60,11 @@ func (c *CompositeFormatter) match(file *walk.File) (bool, []*Formatter) {
6160
func (c *CompositeFormatter) Apply(ctx context.Context, files []*walk.File) error {
6261
var toRelease []*walk.File
6362

63+
cache := NewMatcherCache()
64+
6465
for _, file := range files {
6566
// match the file against the formatters
66-
globalExclude, matches := c.match(file)
67+
globalExclude, matches := c.match(file, cache)
6768

6869
// if the file is globally excluded, we do not emit a warning
6970
if globalExclude {
@@ -147,12 +148,23 @@ func NewCompositeFormatter(
147148
statz *stats.Stats,
148149
batchSize int,
149150
) (*CompositeFormatter, error) {
151+
matchers := make([]Matcher, 2)
152+
153+
var err error
154+
150155
// compile global exclude globs
151-
globalExcludes, err := compileGlobs(cfg.Excludes)
156+
matchers[0], err = NewGlobExclusionMatcher(cfg.Excludes)
152157
if err != nil {
153158
return nil, fmt.Errorf("failed to compile global excludes: %w", err)
154159
}
155160

161+
matchers[1], err = NewMimetypeExclusionMatcher(cfg.DisallowedMimetypes)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to assemble global disallowed MIME types: %w", err)
164+
}
165+
166+
globalMatcher := NewCompositeMatcher(matchers)
167+
156168
// parse unmatched log level
157169
unmatchedLevel, err := log.ParseLevel(cfg.OnUnmatched)
158170
if err != nil {
@@ -191,7 +203,7 @@ func NewCompositeFormatter(
191203
return &CompositeFormatter{
192204
cfg: cfg,
193205
stats: statz,
194-
globalExcludes: globalExcludes,
206+
globalMatcher: globalMatcher,
195207
unmatchedLevel: unmatchedLevel,
196208

197209
scheduler: scheduler,

format/formatter.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"time"
1414

1515
"github.com/charmbracelet/log"
16-
"github.com/gobwas/glob"
1716
"github.com/numtide/treefmt/v2/config"
1817
"github.com/numtide/treefmt/v2/walk"
1918
"mvdan.cc/sh/v3/expand"
@@ -37,9 +36,8 @@ type Formatter struct {
3736
executable string // path to the executable described by Command
3837
workingDir string
3938

40-
// internal, compiled versions of Includes and Excludes.
41-
includes []glob.Glob
42-
excludes []glob.Glob
39+
// internal, compiled versions of inclusion and exclusion matchers.
40+
matcher *CompositeMatcher
4341
}
4442

4543
func (f *Formatter) Name() string {
@@ -123,11 +121,12 @@ func (f *Formatter) Apply(ctx context.Context, files []*walk.File) error {
123121
return nil
124122
}
125123

126-
// Wants is used to determine if a Formatter wants to process a path based on it's configured Includes and Excludes
127-
// patterns.
124+
// Wants is used to determine if a Formatter wants to process a path based on
125+
// its configured Includes and Excludes patterns, plus its AllowedMimetypes and
126+
// DisallowedMimetypes MIME types.
128127
// Returns true if the Formatter should be applied to file, false otherwise.
129-
func (f *Formatter) Wants(file *walk.File) bool {
130-
match := !pathMatches(file.RelPath, f.excludes) && pathMatches(file.RelPath, f.includes)
128+
func (f *Formatter) Wants(file *walk.File, cache MatcherCache) bool {
129+
match := f.matcher.Wants(file.RelPath, cache)
131130
if match {
132131
f.log.Debugf("match: %v", file)
133132
}
@@ -171,20 +170,34 @@ func newFormatter(
171170
f.log = log.WithPrefix("formatter | " + name)
172171
}
173172

174-
// check there is at least one include
175-
if len(cfg.Includes) == 0 {
176-
return nil, fmt.Errorf("formatter '%v' has no includes", f.name)
173+
// check there is at least one include or allowed MIME type
174+
if len(cfg.Includes) == 0 && len(cfg.AllowedMimetypes) == 0 {
175+
return nil, fmt.Errorf("formatter '%v' has no includes or allowed MIME types", f.name)
177176
}
178177

179-
f.includes, err = compileGlobs(cfg.Includes)
178+
matchers := make([]Matcher, 4)
179+
180+
matchers[0], err = NewGlobInclusionMatcher(cfg.Includes)
180181
if err != nil {
181182
return nil, fmt.Errorf("failed to compile formatter '%v' includes: %w", f.name, err)
182183
}
183184

184-
f.excludes, err = compileGlobs(cfg.Excludes)
185+
matchers[1], err = NewGlobExclusionMatcher(cfg.Excludes)
185186
if err != nil {
186187
return nil, fmt.Errorf("failed to compile formatter '%v' excludes: %w", f.name, err)
187188
}
188189

190+
matchers[2], err = NewMimetypeInclusionMatcher(cfg.AllowedMimetypes)
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to assemble formatter '%v' allowed MIME types: %w", f.name, err)
193+
}
194+
195+
matchers[3], err = NewMimetypeExclusionMatcher(cfg.DisallowedMimetypes)
196+
if err != nil {
197+
return nil, fmt.Errorf("failed to assemble formatter '%v' disallowed MIME types: %w", f.name, err)
198+
}
199+
200+
f.matcher = NewCompositeMatcher(matchers)
201+
189202
return &f, nil
190203
}

format/glob.go

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)