Skip to content

fix: resolve literal file and directory args without directory traversal - #1207

Merged
dsherret merged 10 commits into
mainfrom
literal-cli-paths
Jul 28, 2026
Merged

fix: resolve literal file and directory args without directory traversal#1207
dsherret merged 10 commits into
mainfrom
literal-cli-paths

Conversation

@dsherret

@dsherret dsherret commented Jul 22, 2026

Copy link
Copy Markdown
Member

Resolves literal (non-glob) CLI path args directly instead of discovering them via directory traversal, and makes file inclusion/exclusion behave the same regardless of the cwd, path spelling, and arg form (literal path vs glob vs traversal).

Behaviour

  • Literal file args resolve without a traversal. dprint fmt a.ts b.ts … stats and formats those paths directly. Exclusions still apply the same way a traversal would apply them: the file's ancestor directories are checked top down, so a file inside a config-excluded or gitignored directory is not formatted even when explicitly specified (explicitly specifying a file still overrides a gitignore entry for the file itself).
  • Directory args format everything within the directory. An arg at or above the config directory widens to the whole scope, and an ancestor directory arg (ex. dprint fmt ..) behaves like running dprint from that directory, including discovering sibling directories' config files.
  • Paths outside the config's directory now work. They use the config found in their own ancestor tree (nearest config wins), else an explicit --config/in-use global config, else the user's global config, else error.
  • Glob-like args that name an existing path are treated as that path (Fails to match svelte route files that contain bracket parameters #552, Files to format are not found if the path contains square brackets #920, Unable to format or exclude files which contain curly braces in the path #947). This only applies to args whose glob characters are valid in file names ([ and {); args with * or ? are always globs. Applies to positional args, negated args, --excludes, --stdin, and the override flags. Config file patterns intentionally keep pure glob semantics—use character classes there (ex. [{]), which is now documented.
  • Paths are canonicalized before matching, so symlink/junction and differently-cased spellings resolve to the same result.
  • --stdin and the editor service now apply gitignore/exclusion rules the same way fmt <path> does.

Perf

In the DefinitelyTyped repo:

Scenario this PR main 0.55.2
Full traversal (output-file-paths, 20k files) 126.8 ms 125.2 ms 123.9 ms
Single file arg (types/node/package.json) 10.9 ms 18.3 ms 18.3 ms
500 file args 325 ms 1,260 ms 1,040 ms
Dir arg (types/node) 20.1 ms 22.0 ms n/a¹
Glob arg (types/re*/*.json, 1.6k files) 70.5 ms 70.7 ms 72.7 ms

Supersedes #1205

Closes #920
Closes #552
Closes #947
Closes #1111

dsherret added 3 commits July 22, 2026 12:46
Adds `Environment::path_kind` which stats a path in a single call,
returning whether it's a file, directory, or symlink, or `None` when
the path doesn't exist. Literal CLI arg resolution previously called
`path_is_file` and then `path_exists` back to back, statting the same
path twice for directories and nonexistent paths.
`path_exists` stays as a helper on `Environment`, but as a provided
method so environments only implement the `path_kind` stat primitive.
A broken symlink now counts as existing.
@dsherret
dsherret marked this pull request as draft July 22, 2026 17:10
@dsherret
dsherret requested a review from Copilot July 22, 2026 17:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates dprint’s file-resolution pipeline so explicitly provided literal CLI paths (files/directories and “glob-looking” names like routes/[id].svelte) can be resolved directly against the filesystem—avoiding unnecessary directory traversal and enabling correct handling of paths/patterns outside the config base directory.

Changes:

  • Resolve literal CLI args via filesystem stat/canonicalization, expand literal directories to /**, and only traverse when necessary.
  • Add robust escaping/unescaping utilities so glob metacharacters in real filenames are treated literally (including gitignore override paths).
  • Introduce “outside base” path/pattern handling that resolves those paths using an appropriate governing config (tree config, explicit/global config, or error/warn).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/dprint/src/utils/glob/glob.rs Extracts literal CLI paths, optionally skips traversal, and returns outside-base path work for higher-level resolution.
crates/dprint/src/utils/glob/glob_utils.rs Expands pattern detection ([), and adds glob escape/unescape helpers (including CLI-safe escaping).
crates/dprint/src/utils/glob/glob_pattern.rs Uses unescape/escape when mapping patterns and deriving include paths / rebasing bases.
crates/dprint/src/utils/glob/glob_matcher.rs Adjusts ignore behavior for ancestor dirs and stores escaped-literal paths correctly.
crates/dprint/src/utils/gitignore.rs Escapes include-override paths so filenames containing glob chars are unignored correctly.
crates/dprint/src/resolution.rs Resolves/partitions “outside base” CLI paths into additional scopes governed by the correct config.
crates/dprint/src/patterns.rs Updates matcher construction and CLI/config override pattern processing to align with new literal-resolution rules.
crates/dprint/src/paths.rs Refactors pattern assembly for staged/dirty paths and bases plugin-derived includes at the config directory.
crates/dprint/src/environment/environment.rs Introduces PathKind and refines existence checks (path_exists default via path_kind).
crates/dprint/src/environment/real_environment.rs Implements path_is_file and path_kind for real filesystem behavior.
crates/dprint/src/environment/test_environment.rs Implements path_is_file and path_kind for tests.
crates/dprint/src/configuration/resolve_config.rs Tracks whether the resolved config is global (ResolvedConfig.is_global).
crates/dprint/src/commands/formatting.rs Updates --stdin matching to use the new matcher signature and adds regression tests.
crates/dprint/src/arg_parser.rs Makes FilePatternArgs clonable to support reuse across additional resolution scopes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/dprint/src/patterns.rs
@dsherret dsherret changed the title fix: resolve literal file and directory args without directory traversal perf: resolve literal file and directory args without directory traversal Jul 22, 2026
@dsherret dsherret changed the title perf: resolve literal file and directory args without directory traversal fix: resolve literal file and directory args without directory traversal Jul 22, 2026
dsherret added 2 commits July 22, 2026 14:06
…t_ignored

The ancestor loop started with the file itself, checking it with
directory semantics (a dir-only gitignore pattern like `sub.ts/` could
ignore a file named `sub.ts`), and passed base-relative paths to the
gitignore tree, which resolves gitignore files by walking the path's
ancestor directories on the file system, so gitignored ancestor
directories weren't detected when the cwd differed from the base.
@dsherret
dsherret marked this pull request as ready for review July 22, 2026 19:44
@dsherret
dsherret requested a review from Copilot July 22, 2026 19:44
Comment thread website/src/config.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment thread website/src/config.md Outdated
dsherret added 3 commits July 22, 2026 15:57
A traversal or literal path arg that encountered the in-use config
file's own directory (ex. the global config file when formatting an
ancestor of its directory) rediscovered that config file as a
descendant config, and the sub scope resolution silently dropped it
because it matched the config already in use, so the files were never
formatted.

Closes #1111
- restore `path_exists` to follow symlinks. Implementing it on top of the
  new lstat-based `path_kind` changed the semantics for every caller (npm
  plugin resolution, plugin cache, incremental file, config discovery) so
  that a broken symlink counted as existing. `path_kind` stays for the glob
  code, which handles symlinks explicitly.
- stop the ancestor walk in `FileMatcher::matches_and_dir_not_ignored` at
  the base directory. `check_dir_chain` excludes the base dir because a
  traversal starts within it rather than descending into it, so a config
  whose base dir was gitignored matched differently via `--stdin` than via
  a normal `fmt`.
- let `output-file-paths` skip an unresolvable path with a warning instead
  of erroring. It's the command users are pointed at to diagnose which
  files dprint is finding, so it should show what it resolved. Added as a
  separate `allow_skipping_paths` so the "no files found" error is
  unchanged.
- only apply the directory-covering rebase in `into_new_base` to excludes.
  An exclude naming a directory covers everything within it, but an include
  naming a directory matches that one path, so mapping it to `**` widened
  include patterns (ex. `./*` becoming `./**` in a descendant base).
- don't treat negated patterns as literal include paths in
  `include_paths`, which emitted a bogus gitignore whitelist entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants