Repository: editorconfig-checker/editorconfig-checker
Pull request #462 (2025-04-20 15:42:40)

feat: update default paths to exclude
I use editorconfig-checker in a variety of different project setups and noticed there's always _something_ I have to ignore on my own. As a result adopting the checker into established projects effectively requires a config file increasing friction.

This gets repetitive very fast. As a result I propose to amend the default exclusions with additional lockfiles / package manager definitions that are outside user control and shouldn't be checked by editorconfig-checker.

To make maintenance (at least in my eyes) somewhat easier I tried sorting the entries and grouping them with inline comments. I can revert this, particularly if the original order had implications I am not aware of.

---

I have a couple of questions here and the list might need some updates based on that:

A lot of definitions start with the `^` character, that forces an exact file name match and makes the default excludes impractical for e.g. mono-repos with nested structures. Is this intended behaviour? What is preferred here?

The `node_modules` exclusion specifically starts with `[\\/]`. What is the purpose of this? Other folders, e.g. `.git` are not excluded this way but just use their name directly.

Are collapsed or expanded regex definitions preferred? See `\\.jpeg$` & `\\.jpg$` vs. `\\.jpe?g$` for example.

The `main_test.go` appears to have at least one obsolete `exclude` directive (see [1](https://github.com/editorconfig-checker/editorconfig-checker/blob/879d4bbaf74fe28858afb4797f69073d2448fab1/cmd/editorconfig-checker/main_test.go#L46) and [2](https://github.com/editorconfig-checker/editorconfig-checker/blob/879d4bbaf74fe28858afb4797f69073d2448fab1/cmd/editorconfig-checker/main_test.go#L55)) where `.git` is excluded manually but it has been part of the default exclude since https://github.com/editorconfig-checker/editorconfig-checker/pull/427. I guess it should be removed here? Additionally, it probably makes sense to add `\\.exe$` to the default exclude as its own pattern as well?

rasa: @Okeanos If this PR is accepted, the [readme](https://github.com/editorconfig-checker/editorconfig-checker/blob/HEAD/README.md?plain=1#L290) will need to be updated too. Perhaps do that in this PR?
klaernie: First of all: order is most definitely not important - everything on that list is excluded, ordering it "the right way" may shave off nanoseconds of the runtime on a directory with many excluded files.

> A lot of definitions start with the ^ character, that forces an exact file name match and makes the default excludes impractical for e.g. mono-repos with nested structures. Is this intended behaviour? What is preferred here?

I think this is mainly caused by the different people adding to this list over the years. We should probably do for unanchored, unless there is a reason to only consider files at the top level - which I think there is none.

> The node_modules exclusion specifically starts with `[\\/]`. What is the purpose of this? Other folders, e.g. `.git` are not excluded this way but just use their name directly.

Since I added `node_modules`, I can tell: I only wanted to excluded folders named `node_modules` exactly - so it is wrapped in path separators. For `.git` the regex actually matches every path ending in `.git` - which I would consider a buggy behaviour, but I do not know if anybody depends on this behaviour, so I would consider fixing this (aka: turning it into `[\\/]\.git[\\/]`) a breaking change.

> Are collapsed or expanded regex definitions preferred? See `\\.jpeg$` & `\\.jpg$` vs. `\\.jpe?g$` for example.

I would consider collapsed regexes beneficial - it requires less steps for the regex engine, so increases performance (theoretically) and definitely improves readability.

> The `main_test.go` appears to have at least one obsolete `exclude` directive (see [1](https://github.com/editorconfig-checker/editorconfig-checker/blob/879d4bbaf74fe28858afb4797f69073d2448fab1/cmd/editorconfig-checker/main_test.go#L46) and [2](https://github.com/editorconfig-checker/editorconfig-checker/blob/879d4bbaf74fe28858afb4797f69073d2448fab1/cmd/editorconfig-checker/main_test.go#L55)) where `.git` is excluded manually but it has been part of the default exclude since #427. I guess it should be removed here? Additionally, it probably makes sense to add `\\.exe$` to the default exclude as its own pattern as well?

Yes to all, good catch. We should probably also exclude other known binary files that are likely to occur in source code directories - e.g. binaries produced by compilers like `.so`, `.dll` and `.pyc`, maybe even `.jar`. In the end the content-type check should catch and exclude them anyway, but running a regex against a name is way cheaper.
rasa: Sorry to blow up this PR, but https://github.com/sindresorhus/binary-extensions/blob/main/binary-extensions.json has a very complete collection of binary extensions. What would be the downside of simply including them all?

Also, can we `filepath.ToSlash()` the path before regex-ing it, so we can use `/` instead of `[\\/]`? 
klaernie: > Sorry to blow up this PR, but https://github.com/sindresorhus/binary-extensions/blob/main/binary-extensions.json has a very complete collection of binary extensions. What would be the downside of simply including them all?

Oh, that is neat! We could add this repository as a git submodule, and goembed the json at compile time. This would reduce the list of excludes we need to maintain drastically, and would also be easy to communicate in the readme. 

> Also, can we `filepath.ToSlash()` the path before regex-ing it, so we can use `/` instead of `[\\/]`?

You are totally right - we already convert each file path in `files.GetRelativePath()` - so we can reduce the regexes to only use `/` as a path separator without any code changes.
Okeanos: Okay, let me try to summarize the discussion and ensure I got everything:

- Regexes shouldn't start with `^` unless definitely necessary in order to better support e.g. mono-repos
- folder regexes should start with `[\\/]` to prevent matches against files of the same name
  - is presumably a breaking change for existing folders in the regex list
- there should be only one regex per match where feasible, e.g. `\\.jpe?g$` instead of \\.jpeg$ and \\.jpg$
- `main_test.go` can be cleaned up
- binary files should be excluded broadly, where necessary via content-type checks but preferably as regex (because it is cheaper)
  - we should look to using https://github.com/sindresorhus/binary-extensions/blob/main/binary-extensions.json as source of binary file names to exclude
- paths should be converted via [`filepath.ToSlash()`](https://pkg.go.dev/path/filepath#ToSlash) before being matched against regexes; this makes the `[\\/]` construct unnecessary and allows writing `/` instead going forward on all platforms.

---

I can take care of:

- `^` matches
- collapsing some regexes
- `main_test.go` cleanup
- adding `[\\/]` to folders (what do we do about the breaking change implication?)

I propose separate PRs for:

- use `filepath.ToSlash()` and simplify `[\\/]` to `/`
- using the external binary extensions list

Does that sound alright?
klaernie: > * paths should be converted via [`filepath.ToSlash()`](https://pkg.go.dev/path/filepath#ToSlash) before being matched against regexes; this makes the `[\\/]` construct unnecessary and allows writing `/` instead going forward on all platforms.

This is not needed, since we already do this. Only the regexes need to be touched, so they only use `/` to match folders.
Okeanos: - [x] remove leading `^`
- [x] collapse regexes (that semantically target the same thing)
- [x] remove unnecessary `[\\/]` and replace it with `/` because paths are normalized to Unix style before being tested
- [x] remove unnecessary excludes in `main_test.go` (added `\\.exe$` as ignore regex)

I didn't touch the leading `/` for folders yet and neither the "advanced binary check list".

Patch:
diff --git a/README.md b/README.md
index 7b52399..64b6a69 100644
--- a/README.md
+++ b/README.md
@@ -271,6 +271,8 @@ If you want to see which files would be checked without checking them you can pa
 
 Note that while `--dry-run` might output absolute paths, the regular expression you write must match the filenames using relative paths from where editorconfig-checker is used. This becomes especially relevant if you need to anchor your regular expression in order to only match files in the top level your checked directory.
 
+Additionally, paths will be normalized to Unix style before matching against the regex list happens. As a result you don't have to write `[\\/]` to account for Windows and Unix path styles but can just use `/` instead.
+
 #### Inline
 
 If you want to exclude a file inline you need a comment on the first line of the file that contains: `editorconfig-checker-disable-file`
@@ -288,52 +290,86 @@ add x y =
 If you choose to [ignore them](#ignoring-default-excludes), these paths are excluded automatically:
 
 ```txt
-"\\.git[\\/]",
-"^\\.jj/",
-"[\\/]node_modules[\\/]",
-"^\\.yarn/",
-"^yarn\\.lock$",
-"^package-lock\\.json$",
-"^composer\\.lock$",
-"^Cargo\\.lock$",
-"^Gemfile\\.lock$",
-"^\\.pnp\\.cjs$",
-"^\\.pnp\\.js$",
-"^\\.pnp\\.loader\\.mjs$",
-"\\.snap$",
-"\\.otf$",
-"\\.woff$",
-"\\.woff2$",
+// source control related files and folders
+"\\.git/",
+"\\.jj/",
+// package manager, generated, & lock files
+// Cargo (Rust)
+"Cargo\\.lock$",
+// Composer (PHP)
+"composer\\.lock$",
+// RubyGems (Ruby)
+"Gemfile\\.lock$",
+// Go Modules (Go)
+"go\\.(mod|sum)$",
+// Gradle (Java)
+"gradle/wrapper/gradle-wrapper\\.properties$",
+"gradlew(\\.bat)?$",
+"(buildscript-)?gradle\\.lockfile?$",
+// Maven (Java)
+"\\.mvn/wrapper/maven-wrapper\\.properties$",
+"\\.mvn/wrapper/MavenWrapperDownloader\\.java$",
+"mvnw(\\.cmd)?$",
+// NodeJS
+"/node_modules/",
+// npm (NodeJS)
+"npm-shrinkwrap\\.json$",
+"package-lock\\.json$",
+// pip (Python)
+"Pipfile\\.lock$",
+// Poetry (Python)
+"poetry\\.lock$",
+// pnpm (NodeJS)
+"pnpm-lock\\.yaml$",
+// Terraform & OpenTofu
+"\\.terraform\\.lock\\.hcl$",
+// uv (Python)
+"uv\\.lock$",
+// yarn (NodeJS)
+"\\.pnp\\.c?js$",
+"\\.pnp\\.loader\\.mjs$",
+"\\.yarn/",
+"yarn\\.lock$",
+// font files
 "\\.eot$",
+"\\.otf$",
 "\\.ttf$",
+"\\.woff2?$",
+// image & video formats
+"\\.avif$",
 "\\.gif$",
+"\\.ico$",
+"\\.jpe?g$",
+"\\.mp4$",
+"\\.p[bgnp]m$",
 "\\.png$",
-"\\.jpg$",
-"\\.jpeg$",
+"\\.svg$",
+"\\.tiff?$",
 "\\.webp$",
-"\\.avif$",
-"\\.pnm$",
-"\\.pbm$",
-"\\.pgm$",
-"\\.ppm$",
-"\\.mp4$",
 "\\.wmv$",
-"\\.svg$",
-"\\.ico$",
+// other binary or container formats
 "\\.bak$",
 "\\.bin$",
+"\\.docx?$",
+"\\.exe$",
 "\\.pdf$",
-"\\.zip$",
-"\\.gz$",
-"\\.tar$",
+"\\.snap$",
+"\\.xlsx?$",
+// archive formats
 "\\.7z$",
 "\\.bz2$",
+"\\.gz$",
+"\\.jar$",
+"\\.tar$",
+"\\.tgz$",
+"\\.war$",
+"\\.zip$",
+// log & (git) patch files
 "\\.log$",
 "\\.patch$",
-"\\.css\\.map$",
-"\\.js\\.map$",
-"min\\.css$",
-"min\\.js$",
+// generated or minified CSS and JavaScript files
+"\\.(css|js)\\.map$",
+"min\\.(css|js)$",
 ```
 
 #### Ignoring Default Excludes
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 095cc19..3e82c2e 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -21,52 +21,86 @@ var DefaultExcludes = strings.Join(defaultExcludes, "|")
 
 // defaultExcludes are an array to produce the correct string from
 var defaultExcludes = []string{
-	"\\.git[\\/]",
-	"^\\.jj/",
-	"[\\/]node_modules[\\/]",
-	"^\\.yarn/",
-	"^yarn\\.lock$",
-	"^package-lock\\.json$",
-	"^composer\\.lock$",
-	"^Cargo\\.lock$",
-	"^Gemfile\\.lock$",
-	"^\\.pnp\\.cjs$",
-	"^\\.pnp\\.js$",
-	"^\\.pnp\\.loader\\.mjs$",
-	"\\.snap$",
-	"\\.otf$",
-	"\\.woff$",
-	"\\.woff2$",
+	// source control related files and folders
+	"\\.git/",
+	"\\.jj/",
+	// package manager, generated, & lock files
+	// Cargo (Rust)
+	"Cargo\\.lock$",
+	// Composer (PHP)
+	"composer\\.lock$",
+	// RubyGems (Ruby)
+	"Gemfile\\.lock$",
+	// Go Modules (Go)
+	"go\\.(mod|sum)$",
+	// Gradle (Java)
+	"gradle/wrapper/gradle-wrapper\\.properties$",
+	"gradlew(\\.bat)?$",
+	"(buildscript-)?gradle\\.lockfile?$",
+	// Maven (Java)
+	"\\.mvn/wrapper/maven-wrapper\\.properties$",
+	"\\.mvn/wrapper/MavenWrapperDownloader\\.java$",
+	"mvnw(\\.cmd)?$",
+	// NodeJS
+	"/node_modules/",
+	// npm (NodeJS)
+	"npm-shrinkwrap\\.json$",
+	"package-lock\\.json$",
+	// pip (Python)
+	"Pipfile\\.lock$",
+	// Poetry (Python)
+	"poetry\\.lock$",
+	// pnpm (NodeJS)
+	"pnpm-lock\\.yaml$",
+	// Terraform & OpenTofu
+	"\\.terraform\\.lock\\.hcl$",
+	// uv (Python)
+	"uv\\.lock$",
+	// yarn (NodeJS)
+	"\\.pnp\\.c?js$",
+	"\\.pnp\\.loader\\.mjs$",
+	"\\.yarn/",
+	"yarn\\.lock$",
+	// font files
 	"\\.eot$",
+	"\\.otf$",
 	"\\.ttf$",
+	"\\.woff2?$",
+	// image & video formats
+	"\\.avif$",
 	"\\.gif$",
+	"\\.ico$",
+	"\\.jpe?g$",
+	"\\.mp4$",
+	"\\.p[bgnp]m$",
 	"\\.png$",
-	"\\.jpg$",
-	"\\.jpeg$",
+	"\\.svg$",
+	"\\.tiff?$",
 	"\\.webp$",
-	"\\.avif$",
-	"\\.pnm$",
-	"\\.pbm$",
-	"\\.pgm$",
-	"\\.ppm$",
-	"\\.mp4$",
 	"\\.wmv$",
-	"\\.svg$",
-	"\\.ico$",
+	// other binary or container formats
 	"\\.bak$",
 	"\\.bin$",
+	"\\.docx?$",
+	"\\.exe$",
 	"\\.pdf$",
-	"\\.zip$",
-	"\\.gz$",
-	"\\.tar$",
+	"\\.snap$",
+	"\\.xlsx?$",
+	// archive formats
 	"\\.7z$",
 	"\\.bz2$",
+	"\\.gz$",
+	"\\.jar$",
+	"\\.tar$",
+	"\\.tgz$",
+	"\\.war$",
+	"\\.zip$",
+	// log & (git) patch files
 	"\\.log$",
 	"\\.patch$",
-	"\\.css\\.map$",
-	"\\.js\\.map$",
-	"min\\.css$",
-	"min\\.js$",
+	// generated or minified CSS and JavaScript files
+	"\\.(css|js)\\.map$",
+	"min\\.(css|js)$",
 }
 
 // keep synced with pkg/validation/validation.go#L20


Test patch:
diff --git a/cmd/editorconfig-checker/main_test.go b/cmd/editorconfig-checker/main_test.go
index 64a02f2..c16cfd1 100644
--- a/cmd/editorconfig-checker/main_test.go
+++ b/cmd/editorconfig-checker/main_test.go
@@ -43,7 +43,7 @@ func runWithArguments(t *testing.T, args ...string) (string, int) {
 
 func TestMainOurCodebase(t *testing.T) {
 	cdRelativeToRepo(t, "")
-	output, lastSeenCode := runWithArguments(t, "--debug", "--verbose", "--exclude", `\.git`, "--exclude", `\.exe$`)
+	output, lastSeenCode := runWithArguments(t, "--debug", "--verbose")
 	if lastSeenCode != exitCodeNormal {
 		t.Errorf("main exited with return code %d, but we expected %d", lastSeenCode, exitCodeNormal)
 		t.Logf("Output:\n%s", output)
@@ -52,7 +52,7 @@ func TestMainOurCodebase(t *testing.T) {
 
 func TestMainMissingExplicitConfig(t *testing.T) {
 	cdRelativeToRepo(t, "")
-	output, lastSeenCode := runWithArguments(t, "--debug", "--verbose", "--exclude", `\.git`, "--exclude", `\.exe$`, "--config", "/nonexistant")
+	output, lastSeenCode := runWithArguments(t, "--debug", "--verbose", "--config", "/nonexistant")
 	if lastSeenCode != exitCodeConfigFileNotFound {
 		t.Errorf("main exited with return code %d, but we expected %d", lastSeenCode, exitCodeConfigFileNotFound)
 		t.Logf("Output:\n%s", output)
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index b30cf95..d978e31 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -86,7 +86,7 @@ func TestGetExcludesAsRegularExpression(t *testing.T) {
 	}
 
 	actual = c.GetExcludesAsRegularExpression()
-	expected = `testfiles|testdata|\.git[\/]|^\.jj/|[\/]node_modules[\/]|^\.yarn/|^yarn\.lock$|^package-lock\.json$|^composer\.lock$|^Cargo\.lock$|^Gemfile\.lock$|^\.pnp\.cjs$|^\.pnp\.js$|^\.pnp\.loader\.mjs$|\.snap$|\.otf$|\.woff$|\.woff2$|\.eot$|\.ttf$|\.gif$|\.png$|\.jpg$|\.jpeg$|\.webp$|\.avif$|\.pnm$|\.pbm$|\.pgm$|\.ppm$|\.mp4$|\.wmv$|\.svg$|\.ico$|\.bak$|\.bin$|\.pdf$|\.zip$|\.gz$|\.tar$|\.7z$|\.bz2$|\.log$|\.patch$|\.css\.map$|\.js\.map$|min\.css$|min\.js$`
+	expected = `testfiles|testdata|\.git/|\.jj/|Cargo\.lock$|composer\.lock$|Gemfile\.lock$|go\.(mod|sum)$|gradle/wrapper/gradle-wrapper\.properties$|gradlew(\.bat)?$|(buildscript-)?gradle\.lockfile?$|\.mvn/wrapper/maven-wrapper\.properties$|\.mvn/wrapper/MavenWrapperDownloader\.java$|mvnw(\.cmd)?$|/node_modules/|npm-shrinkwrap\.json$|package-lock\.json$|Pipfile\.lock$|poetry\.lock$|pnpm-lock\.yaml$|\.terraform\.lock\.hcl$|uv\.lock$|\.pnp\.c?js$|\.pnp\.loader\.mjs$|\.yarn/|yarn\.lock$|\.eot$|\.otf$|\.ttf$|\.woff2?$|\.avif$|\.gif$|\.ico$|\.jpe?g$|\.mp4$|\.p[bgnp]m$|\.png$|\.svg$|\.tiff?$|\.webp$|\.wmv$|\.bak$|\.bin$|\.docx?$|\.exe$|\.pdf$|\.snap$|\.xlsx?$|\.7z$|\.bz2$|\.gz$|\.jar$|\.tar$|\.tgz$|\.war$|\.zip$|\.log$|\.patch$|\.(css|js)\.map$|min\.(css|js)$`
 
 	if actual != expected {
 		t.Errorf("expected %s, got %s", expected, actual)