Wrap platform (#4778)

* Initial PlatformInfo implementation

* Wrap Platform from dart:io

* Allow Platfrom in tool/

* Fix formatting

* Tests that Platform usage is not accidentally reintroduced

* Allow Platform from dart:io in tests

* Fixed missing methods

* Only run analyzer tests on linux
diff --git a/lib/src/authentication/credential.dart b/lib/src/authentication/credential.dart
index 9526ea6..91b3a2e 100644
--- a/lib/src/authentication/credential.dart
+++ b/lib/src/authentication/credential.dart
@@ -2,9 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
 import '../exceptions.dart';
+import '../platform_info.dart';
 import '../source/hosted.dart';
 import '../utils.dart';
 
@@ -134,7 +133,7 @@
     final String tokenValue;
     final environment = env;
     if (environment != null) {
-      final value = Platform.environment[environment];
+      final value = platform.environment[environment];
       if (value == null) {
         dataError(
           'Saved credential for "$url" pub repository requires environment '
diff --git a/lib/src/authentication/token_store.dart b/lib/src/authentication/token_store.dart
index 4d87b65..f213cd3 100644
--- a/lib/src/authentication/token_store.dart
+++ b/lib/src/authentication/token_store.dart
@@ -3,12 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:convert';
-import 'dart:io';
 
 import '../exceptions.dart';
 import '../io.dart';
 import '../log.dart' as log;
 import '../path.dart';
+import '../platform_info.dart';
 import 'credential.dart';
 
 /// Stores and manages authentication credentials.
@@ -96,7 +96,7 @@
   }
 
   Never missingConfigDir() {
-    final variable = Platform.isWindows ? '%APPDATA%' : r'$HOME';
+    final variable = platform.isWindows ? '%APPDATA%' : r'$HOME';
     throw DataException('No config dir found. Check that $variable is set');
   }
 
diff --git a/lib/src/command.dart b/lib/src/command.dart
index 89e6628..d1a9d0b 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -20,6 +20,7 @@
 import 'http.dart';
 import 'log.dart' as log;
 import 'path.dart';
+import 'platform_info.dart';
 import 'pub_embeddable_command.dart';
 import 'sdk.dart';
 import 'solver.dart';
@@ -40,7 +41,7 @@
 final lineLength = _lineLength();
 
 int _lineLength() {
-  final fromEnv = Platform.environment['_PUB_TEST_TERMINAL_COLUMNS'];
+  final fromEnv = platform.environment['_PUB_TEST_TERMINAL_COLUMNS'];
   if (fromEnv != null) {
     final parsed = int.tryParse(fromEnv);
     if (parsed != null && parsed > 0) return parsed;
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index 62836ed..8166762 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -20,6 +20,7 @@
 import '../log.dart' as log;
 import '../oauth2.dart' as oauth2;
 import '../path.dart';
+import '../platform_info.dart';
 import '../pubspec.dart';
 import '../solver/type.dart';
 import '../source/hosted.dart' show validateAndNormalizeHostedUrl;
@@ -247,8 +248,8 @@
         // explicitly have to define mock servers as official server to test
         // publish command with oauth2 credentials.
         if (runningFromTest &&
-            Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
-          Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
+            platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
+          platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
       };
 
       // Using OAuth2 authentication client for the official pub servers
diff --git a/lib/src/command/token_add.dart b/lib/src/command/token_add.dart
index 9359e81..e913c88 100644
--- a/lib/src/command/token_add.dart
+++ b/lib/src/command/token_add.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
 import '../authentication/credential.dart';
 import '../command.dart';
@@ -11,6 +10,7 @@
 import '../exceptions.dart';
 import '../io.dart';
 import '../log.dart' as log;
+import '../platform_info.dart';
 import '../source/hosted.dart';
 import '../utils.dart';
 
@@ -141,7 +141,7 @@
       'token stored in the environment variable "$envVar".',
     );
 
-    if (!Platform.environment.containsKey(envVar)) {
+    if (!platform.environment.containsKey(envVar)) {
       // If environment variable doesn't exist when
       // pub token add <hosted-url> --env-var <ENV_VAR> is called, we should
       // print a warning.
diff --git a/lib/src/command/uploader.dart b/lib/src/command/uploader.dart
index d163c0b..d5361f0 100644
--- a/lib/src/command/uploader.dart
+++ b/lib/src/command/uploader.dart
@@ -3,9 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
 import '../command.dart';
+import '../platform_info.dart';
 import '../utils.dart';
 
 /// Handles the `uploader` pub command.
@@ -28,7 +28,7 @@
   UploaderCommand() {
     argParser.addOption(
       'server',
-      defaultsTo: Platform.environment['PUB_HOSTED_URL'] ?? 'https://pub.dev',
+      defaultsTo: platform.environment['PUB_HOSTED_URL'] ?? 'https://pub.dev',
       help: 'The package server on which the package is hosted.\n',
       hide: true,
     );
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 35f0823..55af1be 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
@@ -35,6 +34,7 @@
 import 'log.dart' as log;
 import 'log.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'sdk.dart';
 import 'utils.dart';
 
@@ -42,7 +42,7 @@
 /// 'flutter' if we are running inside `flutter pub` 'dart' otherwise.
 String topLevelProgram = _isRunningInsideFlutter ? 'flutter' : 'dart';
 
-bool _isRunningInsideFlutter = (Platform.environment['PUB_ENVIRONMENT'] ?? '')
+bool _isRunningInsideFlutter = (platform.environment['PUB_ENVIRONMENT'] ?? '')
     .contains('flutter_cli');
 
 class PubCommandRunner extends CommandRunner<int> implements PubTopLevel {
@@ -206,7 +206,7 @@
     final depsRev = match[1];
 
     String actualRev;
-    final pubRoot = p.dirname(p.dirname(p.fromUri(Platform.script)));
+    final pubRoot = p.dirname(p.dirname(p.fromUri(platform.script)));
     try {
       actualRev =
           git.runSync(['rev-parse', 'HEAD'], workingDir: pubRoot).trim();
diff --git a/lib/src/dart.dart b/lib/src/dart.dart
index 9a12e8d..78d8bac 100644
--- a/lib/src/dart.dart
+++ b/lib/src/dart.dart
@@ -19,6 +19,7 @@
 import 'io.dart';
 import 'log.dart' as log;
 import 'path.dart';
+import 'platform_info.dart';
 
 class AnalysisContextManager {
   static final sessions = <String, AnalysisContextManager>{};
@@ -115,7 +116,7 @@
   String? nativeAssets,
 }) async {
   const platformDill = 'lib/_internal/vm_platform_strong.dill';
-  final sdkRoot = p.relative(p.dirname(p.dirname(Platform.resolvedExecutable)));
+  final sdkRoot = p.relative(p.dirname(p.dirname(platform.resolvedExecutable)));
   String? tempDir;
   FrontendServerClient? client;
   try {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 8e52054..314729a 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -27,6 +27,7 @@
 import 'package_graph.dart';
 import 'package_name.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'pubspec.dart';
 import 'pubspec_utils.dart';
 import 'sdk.dart';
@@ -710,9 +711,9 @@
       } else {
         ensureDir(_snapshotPath);
       }
-      // Don't do more than `Platform.numberOfProcessors - 1` compilations
+      // Don't do more than `platform.numberOfProcessors - 1` compilations
       // concurrently. Though at least one.
-      final pool = Pool(max(Platform.numberOfProcessors - 1, 1));
+      final pool = Pool(max(platform.numberOfProcessors - 1, 1));
       return waitAndPrintErrors(
         executables.map((executable) async {
           await pool.withResource(() async {
@@ -1417,7 +1418,7 @@
   /// will result in [acquireDependencies] to only print a summary of the
   /// results.
   bool get _summaryOnlyEnvironment =>
-      (Platform.environment['PUB_SUMMARY_ONLY'] ?? '0') != '0';
+      (platform.environment['PUB_SUMMARY_ONLY'] ?? '0') != '0';
 
   /// Remove any `pubspec.lock` or `.dart_tool/package_config.json` files in
   /// workspace packages that are not the root package.
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 7d7a7dd..b0c2ccd 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -18,6 +18,7 @@
 import 'log.dart';
 import 'package_config.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'sdk.dart';
 import 'system_cache.dart';
 import 'utils.dart';
@@ -104,7 +105,7 @@
   }
   // We use an absolute path here not because the VM insists but because it's
   // helpful for the subprocess to be able to spawn Dart with
-  // Platform.executableArguments and have that work regardless of the working
+  // platform.executableArguments and have that work regardless of the working
   // directory.
   final packageConfigAbsolute = p.absolute(entrypoint.packageConfigPath);
 
@@ -193,7 +194,7 @@
     // semantics without `fork` for starting the subprocess.
     // https://github.com/dart-lang/sdk/issues/41966.
     final subscription = ProcessSignal.sigint.watch().listen((e) {});
-    final process = await Process.start(Platform.resolvedExecutable, [
+    final process = await Process.start(platform.resolvedExecutable, [
       '--packages=$packageConfig',
       ...vmArgs,
       if (enableAsserts) '--enable-asserts',
@@ -446,7 +447,7 @@
 
 bool _looksLikeFile(String candidate) {
   return candidate.contains('/') ||
-      (Platform.isWindows && candidate.contains(r'\')) ||
+      (platform.isWindows && candidate.contains(r'\')) ||
       candidate.endsWith('.dart') ||
       candidate.endsWith('.snapshot');
 }
diff --git a/lib/src/flutter_releases.dart b/lib/src/flutter_releases.dart
index c048e1a..4810fca 100644
--- a/lib/src/flutter_releases.dart
+++ b/lib/src/flutter_releases.dart
@@ -2,7 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 import 'dart:convert';
-import 'dart:io';
 
 import 'package:collection/collection.dart';
 import 'package:http/http.dart';
@@ -10,9 +9,10 @@
 
 import 'http.dart';
 import 'log.dart';
+import 'platform_info.dart';
 
 String get flutterReleasesUrl =>
-    Platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ??
+    platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ??
     'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json';
 
 // Retrieves all released versions of Flutter.
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 1db8cc6..235c774 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -19,6 +19,7 @@
 import 'package.dart';
 import 'package_name.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'pubspec.dart';
 import 'sdk.dart';
 import 'sdk/dart.dart';
@@ -949,7 +950,7 @@
     required bool isRefreshingBinstub,
   }) {
     var binStubPath = p.join(_binStubDir, executable);
-    if (Platform.isWindows) binStubPath += '.bat';
+    if (platform.isWindows) binStubPath += '.bat';
 
     String? previousPackage;
     if (!isRefreshingBinstub && fileExists(binStubPath)) {
@@ -964,12 +965,12 @@
     // When running tests we want the binstub to invoke the current pub, not the
     // one from the sdk.
     final pubInvocation =
-        runningFromTest ? Platform.script.toFilePath() : 'pub';
+        runningFromTest ? platform.script.toFilePath() : 'pub';
 
     final runPubGlobal = '${package.name}:$script';
 
     final String binstub;
-    if (Platform.isWindows) {
+    if (platform.isWindows) {
       final header = '''
 @echo off
 rem This file was created by pub v${sdk.version}.
@@ -1053,7 +1054,7 @@
       // path names.
       writeTextFile(tmpPath, binstub, encoding: const SystemEncoding());
 
-      if (Platform.isLinux || Platform.isMacOS) {
+      if (platform.isLinux || platform.isMacOS) {
         // Make it executable.
         final result = Process.runSync('chmod', ['+x', tmpPath]);
         if (result.exitCode != 0) {
@@ -1097,7 +1098,7 @@
   /// [installed] should be the name of an installed executable that can be used
   /// to test whether accessing it on the path works.
   void _suggestIfNotOnPath(String installed) {
-    if (Platform.isWindows) {
+    if (platform.isWindows) {
       // See if the shell can find one of the binstubs.
       // "\q" means return exit code 0 if found or 1 if not.
       final result = runProcessSync('where', [r'\q', '$installed.bat']);
@@ -1122,14 +1123,14 @@
       if (result.exitCode == 0) return;
 
       var binDir = _binStubDir;
-      if (binDir.startsWith(Platform.environment['HOME']!)) {
+      if (binDir.startsWith(platform.environment['HOME']!)) {
         binDir = p.join(
           r'$HOME',
-          p.relative(binDir, from: Platform.environment['HOME']),
+          p.relative(binDir, from: platform.environment['HOME']),
         );
       }
       final shellConfigFiles =
-          Platform.isMacOS
+          platform.isMacOS
               // zsh is default on mac - mention that first.
               ? '(.zshrc, .bashrc, .bash_profile, etc.)'
               : '(.bashrc, .bash_profile, .zshrc etc.)';
diff --git a/lib/src/http.dart b/lib/src/http.dart
index ccecab0..a5b461a 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -15,6 +15,7 @@
 import 'package:pool/pool.dart';
 
 import 'log.dart' as log;
+import 'platform_info.dart';
 import 'sdk.dart';
 import 'utils.dart';
 
@@ -339,7 +340,7 @@
             log.io('Attempt #$attemptNumber for $operation'),
     maxAttempts: math.max(
       1, // Having less than 1 attempt doesn't make sense.
-      int.tryParse(Platform.environment['PUB_MAX_HTTP_RETRIES'] ?? '') ?? 7,
+      int.tryParse(platform.environment['PUB_MAX_HTTP_RETRIES'] ?? '') ?? 7,
     ),
   );
 }
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 00ad322..30b8dbc 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -28,6 +28,7 @@
 import 'gzip/gzip.dart';
 import 'log.dart' as log;
 import 'path.dart';
+import 'platform_info.dart';
 import 'utils.dart';
 
 export 'package:http/http.dart' show ByteStream;
@@ -427,7 +428,7 @@
         }
 
         if (pathInDir.contains('/.')) return false;
-        if (!Platform.isWindows) return true;
+        if (!platform.isWindows) return true;
         return !pathInDir.contains('\\.');
       })
       .map((entity) => entity.path)
@@ -455,7 +456,7 @@
   void Function() operation, {
   bool ignoreEmptyDir = false,
 }) {
-  if (!Platform.isWindows) {
+  if (!platform.isWindows) {
     operation();
     return;
   }
@@ -594,11 +595,11 @@
   // ```
   // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno-base.h#n21
   // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/errno.h#n20
-  (Platform.isLinux && (errorCode == 39 || errorCode == 17)) ||
+  (platform.isLinux && (errorCode == 39 || errorCode == 17)) ||
       // On Windows this may fail with ERROR_DIR_NOT_EMPTY or
       // ERROR_ALREADY_EXISTS
       // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
-      (Platform.isWindows && (errorCode == 145 || errorCode == 183)) ||
+      (platform.isWindows && (errorCode == 145 || errorCode == 183)) ||
       // On MacOS rename will fail with ENOTEMPTY if directory exists.
       // We also catch EEXIST - perhaps that could also be thrown...
       // ```
@@ -606,7 +607,7 @@
       // #define	EEXIST		17	/* File exists */
       // ```
       // https://github.com/apple-oss-distributions/xnu/blob/bb611c8fecc755a0d8e56e2fa51513527c5b7a0e/bsd/sys/errno.h#L190
-      (Platform.isMacOS && (errorCode == 66 || errorCode == 17));
+      (platform.isMacOS && (errorCode == 66 || errorCode == 17));
 }
 
 /// Creates a new symlink at path [symlink] that points to [target].
@@ -623,7 +624,7 @@
     // make sure we have a clean absolute path because it will interpret a
     // relative path to be relative to the cwd, not the symlink, and will be
     // confused by forward slashes.
-    if (Platform.isWindows) {
+    if (platform.isWindows) {
       target = p.normalize(p.absolute(target));
     } else {
       // If the directory where we're creating the symlink was itself reached
@@ -667,7 +668,7 @@
 /// The "_PUB_TESTING" variable is automatically set for all the test code's
 /// invocations of pub.
 final bool runningFromTest =
-    Platform.environment.containsKey('_PUB_TESTING') && _assertionsEnabled;
+    platform.environment.containsKey('_PUB_TESTING') && _assertionsEnabled;
 
 final bool _assertionsEnabled = () {
   try {
@@ -680,8 +681,8 @@
 }();
 
 final bool runningFromFlutter =
-    Platform.environment.containsKey('PUB_ENVIRONMENT') &&
-    (Platform.environment['PUB_ENVIRONMENT'] ?? '').contains('flutter_cli');
+    platform.environment.containsKey('PUB_ENVIRONMENT') &&
+    (platform.environment['PUB_ENVIRONMENT'] ?? '').contains('flutter_cli');
 
 /// A regular expression to match the script path of a pub script running from
 /// source in the Dart repo.
@@ -697,7 +698,7 @@
 ///
 /// This can happen when running tests against the repo, as well as when
 /// building Observatory.
-final bool runningFromDartRepo = Platform.script.path.contains(_dartRepoRegExp);
+final bool runningFromDartRepo = platform.script.path.contains(_dartRepoRegExp);
 
 /// The path to the root of the Dart repo.
 ///
@@ -711,8 +712,8 @@
 
       // Get the URL of the repo root in a way that works when either both
       // running as a test or as a pub executable.
-      final url = Platform.script.replace(
-        path: Platform.script.path.replaceAll(_dartRepoRegExp, ''),
+      final url = platform.script.replace(
+        path: platform.script.path.replaceAll(_dartRepoRegExp, ''),
       );
       return p.fromUri(url);
     })();
@@ -759,7 +760,7 @@
 /// [EnvironmentKeys.forceTerminalOutput].
 bool get terminalOutputForStdout {
   final environmentValue =
-      Platform.environment[EnvironmentKeys.forceTerminalOutput];
+      platform.environment[EnvironmentKeys.forceTerminalOutput];
   if (environmentValue == null || environmentValue == '') {
     return stdout.hasTerminal;
   } else if (environmentValue == '0') {
@@ -1081,7 +1082,7 @@
   // Spawning a process on Windows will not look for the executable in the
   // system path. So, if executable looks like it needs that (i.e. it doesn't
   // have any path separators in it), then spawn it through a shell.
-  if (Platform.isWindows && !executable.contains('\\')) {
+  if (platform.isWindows && !executable.contains('\\')) {
     args = ['/c', executable, ...args];
     executable = 'cmd';
   }
@@ -1205,7 +1206,7 @@
         ensureDir(parentDirectory);
         await createFileFromStream(entry.contents, filePath);
 
-        if (Platform.isLinux || Platform.isMacOS) {
+        if (platform.isLinux || platform.isMacOS) {
           // Apply executable bits from tar header, but don't change r/w bits
           // from the default
           final mode = _defaultMode | (entry.header.mode & _executableMask);
@@ -1334,8 +1335,8 @@
 /// `null` if no config dir could be found.
 final String? dartConfigDir = () {
   if (runningFromTest &&
-      Platform.environment.containsKey('_PUB_TEST_CONFIG_DIR')) {
-    return p.join(Platform.environment['_PUB_TEST_CONFIG_DIR']!, 'dart');
+      platform.environment.containsKey('_PUB_TEST_CONFIG_DIR')) {
+    return p.join(platform.environment['_PUB_TEST_CONFIG_DIR']!, 'dart');
   }
   try {
     return applicationConfigHome('dart');
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 00fad85..1e98118 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -18,6 +18,7 @@
 import 'exceptions.dart';
 import 'io.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'progress.dart';
 import 'sdk.dart';
 import 'transcript.dart';
@@ -372,11 +373,11 @@
 
 Pub version: ${sdk.version}
 Created: ${DateTime.now().toIso8601String()}
-FLUTTER_ROOT: ${Platform.environment['FLUTTER_ROOT'] ?? '<not set>'}
-PUB_HOSTED_URL: ${Platform.environment['PUB_HOSTED_URL'] ?? '<not set>'}
-PUB_CACHE: "${Platform.environment['PUB_CACHE'] ?? '<not set>'}"
+FLUTTER_ROOT: ${platform.environment['FLUTTER_ROOT'] ?? '<not set>'}
+PUB_HOSTED_URL: ${platform.environment['PUB_HOSTED_URL'] ?? '<not set>'}
+PUB_CACHE: "${platform.environment['PUB_CACHE'] ?? '<not set>'}"
 Command: $command
-Platform: ${Platform.operatingSystem}
+Platform: ${platform.operatingSystem}
 ''');
 
   if (entrypoint != null) {
diff --git a/lib/src/oauth2.dart b/lib/src/oauth2.dart
index 0cf841a..64d2b4e 100644
--- a/lib/src/oauth2.dart
+++ b/lib/src/oauth2.dart
@@ -4,7 +4,6 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io';
 import 'dart:math';
 
 import 'package:collection/collection.dart';
@@ -19,6 +18,7 @@
 import 'io.dart';
 import 'log.dart' as log;
 import 'path.dart';
+import 'platform_info.dart';
 import 'utils.dart';
 
 /// The global HTTP client with basic retries. Used instead of retryForHttp for
@@ -65,7 +65,7 @@
 /// This can be controlled externally by setting the `_PUB_TEST_TOKEN_ENDPOINT`
 /// environment variable.
 Uri get tokenEndpoint {
-  final tokenEndpoint = Platform.environment['_PUB_TEST_TOKEN_ENDPOINT'];
+  final tokenEndpoint = platform.environment['_PUB_TEST_TOKEN_ENDPOINT'];
   if (tokenEndpoint != null) {
     return Uri.parse(tokenEndpoint);
   } else {
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 1fc04f4..2ccdfd9 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -16,6 +16,7 @@
 import 'log.dart' as log;
 import 'package_name.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'pubspec.dart';
 import 'utils.dart';
 
@@ -310,7 +311,7 @@
             .path;
     if (beneath == './') beneath = '.';
     String resolve(String path) {
-      if (Platform.isWindows) {
+      if (platform.isWindows) {
         return p.joinAll([root, ...p.posix.split(path)]);
       }
       return p.join(root, path);
@@ -373,7 +374,7 @@
             }
             return contents.map((entity) {
               final relative = p.relative(entity.path, from: root);
-              if (Platform.isWindows) {
+              if (platform.isWindows) {
                 return p.posix.joinAll(p.split(relative));
               }
               return relative;
@@ -437,7 +438,7 @@
                   // [1]:
                   // https://git-scm.com/docs/git-config/2.14.6#Documentation/git-config.txt-coreignoreCase
                   // [2]: https://github.com/dart-lang/pub/issues/3003
-                  ignoreCase: Platform.isMacOS || Platform.isWindows,
+                  ignoreCase: platform.isMacOS || platform.isWindows,
                 );
           },
           isDir: (dir) => dirExists(resolve(dir)),
@@ -584,7 +585,7 @@
 
 bool _looksLikeGlob(String s) => Glob.quote(s) != s;
 String _useBackSlashesOnWindows(String path) {
-  if (Platform.isWindows) {
+  if (platform.isWindows) {
     return p.joinAll(p.split(path));
   }
   return path;
diff --git a/lib/src/platform_info.dart b/lib/src/platform_info.dart
new file mode 100644
index 0000000..0a75feb
--- /dev/null
+++ b/lib/src/platform_info.dart
@@ -0,0 +1,207 @@
+// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+/// A proxy for [Platform] from `dart:io` which can be overridden.
+PlatformInfo get platform =>
+    Zone.current[_platformInfoKey] as PlatformInfo? ??
+    PlatformInfo.nativePlatform();
+
+/// The key for the [platform] in the current [Zone].
+final _platformInfoKey = Object();
+
+/// Runs [callback] in a [Zone] where `platform` is overridden by [platform].
+Future<T> withPlatform<T>(
+  FutureOr<T> Function() callback, {
+  required PlatformInfo platform,
+}) {
+  return runZoned(() async {
+    return await callback();
+  }, zoneValues: {_platformInfoKey: platform});
+}
+
+abstract final class PlatformInfo {
+  const PlatformInfo._();
+
+  factory PlatformInfo.nativePlatform() = _NativePlatformInfo;
+
+  factory PlatformInfo.override({
+    required Map<String, String> environment,
+    required String executable,
+    required bool isAndroid,
+    required bool isFuchsia,
+    required bool isIOS,
+    required bool isLinux,
+    required bool isMacOS,
+    required bool isWindows,
+    required String lineTerminator,
+    required String operatingSystem,
+    required String pathSeparator,
+    required String resolvedExecutable,
+    required String version,
+    required int numberOfProcessors,
+    required Uri script,
+  }) = _PlatformInfoOverride;
+
+  /// Returns [Platform.environment].
+  Map<String, String> get environment;
+
+  /// Returns [Platform.executable].
+  String get executable;
+
+  /// Returns [Platform.isAndroid].
+  bool get isAndroid;
+
+  /// Returns [Platform.isFuchsia].
+  bool get isFuchsia;
+
+  /// Returns [Platform.isIOS].
+  bool get isIOS;
+
+  /// Returns [Platform.isLinux].
+  bool get isLinux;
+
+  /// Returns [Platform.isMacOS].
+  bool get isMacOS;
+
+  /// Returns [Platform.isWindows].
+  bool get isWindows;
+
+  /// Returns [Platform.lineTerminator].
+  String get lineTerminator;
+
+  /// Returns [Platform.operatingSystem].
+  String get operatingSystem;
+
+  /// Returns [Platform.pathSeparator].
+  String get pathSeparator;
+
+  /// Returns [Platform.resolvedExecutable].
+  String get resolvedExecutable;
+
+  /// Returns [Platform.numberOfProcessors].
+  int get numberOfProcessors;
+
+  /// Returns [Platform.script].
+  Uri get script;
+
+  /// Returns [Platform.version] from 'dart:io'.
+  String get version;
+}
+
+final class _NativePlatformInfo extends PlatformInfo {
+  const _NativePlatformInfo() : super._();
+
+  @override
+  Map<String, String> get environment => Platform.environment;
+
+  @override
+  String get executable => Platform.executable;
+
+  @override
+  bool get isAndroid => Platform.isAndroid;
+
+  @override
+  bool get isFuchsia => Platform.isFuchsia;
+
+  @override
+  bool get isIOS => Platform.isIOS;
+
+  @override
+  bool get isLinux => Platform.isLinux;
+
+  @override
+  bool get isMacOS => Platform.isMacOS;
+
+  @override
+  bool get isWindows => Platform.isWindows;
+
+  @override
+  String get lineTerminator => Platform.lineTerminator;
+
+  @override
+  String get operatingSystem => Platform.operatingSystem;
+
+  @override
+  String get pathSeparator => Platform.pathSeparator;
+
+  @override
+  String get resolvedExecutable => Platform.resolvedExecutable;
+
+  @override
+  String get version => Platform.version;
+
+  @override
+  int get numberOfProcessors => Platform.numberOfProcessors;
+
+  @override
+  Uri get script => Platform.script;
+}
+
+final class _PlatformInfoOverride extends PlatformInfo {
+  const _PlatformInfoOverride({
+    required this.environment,
+    required this.executable,
+    required this.isAndroid,
+    required this.isFuchsia,
+    required this.isIOS,
+    required this.isLinux,
+    required this.isMacOS,
+    required this.isWindows,
+    required this.lineTerminator,
+    required this.operatingSystem,
+    required this.pathSeparator,
+    required this.resolvedExecutable,
+    required this.version,
+    required this.numberOfProcessors,
+    required this.script,
+  }) : super._();
+
+  @override
+  final Map<String, String> environment;
+
+  @override
+  final String executable;
+
+  @override
+  final bool isAndroid;
+
+  @override
+  final bool isFuchsia;
+
+  @override
+  final bool isIOS;
+
+  @override
+  final bool isLinux;
+
+  @override
+  final bool isMacOS;
+
+  @override
+  final bool isWindows;
+
+  @override
+  final String lineTerminator;
+
+  @override
+  final String operatingSystem;
+
+  @override
+  final String pathSeparator;
+
+  @override
+  final String resolvedExecutable;
+
+  @override
+  final String version;
+
+  @override
+  final int numberOfProcessors;
+
+  @override
+  final Uri script;
+}
diff --git a/lib/src/sdk/dart.dart b/lib/src/sdk/dart.dart
index a67f751..3b1578f 100644
--- a/lib/src/sdk/dart.dart
+++ b/lib/src/sdk/dart.dart
@@ -2,13 +2,12 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
 import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml/yaml.dart';
 
 import '../io.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../sdk.dart';
 import 'sdk_package_config.dart';
 
@@ -27,7 +26,7 @@
 
   static final String _rootDirectory = () {
     // If DART_ROOT is specified, then this always points to the Dart SDK
-    if (Platform.environment['DART_ROOT'] case final root?) {
+    if (platform.environment['DART_ROOT'] case final root?) {
       return root;
     }
 
@@ -35,7 +34,7 @@
 
     // The Dart executable is in "/path/to/sdk/bin/dart", so two levels up is
     // "/path/to/sdk".
-    final aboveExecutable = p.dirname(p.dirname(Platform.resolvedExecutable));
+    final aboveExecutable = p.dirname(p.dirname(platform.resolvedExecutable));
     assert(fileExists(p.join(aboveExecutable, 'version')));
     return aboveExecutable;
   }();
@@ -64,8 +63,8 @@
     // tests on the bots are not run from a built SDK so this lets us avoid
     // parsing the missing version file.
     final sdkVersion =
-        Platform.environment['_PUB_TEST_SDK_VERSION'] ??
-        Platform.version.split(' ').first;
+        platform.environment['_PUB_TEST_SDK_VERSION'] ??
+        platform.version.split(' ').first;
 
     return Version.parse(sdkVersion);
   }();
diff --git a/lib/src/sdk/flutter.dart b/lib/src/sdk/flutter.dart
index 4fb494a..9a81989 100644
--- a/lib/src/sdk/flutter.dart
+++ b/lib/src/sdk/flutter.dart
@@ -10,6 +10,7 @@
 import '../io.dart';
 import '../log.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../sdk.dart';
 
 class FlutterSdk extends Sdk {
@@ -28,8 +29,8 @@
   late final bool isAvailable = rootDirectory != null && version != null;
   late final String? rootDirectory = () {
     // If FLUTTER_ROOT is specified, then this always points to the Flutter SDK
-    if (Platform.environment.containsKey('FLUTTER_ROOT')) {
-      return Platform.environment['FLUTTER_ROOT'];
+    if (platform.environment.containsKey('FLUTTER_ROOT')) {
+      return platform.environment['FLUTTER_ROOT'];
     }
 
     // We can try to find the Flutter SDK relative to the Dart SDK.
diff --git a/lib/src/sdk/fuchsia.dart b/lib/src/sdk/fuchsia.dart
index bb3a78d..20de1d4 100644
--- a/lib/src/sdk/fuchsia.dart
+++ b/lib/src/sdk/fuchsia.dart
@@ -2,12 +2,11 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
 import 'package:pub_semver/pub_semver.dart';
 
 import '../io.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../sdk.dart';
 
 class FuchsiaSdk extends Sdk {
@@ -20,7 +19,7 @@
 
   static final bool _isAvailable = _rootDirectory != null;
   static final String? _rootDirectory =
-      Platform.environment['FUCHSIA_DART_SDK_ROOT'];
+      platform.environment['FUCHSIA_DART_SDK_ROOT'];
 
   @override
   String get installMessage =>
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 238cd1e..1ba043e 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -16,6 +16,7 @@
 import '../package.dart';
 import '../package_name.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../pubspec.dart';
 import '../source.dart';
 import '../system_cache.dart';
@@ -340,7 +341,7 @@
     );
 
     // Git doesn't recognize backslashes in paths, even on Windows.
-    if (Platform.isWindows) pathInCache = pathInCache.replaceAll('\\', '/');
+    if (platform.isWindows) pathInCache = pathInCache.replaceAll('\\', '/');
 
     final repoPath = _repoCachePath(description, cache);
     final revision = resolvedDescription.resolvedRef;
@@ -1178,7 +1179,7 @@
 String _gitDirArg(String path) {
   path = p.absolute(path);
   final forwardSlashPath =
-      Platform.isWindows ? path.replaceAll('\\', '/') : path;
+      platform.isWindows ? path.replaceAll('\\', '/') : path;
   return '--git-dir=$forwardSlashPath';
 }
 
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index 9f04928..b149d68 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -26,6 +26,7 @@
 import '../package.dart';
 import '../package_name.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../pubspec.dart';
 import '../rate_limited_scheduler.dart';
 import '../source.dart';
@@ -109,8 +110,8 @@
   }
   if (runningFromTest &&
       u == Uri.parse('https://pub.dev') &&
-      Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL')) {
-    u = Uri.parse(Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']!);
+      platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL')) {
+    u = Uri.parse(platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL']!);
   }
   return u;
 }
@@ -141,8 +142,8 @@
     final origin = parsedUrl.origin;
     // Allow the defaultHostedUrl to be overriden when running from tests
     if (runningFromTest &&
-        io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] != null) {
-      return origin == io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'];
+        platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] != null) {
+      return origin == platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'];
     }
     return origin == pubDevUrl || origin == pubDartlangUrl;
   }
@@ -171,11 +172,11 @@
       // Allow the defaultHostedUrl to be overriden when running from tests
       if (runningFromTest) {
         defaultHostedUrl =
-            io.Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] ??
+            platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'] ??
             defaultHostedUrl;
       }
       return validateAndNormalizeHostedUrl(
-        io.Platform.environment['PUB_HOSTED_URL'] ?? defaultHostedUrl,
+        platform.environment['PUB_HOSTED_URL'] ?? defaultHostedUrl,
       ).toString();
     } on FormatException catch (e) {
       throw ConfigException(
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index 274d495..3eacc07 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -16,6 +16,7 @@
 import 'package.dart';
 import 'package_name.dart';
 import 'path.dart';
+import 'platform_info.dart';
 import 'pubspec.dart';
 import 'source.dart';
 import 'source/cached.dart';
@@ -42,14 +43,14 @@
 
   static String defaultDir =
       (() {
-        final envCache = Platform.environment['PUB_CACHE'];
+        final envCache = platform.environment['PUB_CACHE'];
         if (envCache != null) {
           return envCache;
-        } else if (Platform.isWindows) {
+        } else if (platform.isWindows) {
           // %LOCALAPPDATA% is used as the cache location over %APPDATA%,
           // because the latter is synchronised between devices when the user
           // roams between them, whereas the former is not.
-          final localAppData = Platform.environment['LOCALAPPDATA'];
+          final localAppData = platform.environment['LOCALAPPDATA'];
           if (localAppData == null) {
             dataError('''
 Could not find the pub cache. No `LOCALAPPDATA` environment variable exists.
@@ -58,7 +59,7 @@
           }
           return p.join(localAppData, 'Pub', 'Cache');
         } else {
-          final home = Platform.environment['HOME'];
+          final home = platform.environment['HOME'];
           if (home == null) {
             dataError('''
 Could not find the pub cache. No `HOME` environment variable exists.
@@ -335,9 +336,9 @@
     //
     // Thus, we migrated to storing the pub-cache in `%LOCALAPPDATA%`. And
     // finished the migration in Dart 3 to keep things simple.
-    if (!Platform.isWindows) return;
+    if (!platform.isWindows) return;
 
-    final appData = Platform.environment['APPDATA'];
+    final appData = platform.environment['APPDATA'];
     if (appData == null) return;
     final legacyCacheLocation = p.join(appData, 'Pub', 'Cache');
     final legacyCacheDeprecatedFile = p.join(
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index a769cf6..3b37458 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -22,6 +22,7 @@
 import 'exit_codes.dart' as exit_codes;
 import 'io.dart';
 import 'log.dart' as log;
+import 'platform_info.dart';
 import 'pubspec_parse.dart';
 
 /// A regular expression matching a Dart identifier.
@@ -267,7 +268,7 @@
 Set<String> createFileFilter(Iterable<String> files) {
   return files.expand<String>((file) {
     final result = ['/$file'];
-    if (Platform.isWindows) result.add('\\$file');
+    if (platform.isWindows) result.add('\\$file');
     return result;
   }).toSet();
 }
@@ -280,7 +281,7 @@
 Set<String> createDirectoryFilter(Iterable<String> dirs) {
   return dirs.expand<String>((dir) {
     final result = ['/$dir/'];
-    if (Platform.isWindows) {
+    if (platform.isWindows) {
       result
         ..add('/$dir\\')
         ..add('\\$dir/')
@@ -444,7 +445,7 @@
     case ForceColorOption.never:
       return false;
     case ForceColorOption.auto:
-      return (!Platform.environment.containsKey('NO_COLOR')) &&
+      return (!platform.environment.containsKey('NO_COLOR')) &&
           terminalOutputForStdout &&
           stdout.supportsAnsiEscapes;
   }
@@ -467,8 +468,8 @@
     runningFromTest ||
     // When not outputting to terminal we can also use unicode.
     !terminalOutputForStdout ||
-    !Platform.isWindows ||
-    Platform.environment.containsKey('WT_SESSION');
+    !platform.isWindows ||
+    platform.environment.containsKey('WT_SESSION');
 
 /// Prepends each line in [text] with [prefix].
 ///
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart
index f3e48a8..542bea7 100644
--- a/lib/src/validator/analyze.dart
+++ b/lib/src/validator/analyze.dart
@@ -3,11 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
 import '../io.dart';
 import '../log.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../validator.dart';
 
 /// Runs `dart analyze` and gives a warning if it returns non-zero.
@@ -25,7 +25,7 @@
     final entries = _entriesToAnalyze
         .map((dir) => p.join(package.dir, dir))
         .where(entryExists);
-    final result = await runProcess(Platform.resolvedExecutable, [
+    final result = await runProcess(platform.resolvedExecutable, [
       'analyze',
       ...entries,
       p.join(package.dir, 'pubspec.yaml'),
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index 1d9fe3b..1718d04 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -12,6 +12,7 @@
 import '../io.dart';
 import '../log.dart' as log;
 import '../path.dart';
+import '../platform_info.dart';
 import '../utils.dart';
 import '../validator.dart';
 
@@ -59,7 +60,7 @@
         beneath = '';
       }
       String resolve(String path) {
-        if (Platform.isWindows) {
+        if (platform.isWindows) {
           return p.joinAll([root, ...p.posix.split(path)]);
         }
         return p.join(root, path);
@@ -91,7 +92,7 @@
             },
           ).map((file) {
             final relative = p.relative(resolve(file), from: package.dir);
-            return Platform.isWindows
+            return platform.isWindows
                 ? p.posix.joinAll(p.split(relative))
                 : relative;
           }).toSet();
diff --git a/lib/src/validator/leak_detection.dart b/lib/src/validator/leak_detection.dart
index e65000f..78d3f07 100644
--- a/lib/src/validator/leak_detection.dart
+++ b/lib/src/validator/leak_detection.dart
@@ -15,6 +15,7 @@
 
 import '../ignore.dart';
 import '../path.dart';
+import '../platform_info.dart';
 import '../validator.dart';
 
 /// All recognized secrets fit in ASCII (first seven bits). So for speed we
@@ -32,7 +33,7 @@
     // Load `false_secrets` from `pubspec.yaml`.
     final falseSecrets = Ignore(
       package.pubspec.falseSecrets,
-      ignoreCase: Platform.isWindows || Platform.isMacOS,
+      ignoreCase: platform.isWindows || platform.isMacOS,
     );
 
     final pool = Pool(20); // don't read more than 20 files concurrently!
diff --git a/test/platform_info_test.dart b/test/platform_info_test.dart
new file mode 100644
index 0000000..dfb2f53
--- /dev/null
+++ b/test/platform_info_test.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub/src/platform_info.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('overriding works', () async {
+    final originalOS = platform.operatingSystem;
+    final fakePlatform = PlatformInfo.override(
+      environment: {'FOO': 'BAR'},
+      executable: 'dart_fake',
+      isAndroid: false,
+      isFuchsia: false,
+      isIOS: false,
+      isLinux: false,
+      isMacOS: false,
+      isWindows: true,
+      lineTerminator: '\r\n',
+      operatingSystem: 'windows',
+      pathSeparator: '\\',
+      resolvedExecutable: 'C:\\bin\\dart_fake',
+      version: '3.0.0-fake',
+      script: Uri.file('C:\\bin\\dart_fake.dart'),
+      numberOfProcessors: 2,
+    );
+
+    await withPlatform(() async {
+      expect(platform.operatingSystem, 'windows');
+      expect(platform.isWindows, isTrue);
+      expect(platform.isLinux, isFalse);
+      expect(platform.environment['FOO'], 'BAR');
+      expect(platform.executable, 'dart_fake');
+      expect(platform.pathSeparator, '\\');
+    }, platform: fakePlatform);
+
+    expect(platform.operatingSystem, originalOS);
+  }, testOn: 'vm');
+
+  test('overriding works (also in browser)', () async {
+    final fakePlatform = PlatformInfo.override(
+      environment: {'FOO': 'BAR'},
+      executable: 'dart_fake',
+      isAndroid: false,
+      isFuchsia: false,
+      isIOS: false,
+      isLinux: false,
+      isMacOS: false,
+      isWindows: true,
+      lineTerminator: '\r\n',
+      operatingSystem: 'windows',
+      pathSeparator: '\\',
+      resolvedExecutable: 'C:\\bin\\dart_fake',
+      version: '3.0.0-fake',
+      script: Uri.file('C:\\bin\\dart_fake.dart'),
+      numberOfProcessors: 2,
+    );
+
+    await withPlatform(() async {
+      expect(platform.operatingSystem, 'windows');
+      expect(platform.isWindows, isTrue);
+      expect(platform.isLinux, isFalse);
+      expect(platform.environment['FOO'], 'BAR');
+      expect(platform.executable, 'dart_fake');
+      expect(platform.pathSeparator, '\\');
+    }, platform: fakePlatform);
+  });
+
+  test('Platform is not used outside platform_info.dart', () async {
+    // This test exists to ensure that we don't use Platform from dart:io
+    // unintentionally. We only want to use it in lib/src/platform_info.dart!
+    // Everywhere else we should rely on `platform` from here.
+    // This way, we can overrride the platform when we need to.
+    final allowListedFiles = [
+      'lib/src/platform_info.dart',
+      'test/platform_info_test.dart',
+    ];
+
+    final root = p.normalize(p.absolute('.'));
+    final collection = AnalysisContextCollection(
+      includedPaths: [p.join(root, 'lib')],
+    );
+
+    for (final context in collection.contexts) {
+      for (final filePath in context.contextRoot.analyzedFiles()) {
+        if (!filePath.endsWith('.dart')) continue;
+
+        // Skip allow listed files
+        if (allowListedFiles.contains(p.relative(filePath, from: root))) {
+          continue;
+        }
+
+        final result = await context.currentSession.getResolvedUnit(filePath);
+        if (result is ResolvedUnitResult) {
+          SourceSpan? first;
+          result.unit.accept(
+            ForEachIdentifier((element) {
+              if (first == null &&
+                  element.element?.name == 'Platform' &&
+                  element.element?.library?.name == 'dart.io') {
+                first = SourceFile.fromString(
+                  result.content,
+                  url: filePath,
+                ).span(element.offset, element.end);
+              }
+            }),
+          );
+          if (first != null) {
+            fail(
+              first!.message(
+                'Found Platform usage from dart:io, '
+                'use lib/src/platform_info.dart instead.',
+              ),
+            );
+          }
+        }
+      }
+    }
+  }, testOn: 'vm && linux');
+}
+
+final class ForEachIdentifier extends GeneralizingAstVisitor<void> {
+  final void Function(Identifier element) _visitIdentifier;
+  ForEachIdentifier(this._visitIdentifier);
+
+  @override
+  void visitComment(Comment node) {
+    // Do not walk into comments! They are allowed to reference Platform!
+  }
+
+  @override
+  void visitIdentifier(Identifier element) {
+    _visitIdentifier(element);
+    super.visitIdentifier(element);
+  }
+}
diff --git a/tool/test.dart b/tool/test.dart
index f767263..700a2b6 100755
--- a/tool/test.dart
+++ b/tool/test.dart
@@ -14,9 +14,9 @@
 
 import 'dart:io';
 
+import 'package:path/path.dart' as p;
 import 'package:pub/src/dart.dart';
 import 'package:pub/src/exceptions.dart';
-import 'package:pub/src/path.dart';
 
 Future<void> main(List<String> args) async {
   if (Platform.environment['FLUTTER_ROOT'] != null) {