name: dart-use-path-package
description: >-
Cross-platform file and directory path manipulation, segment splitting, extension extraction, and context conversion using package:path and package:file. Use when writing, inspecting, joining, splitting, or refactoring file paths, directory names, or extensions, or replacing raw string path operations (.split('/'), '$dir/$file', .endsWith('.ext'), .replaceAll('\\', '/')). Don't use for HTTP network URI routing, database query strings, or non-path string processing.
metadata:
model: models/gemini-3.1-pro-preview
last_modified: Wed, 23 Sep 2026 22:10:00 GMT
Safe Cross-Platform Path Manipulation in Dart
Contents
1. Core Principles & Cross-Platform Rules
Avoid Treating File Paths as Raw Strings
- Native file paths on Windows use backslashes (
\), whereas macOS and Linux use forward slashes (/).
- String operations like
.contains('foo/'), .startsWith('foo/'), or .split('/') silently fail on Windows native paths.
- String interpolation like
'$dir/$file' injects forward slashes on Windows and produces duplicate slashes (//) when $dir ends with a trailing slash.
Rule: Always decompose paths into segments using p.split(path) before inspecting directory hierarchy or segment names, and always join path components using p.join(...).
Pragmatic Boundary Joining vs. Multi-Segment Decomposition (p.join)
- Cross-Platform Libraries (Windows + POSIX): Pass individual path segments to
p.join(dir, 'sub', 'file.json') so package:path inserts OS-native separators (\ on Windows, / on POSIX) between every component.
- POSIX-Only Tools & Static Subpath Greppability: In codebases exclusively targeting Linux/macOS (or when joining a dynamic base path to a known static subpath), decomposing 5–6 static segments into separate arguments (
p.join(home, '.local', 'share', 'app', 'bin', 'config.json')) causes dart format to wrap across 6–8 vertical lines and destroys substring greppability (grep / code_search for .local/share/app/bin).
- Rule for POSIX Targets: Prefer 2-argument boundary joining (
p.join(home, '.local/share/app/bin/config.json')). This prevents duplicate-slash bugs (//) at variable boundaries while preserving single-line readability and exact string searchability.
Normalization vs. Canonicalization (p.normalize vs. p.canonicalize)
p.normalize(path) resolves . and .. segments purely lexically without consulting the filesystem or standardizing case.
- When deduplicating directory paths or comparing physical file identity across symlinks, relative roots, or case-insensitive filesystems, use
p.canonicalize(path).
Strip Location Specifiers & Convert URIs Safely
- Strings formatted as
<path>:<line>-<col> or <path>:<line> are not pure file paths. Passing them directly to p.normalize or Uri.parse causes bugs (on Windows, Uri.parse mistakes C: for a URI scheme and :line for a port).
- Extract the trailing
:line-col suffix via regular expression (RegExp(r'^(.*?):(\d+(?:-\d+)?)#x27;)) before passing the file path to package:path.
- URI Boundary Conversions: When converting between file paths and
Uri objects, always use p.toUri(path) and p.fromUri(uri) rather than Uri.parse(path) or manual string concatenation.
2. Recommended package:path Idioms vs. String Anti-Patterns
Path Joining
- Prefer:
p.join(dir, file)
- Avoid:
'$dir/$file' or 'a/$b'
- Why: String interpolation injects
/ on Windows and creates duplicate
slashes (//) when $dir ends with a trailing separator.
Segment Matching
- Prefer:
p.split(path).contains('foo')
- Avoid:
path.contains('foo/')
- Why: String matching fails on Windows backslashes (
foo\bar) and produces
false positives on partial substring names (e.g. barfoo/).
Root and Directory Prefixes
- Prefer:
if (p.split(path) case ['foo', ...]) (or case ['foo', ...final rest] when extracting tail segments), or p.isWithin('foo', path)
- Avoid:
path.startsWith('foo/') or p.split(path).first == 'foo'
- Why: String prefix matching fails on Windows separators (
foo\bar). Calling
p.split(path).first throws a StateError on empty lists and requires
separate .skip(1) slicing, whereas list patterns safely check non-emptiness,
match multi-segment prefixes, and optionally bind ...final rest in a single
step. When unnormalized relative prefixes like ./foo/bar may appear, use
p.isWithin('foo', path) (or p.split(p.normalize(path))).
File Extensions
- Prefer:
p.extension(path) == '.wasm'
- Avoid:
path.endsWith('.wasm')
- Why: Substring suffix matching falsely matches directories (
foo.wasm/)
or non-extension suffixes.
Extension Slicing and Compound Extensions
- Prefer:
p.withoutExtension(path) and p.extension(path, 2)
- Avoid:
path.lastIndexOf('.') and manual substring slicing
- Why: Manual arithmetic breaks on hidden dotfiles (
.gitignore) and
compound extensions (.js.map, .tar.gz).
POSIX and URL Path Conversion
- Prefer:
p.posix.joinAll(p.split(path)) or p.url.joinAll(p.split(path))
- Avoid:
path.replaceAll(r'\', '/')
- Why: Ad-hoc separator replacement fails on root drives and mixes OS
context with POSIX or URL targets.
URI Conversion
- Prefer:
p.toUri(path) and p.fromUri(uri)
- Avoid:
Uri.parse(path) and uri.path
- Why: Direct URI parsing fails on Windows drive letters (
C:) and leaks
percent-encoding (e.g. %20 for spaces).
Directory Basename Helper
- Prefer:
String canonicalDirName(Directory d) => p.basename(p.normalize(d.absolute.path));
- Avoid: Repeating
p.basename(p.normalize(dir.absolute.path)) inline
across files.
- Why: Centralizes canonical directory naming logic and reduces boilerplate.
3. Bridging Native Paths to POSIX, Git, & URL Contexts
Avoid calling .replaceAll('\\', '/') or .replaceAll(r'\', '/') to convert
OS-native paths into POSIX paths (for Git, YAML, archive manifests) or URL
segments.
Rule: Split the relative native path using p.split(...), inspect segments
with Dart 3 list pattern matching, and join using p.posix.joinAll(...) or
p.url.joinAll(...). Always call p.relative(filePath, from: root) first so
leading root segments ('/' on POSIX or r'C:\' on Windows) do not interfere
with relative prefix patterns:
import 'package:path/path.dart' as p;
String computeWebAssetKey(String filePath, String projectRoot) {
final relative = p.relative(filePath, from: projectRoot);
final segments = p.split(relative);
return switch (segments) {
['assets', ...] => p.posix.joinAll(segments),
_ => p.posix.joinAll(['assets', ...segments]),
};
}
Git Paths and Repository Metadata
- Git repository tree objects,
.gitignore pattern rules, .gitattributes,
and git-tracked symlinks strictly use POSIX forward slashes (/), even on
Windows.
- Inserting native Windows backslashes (
\) into .gitignore or git commands
causes Git to treat \ as an escape character rather than a directory
separator, silently breaking pattern matching.
- When generating
.gitignore entries, repository manifests, or symlink
targets programmatically from native file paths, convert the relative native
path using p.posix.joinAll(p.split(relativePath)) or p.posix.join(...).
4. Mockable File Systems (package:file vs. Global p.*)
In codebases that use package:file (e.g., CLI applications or services tested
with MemoryFileSystem), avoid calling top-level p.* functions on File or
Directory paths.
- Top-level
p.* functions bind to the host operating system running the test.
- If a unit test creates a
MemoryFileSystem(style: FileSystemStyle.windows) on a Linux or macOS runner, global p.split(file.path) will split on / instead of \, breaking the test.
Rule: Always use the Context attached to the FileSystem (file.fileSystem.path):
import 'package:file/file.dart';
List<String> listSubdirectoryNames(Directory dir) {
final pathContext = dir.fileSystem.path;
return dir
.listSync()
.whereType<Directory>()
.map((d) => pathContext.basename(d.path))
.toList();
}
5. Extensions, Compound Extensions & Stem Extraction
Avoid manual .lastIndexOf('.') and .substring() arithmetic when extracting file extensions or inserting content hashes. p.extension natively supports multi-level extensions via its optional level parameter.
- Multi-Dot Stem Nuance: Calling
p.extension('main.dart.wasm', 2) returns '.dart.wasm' because it blindly captures the last two dot-separated segments. When hashing or stripping extensions on files that may have multi-dot stems (e.g., main.dart.wasm vs. main.dart.js.map), check whether p.extension(filename, 2) matches a known compound extension (or .endsWith('.map')) before falling back to single-level p.extension(filename):
import 'package:path/path.dart' as p;
String insertContentHash(String filename, String hash) {
final compoundExt = p.extension(filename, 2);
// Only use the 2-level extension for true compound suffixes (e.g., '.js.map')
final ext = compoundExt.endsWith('.map')
? compoundExt
: p.extension(filename);
final stem = filename.substring(0, filename.length - ext.length);
return '$stem.$hash$ext';
}
6. Workflows & Audit Checklist
Path Refactoring Checklist
References & Examples