Manipulation sûre de chemins multi-plateformes en Dart
Sommaire
- 1. Principes fondamentaux et règles multi-plateformes
- 2. Idiomes recommandés package:path vs. anti-motifs de chaînes
- 3. Convertir les chemins natifs en contextes POSIX, Git et URL
- 4. Systèmes de fichiers mockables (
package:filevs.p.*global) - 5. Extensions, extensions composées et extraction de radical
- 6. Workflows et liste de contrôle d'audit
- Références et exemples
1. Principes fondamentaux et règles multi-plateformes
Éviter de traiter les chemins de fichiers comme des chaînes brutes
- Les chemins de fichiers natifs sous Windows utilisent des antislashs (
\), tandis que macOS et Linux utilisent des barres obliques (/). - Les opérations sur les chaînes comme
.contains('foo/'),.startsWith('foo/')ou.split('/')échouent silencieusement sur les chemins natifs Windows. - L'interpolation de chaînes comme
'$dir/$file'injecte des barres obliques sur Windows et produit des barres doubles (//) quand$dirse termine par un séparateur.
Règle : Décomposez toujours les chemins en segments avec p.split(path) avant d'inspecter la hiérarchie de répertoires ou les noms de segments, et joignez toujours les composants de chemin avec p.join(...).
Jointure pragmatique des frontières vs. décomposition multi-segments (p.join)
- Bibliothèques multi-plateformes (Windows + POSIX) : Passez les segments de chemin individuels à
p.join(dir, 'sub', 'file.json')pour quepackage:pathinsère les séparateurs natifs du système (\sous Windows,/sur POSIX) entre chaque composant. - Outils POSIX uniquement et greppabilité statique de sous-chemins : Dans les bases de code ciblant exclusivement Linux/macOS (ou lors de la jointure d'un chemin de base dynamique à un sous-chemin statique connu), la décomposition de 5–6 segments statiques en arguments séparés (
p.join(home, '.local', 'share', 'app', 'bin', 'config.json')) fait quedart formatenveloppe sur 6–8 lignes verticales et détruit la greppabilité des sous-chaînes (recherchegrepoucode_searchpour.local/share/app/bin). - Règle pour les cibles POSIX : Préférez la jointure de frontière à 2 arguments (
p.join(home, '.local/share/app/bin/config.json')). Cela prévient les bugs de barres doubles (//) aux frontières de variables tout en préservant la lisibilité sur une seule ligne et la greppabilité exacte.
Normalisation vs. canonicalisation (p.normalize vs. p.canonicalize)
p.normalize(path)résout les segments.et..purement lexicalement sans consulter le système de fichiers ni standardiser la casse.- Lors de la déduplication de chemins de répertoires ou de la comparaison de l'identité physique de fichiers à travers des liens symboliques, des racines relatives ou des systèmes de fichiers insensibles à la casse, utilisez
p.canonicalize(path).
Supprimer les spécificateurs de localisation et convertir les URI de manière sûre
- Les chaînes formatées comme
<path>:<line>-<col>ou<path>:<line>ne sont pas des chemins purs. Les passer directement àp.normalizeouUri.parseprovoque des bugs (sous Windows,Uri.parseconfondC:avec un schéma URI et:lineavec un port). - Extrayez le suffixe
:line-colfinal via expression régulière (RegExp(r'^(.*?):(\d+(?:-\d+)?)$')) avant de passer le chemin de fichier àpackage:path. - Conversions aux frontières URI : Lors de la conversion entre chemins de fichiers et objets
Uri, utilisez toujoursp.toUri(path)etp.fromUri(uri)plutôt queUri.parse(path)ou la concaténation manuelle de chaînes.
2. Idiomes recommandés package:path vs. anti-motifs de chaînes
Jointure de chemins
- Préférez :
p.join(dir, file) - Évitez :
'$dir/$file'ou'a/$b' - Pourquoi : L'interpolation de chaînes injecte
/sous Windows et crée des barres doubles (//) quand$dirse termine par un séparateur.
Correspondance de segments
- Préférez :
p.split(path).contains('foo') - Évitez :
path.contains('foo/') - Pourquoi : La correspondance de chaînes échoue sur les antislashs Windows (
foo\bar) et produit des faux positifs sur les noms de sous-chaînes partielles (ex.barfoo/).
Racine et préfixes de répertoire
- Préférez :
p.split(path).first == 'foo'oup.isWithin('foo', path) - Évitez :
path.startsWith('foo/') - Pourquoi : Échoue sur les séparateurs Windows et manque les variantes de préfixes relatifs comme
./foo/.
Extensions de fichiers
- Préférez :
p.extension(path) == '.wasm' - Évitez :
path.endsWith('.wasm') - Pourquoi : La correspondance de suffixe de sous-chaîne correspond faussement aux répertoires (
foo.wasm/) ou aux suffixes autres que les extensions.
Découpage d'extensions et extensions composées
- Préférez :
p.withoutExtension(path)etp.extension(path, 2) - Évitez :
path.lastIndexOf('.')et la découpe manuelle avecsubstring - Pourquoi : L'arithmétique manuelle s'effondre sur les fichiers cachés pointillés (
.gitignore) et les extensions composées (.js.map,.tar.gz).
Conversion POSIX et URL
- Préférez :
p.posix.joinAll(p.split(path))oup.url.joinAll(p.split(path)) - Évitez :
path.replaceAll(r'\', '/') - Pourquoi : Le remplacement de séparateur ad hoc échoue sur les lecteurs racine et mélange le contexte du système avec les cibles POSIX ou URL.
Conversion URI
- Préférez :
p.toUri(path)etp.fromUri(uri) - Évitez :
Uri.parse(path)eturi.path - Pourquoi : L'analyse directe d'URI échoue sur les lettres de lecteur Windows (
C:) et fuit l'encodage en pourcentage (ex.%20pour les espaces).
Aide pour le nom de base de répertoire
- Préférez :
String canonicalDirName(Directory d) => p.basename(p.normalize(d.absolute.path)); - Évitez : Répéter
p.basename(p.normalize(dir.absolute.path))en ligne dans les fichiers. - Pourquoi : Centralise la logique de nommage canonique des répertoires et réduit le code répétitif.
3. Convertir les chemins natifs en contextes POSIX, Git et URL
Évitez d'appeler .replaceAll('\\', '/') ou .replaceAll(r'\', '/') pour convertir les chemins natifs du système en chemins POSIX (pour Git, YAML, manifestes d'archives) ou segments URL.
Règle : Divisez le chemin relatif natif avec p.split(...), inspectez les segments avec la correspondance de motifs de liste Dart 3, et joignez avec p.posix.joinAll(...) ou p.url.joinAll(...). Appelez toujours p.relative(filePath, from: root) en premier pour que les segments de racine ('/' sur POSIX ou r'C:\' sous Windows) n'interfèrent pas avec les motifs de préfixe relatif :
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]),
};
}
Chemins Git et métadonnées de dépôt
- Les objets d'arbre du dépôt Git, les règles de motifs
.gitignore,.gitattributeset les liens symboliques suivis par git utilisent strictement les barres obliques POSIX (/), même sous Windows. - Insérer des antislashs Windows natifs (
\) dans.gitignoreou les commandes git amène Git à traiter\comme un caractère d'échappement plutôt qu'un séparateur de répertoire, cassant silencieusement la correspondance de motifs. - Lors de la génération d'entrées
.gitignore, de manifestes de dépôt ou de cibles de liens symboliques par programme à partir de chemins de fichiers natifs, convertissez le chemin relatif natif avecp.posix.joinAll(p.split(relativePath))oup.posix.join(...).
4. Systèmes de fichiers mockables (package:file vs. p.* global)
Dans les bases de code utilisant package:file (ex. applications CLI ou services testés avec MemoryFileSystem), évitez d'appeler des fonctions top-level p.* sur les chemins File ou Directory.
- Les fonctions top-level
p.*se lient au système d'exploitation hôte exécutant le test. - Si un test unitaire crée
MemoryFileSystem(style: FileSystemStyle.windows)sur un runner Linux ou macOS, la fonction globalep.split(file.path)divisera sur/au lieu de\, cassant le test.
Règle : Utilisez toujours le Context attaché au 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, extensions composées et extraction de radical
Évitez l'arithmétique manuelle .lastIndexOf('.') et .substring() lors de l'extraction d'extensions de fichiers ou de l'insertion de hachages de contenu. p.extension supporte nativement les extensions multi-niveaux via son paramètre optionnel level.
- Nuance du radical multi-point : Appeler
p.extension('main.dart.wasm', 2)retourne'.dart.wasm'parce qu'il capture aveuglément les deux derniers segments séparés par des points. Lors du hachage ou de la suppression d'extensions sur les fichiers qui peuvent avoir des radicaux multi-points (ex.main.dart.wasmvs.main.dart.js.map), vérifiez sip.extension(filename, 2)correspond à une extension composée connue (ou.endsWith('.map')) avant de revenir à l'extension mono-niveaup.extension(filename):
import 'package:path/path.dart' as p;
String insertContentHash(String filename, String hash) {
final compoundExt = p.extension(filename, 2);
// Utilisez uniquement l'extension à 2 niveaux pour les vrais suffixes composés (ex. '.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 et liste de contrôle d'audit
Liste de contrôle de refactorisation de chemins
- [ ] Remplacez l'interpolation de chaîne (
'$dir/$file') parp.join(dir, file). - [ ] Remplacez
.contains('dir/')et.startsWith('dir/')par des vérifications de segmentsp.split(path)oup.isWithin(parent, child). - [ ] Remplacez
.replaceAll(r'\', '/')parp.posix.joinAll(p.split(path))(oup.url.joinAll). - [ ] Remplacez
.endsWith('.ext')sur les chemins de fichiers parp.extension(path) == '.ext'. - [ ] Remplacez la découpe manuelle par indice de point par
p.withoutExtension(path)etp.extension(path, [level]). - [ ] Vérifiez que le code utilisant
package:fileaccède àfileSystem.pathau lieu dep.*global. - [ ] Assurez-vous que les chemins Git, les entrées
.gitignoreet les cibles de liens symboliques utilisent les barres obliquesp.posix.
Références et exemples
- Exemples de conversion de chemins multi-plateformes et POSIX : examples/cross_platform_paths.dart
- Exemple de contexte de chemin MockableFileSystem : examples/file_system_context.dart