feat(fmt): infer config from .editorconfig (#34071)
## Summary
`deno fmt` now reads `.editorconfig` files and uses their settings to
fill in fmt config fields that aren't otherwise set. This is the
long-standing ask in #14717.
Precedence (highest to lowest):
1. CLI flags (`--indent-width`, `--use-tabs`, ...)
2. `deno.json` `fmt` block
3. `.editorconfig`
4. Built-in defaults
So `.editorconfig` only fills in fields the user hasn't already
configured. Mappings:
| `.editorconfig` | Deno fmt |
| ----------------- | ----------------- |
| `indent_style` | `useTabs` |
| `indent_size` | `indentWidth` |
| `tab_width` | `indentWidth` (fallback when `indent_style = tab` and
`indent_size` is unset) |
| `max_line_length` | `lineWidth` (ignored when `off`) |
| `end_of_line` | `newLineKind` (`lf`/`crlf`) |
`.editorconfig` resolution walks up from the file being formatted,
parsing each file it finds, and stops at the first one with `root =
true` (or at the filesystem root). Sections farther from the file are applied
first so nearer files override them — matching the editorconfig spec.
A small glob-to-regex translator handles the section header patterns:
`*`, `**`, `**/`, `?`, `[abc]`, `[!abc]`, `{a,b,c}`, and `{n..m}`. The
`**/foo.ts` form is treated as matching `foo.ts` at any depth including
the root, matching gitignore-style user expectations.
Parsed `.editorconfig` files are cached per-fmt-run via
`EditorConfigCache`, so repeated lookups within a batch do not re-read
or re-parse them. When a file is found, fmt logs `Found .editorconfig at
<path> and using it` at `debug` level (visible with `-L debug`), once
per discovered file.
### Incremental cache
The fmt incremental cache is keyed on file content plus the batch-level
fmt options. Because `.editorconfig` resolves per-file options that the
batch-level key does not capture, those resolved options are folded into
the cached hash for each file. Editing an `.editorconfig` value
therefore invalidates the cached "already formatted" result even when the file
body is unchanged, so a subsequent `--check` re-evaluates the file rather
than returning a stale pass. Files not governed by any `.editorconfig` keep
their existing cache entries and hash the file text as-is (no extra
allocation).
### Test coverage
- `cli/tools/fmt_editorconfig.rs` — 16 unit tests covering the parser,
glob translation, and `apply_to`/precedence behavior.
- `tests/specs/fmt/editorconfig` — 7 spec tests:
- `infers_indent_size` — `.editorconfig` sets `indent_size = 4` and
`fmt --check` passes on a 4-space file.
- `infers_indent_size_negative` — `--indent-width=2` on the CLI
overrides `.editorconfig`, so check fails for the same file.
- `infers_use_tabs` — `indent_style = tab` in a subdirectory.
- `infers_max_line_length` — `max_line_length = 200` lets a long line
through that would otherwise wrap.
- `deno_json_takes_precedence` — explicit `indentWidth: 2` in
`deno.json` is not overridden by `.editorconfig`'s `indent_size = 4`.
- `nested_overrides_parent` — a nearer non-root `.editorconfig`
overrides a farther `root = true` one (exercises the walk-up and
nearer-overrides-farther merge order).
- `logs_at_debug` — `-L debug` prints the "found and using it" notice.
### Robustness
The `.editorconfig` glob translator and value parser degrade gracefully
on malformed or adversarial input rather than crashing:
- Numeric range `{n..m}` expansion is bounded, so a pathological span
such as `{1..1000000000}` does not build a giant regex; oversized
ranges fall back to a literal that simply will not match.
- Brace-nesting recursion is depth-capped, so deeply nested alternations
like `{a,{a,{a,...}}}` cannot overflow the stack.
- `indent_size` / `tab_width` / `max_line_length` parse with saturating
integer conversion, clamping out-of-range values instead of silently
dropping them.
- Literal brace/bracket characters are escaped so unbalanced or degraded
patterns still compile to a valid regex (a section that fails to
compile is simply inert).
Closes #14717
---------
Co-authored-by: lunadogbot <lunadogbot@users.noreply.github.com>
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>