fix: resolve literal file and directory args without directory traversal - #1207
Merged
Conversation
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.
There was a problem hiding this comment.
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.
…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
commented
Jul 22, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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).dprint fmt ..) behaves like running dprint from that directory, including discovering sibling directories' config files.--config/in-use global config, else the user's global config, else error.[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.--stdinand the editor service now apply gitignore/exclusion rules the same wayfmt <path>does.Perf
In the DefinitelyTyped repo:
output-file-paths, 20k files)types/node/package.json)types/node)types/re*/*.json, 1.6k files)Supersedes #1205
Closes #920
Closes #552
Closes #947
Closes #1111