Skip to content
AZ Tools

EditorConfig Tester

An .editorconfig is read from the file upwards, not from the top of the page downwards. Every directory between a source file and the disk root may hold one; the walk stops only at a file whose preamble says root = true; the closest file wins property by property; and inside a single file the last matching section wins, with no notion of a more specific rule. Four rules pulling in different directions is why "why is this file getting four spaces" is almost never answered by the line you happen to be looking at. This tester takes the config — or several of them, each introduced by a "# path: src/.editorconfig" marker line — and a list of file paths. For every path it prints the resolved property set with the file, the line and the section glob that produced each value, the chain of config files consulted on the way up, and where root = true cut that chain short. Matching is the EditorConfig glob, which is neither fnmatch nor gitignore. A single * stops at a slash and ** crosses one; ? is one non-slash character; [abc] and [!abc] are character classes that stop being classes the moment a slash appears inside them; {js,ts} is alternation while {js} alone matches the literal three characters; {1..10} matches an integer in the range, negatives included, and rejects a leading zero. And the rule that catches nearly everyone: a glob with a slash anywhere except at the end is anchored to the directory of its own .editorconfig, while a glob without a slash matches the bare file name at any depth. Three values in the result are often written nowhere at all. indent_size becomes tab when indent_style is tab and no size was given; tab_width copies indent_size when indent_size is a number; and indent_size takes tab_width when it is the word tab. All three are applied after the merge, so a tab_width in the root file can end up feeding an indent_size written three directories down. Those rows are marked as derived and name the line they came from. The findings panel names what a config file cannot tell you it is doing: a section that matches none of your paths, a value that is set and then beaten everywhere, indent_size with no indent_style, {js, ts} where the space becomes part of the match, a repeated section name, a key outside the nine standard ones, a value its property does not accept, a property written before the first section (which is silently dropped), and a missing root = true, which leaves an .editorconfig in your home directory in play. What it cannot tell you: whether your editor honours any of this. Support is per plugin — max_line_length and several charset values are widely ignored — and unset is handed to the plugin as the literal value unset for it to act on. "Matches nothing" also only means nothing among the paths you pasted, so paste a representative list. Everything runs in your browser; no file is uploaded.

Config files

2

Sections

6

Paths

8

Findings

1

src/app.js

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size2.editorconfig:8 [*]
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width2derived — written in no file.editorconfig:8 [*]copied from indent_size, which had no tab_width beside it
lib/util.js

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size4.editorconfig:20 [lib/**.js]overrides .editorconfig:8
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width4derived — written in no file.editorconfig:20 [lib/**.js]copied from indent_size, which had no tab_width beside it
lib/deep/util.min.js

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size4.editorconfig:20 [lib/**.js]overrides .editorconfig:8
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width4derived — written in no file.editorconfig:20 [lib/**.js]copied from indent_size, which had no tab_width beside it
main.py

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size4.editorconfig:11 [*.{py,rs}]overrides .editorconfig:8
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width4derived — written in no file.editorconfig:11 [*.{py,rs}]copied from indent_size, which had no tab_width beside it
Makefile

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size2.editorconfig:8 [*]
indent_styletab.editorconfig:17 [Makefile]overrides .editorconfig:7
insert_final_newlinetrue.editorconfig:6 [*]
tab_width2derived — written in no file.editorconfig:8 [*]copied from indent_size, which had no tab_width beside it
docs/guide.md

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size2.editorconfig:8 [*]
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width2derived — written in no file.editorconfig:8 [*]copied from indent_size, which had no tab_width beside it
trim_trailing_whitespacefalse.editorconfig:14 [*.{md, txt}]
notes.txt

Search chain: .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size2.editorconfig:8 [*]
indent_stylespace.editorconfig:7 [*]
insert_final_newlinetrue.editorconfig:6 [*]
tab_width2derived — written in no file.editorconfig:8 [*]copied from indent_size, which had no tab_width beside it
vendor/jquery.js

Search chain: vendor/.editorconfig → .editorconfig · stopped by root = true

PropertyValueSet by
charsetutf-8.editorconfig:4 [*]
end_of_linelf.editorconfig:5 [*]
indent_size2.editorconfig:8 [*]
indent_styletabvendor/.editorconfig:2 [*]overrides .editorconfig:7
insert_final_newlinetrue.editorconfig:6 [*]
tab_width4vendor/.editorconfig:3 [*]
trim_trailing_whitespacefalsevendor/.editorconfig:4 [*]
Findings

Each line is a place where the file does something other than what it looks like.

  • .editorconfig:13 — a space inside the braces is part of the string it matches: [*.{md, txt}]

Resolution follows editorconfig-core; section names are case-sensitive. Nothing is uploaded.

How to use

  1. Paste the .editorconfig into the first box. For more than one file, introduce each with a marker line such as "# path: vendor/.editorconfig", and put the project-root file first.
  2. List the paths to test in the second box, one per line, relative to the project root — the directory that holds the top-level .editorconfig.
  3. Read each path's table: the property, the value it resolves to, and the file, line and section glob that set it. A row marked derived was written in no file; the note says which fallback produced it.
  4. Check the search chain above each table: the .editorconfig files consulted, nearest first, and whether root = true stopped the walk or it would carry on above your project.
  5. Work through the findings panel. Every entry points at a file and a line where the config does something other than what it looks like — a dead section, a setting that never wins, a space inside a brace group.

Frequently asked questions

Why does my file get four spaces when [*] says two?
Because something later or nearer won, and EditorConfig has no specificity ranking to appeal to. Inside one file the last matching section wins, so a [*.py] block twenty lines below your [*] block quietly overrides it — and so does a second [*] block further down, which is why a duplicated section name matters. Across files the closest .editorconfig wins property by property, so a two-line file in the directory you are editing beats the careful one at the project root. Order and distance are the whole story, and that is exactly what the "Set by" column reports: the file, the line and the section glob that produced the value, with the settings it beat listed underneath.
What is the difference between [*.js], [lib/*.js] and [**/*.js]?
The presence of a slash, and nothing else. A glob with no slash in it is matched against the file name alone at any depth, so [*.js] covers src/a.js and vendor/dist/b.js alike. As soon as the glob contains a slash anywhere but at the end, it is anchored to the directory of the .editorconfig that holds it: [lib/*.js] matches lib/a.js and not lib/sub/a.js, and not src/lib/a.js either. [**/*.js] is the anchored form that crosses directories, because ** matches across separators where a single * stops at one — and the /**/ in the middle also stands for no directory at all, so it matches a.js as well as lib/sub/a.js. One trap at the end: a trailing slash, as in [lib/], matches nothing, because a file path never ends in a slash. The subtree is [lib/**].
Where did indent_size = tab and tab_width come from? I never wrote them.
From the two fallbacks the core applies after merging everything. If indent_style is tab and no indent_size was set, indent_size becomes the word tab. If indent_size is a number and no tab_width was set, tab_width copies it — which is why a file that only ever mentions indent_size reports a tab_width too. And in the other direction, if indent_size is the word tab and a tab_width does exist, indent_size takes that number. Because this happens on the merged result, the two halves can come from different files: a tab_width in the root config can feed an indent_size written three directories down. unset travels through the same machinery, so indent_size = unset also leaves tab_width unset.
What does root = true actually stop?
The upward search for further .editorconfig files. Without it the walk does not stop at your project: it continues through every parent directory, your home directory and the filesystem root included, and anything it finds there fills in properties your own file did not set. Two details decide whether it works. It must be in the preamble, above the first section header — written inside a [*] block it is just an ordinary property named root and stops nothing at all. And it is read from a file even when none of that file's sections match, so a config whose sections are all wrong still ends the search.
Why does [*.{js, ts}] not apply to my .ts files?
Because the space is part of the pattern. Brace alternation matches the strings between the commas literally, with no trimming anywhere in a section header, so {js, ts} offers "js" and " ts" — and only a file whose name really contains a space before ts matches the second one. Two neighbours of the same trap are worth knowing: {js} with no comma is not alternation at all and matches the literal characters {js}, so [*.{js}] applies to a file called app.{js} and to nothing else; and a numeric range such as {1..10} refuses a leading zero, so 1.txt and 10.txt match while 01.txt does not.
Does a backslash escape a star?
Not in the reference implementation, whatever the prose suggests. The core translates a glob into a regular expression in one pass, and the escape is only honoured for a comma, a closing brace, a # and a ; — the characters that would otherwise end the pattern or split an alternation. A backslash in front of a star or a question mark is dropped and the wildcard still matches anything, so [a\*b.txt] also matches aXb.txt. Worse, \[ and \{ leave the generated expression unbalanced: compiling the glob throws, and the editor plugin usually reacts by ignoring the whole .editorconfig without saying so. This tester reproduces that instead of guessing — the section is reported as an unusable glob. The practical advice is to keep braces, brackets and stars out of section headers.

Related tools