Avoid transitive imports to package:analyzer (#2712)

Closes https://github.com/dart-lang/sdk/issues/63923

The analyzer package sources dominate the compile time for small tests.
Avoid importing the analyzer transitively from `package:test/test.dart`.

Move the tests of the `test_api` import restrictions to the integration
test directory so it's easier to reuse for tests across the packages.
Add a test that `package:test/test.dart` does not transitively import
any library under the analyzer package.

Use a more narrow import of the parts of the `Platform` library which
are needed in the JSON reporter implementation.
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index 180e453..6c49c9b 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -43,16 +43,16 @@
       - name: mono_repo self validate
         run: dart pub global run mono_repo generate --validate
   job_002:
-    name: "analyze_and_format; linux; Dart 3.11.0; PKGS: integration_tests/regression, integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    name: "analyze_and_format; linux; Dart 3.11.0; PKGS: integration_tests/regression, pkgs/checks, pkgs/checks_codegen, pkgs/matcher, pkgs/test_core; `dart analyze`"
     runs-on: ubuntu-latest
     steps:
       - name: Cache Pub hosted dependencies
         uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
         with:
           path: "~/.pub-cache/hosted"
-          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/regression-integration_tests/wasm;commands:format-analyze_0"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/regression-pkgs/checks-pkgs/checks_codegen-pkgs/matcher-pkgs/test_core;commands:analyze_1"
           restore-keys: |
-            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/regression-integration_tests/wasm
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/regression-pkgs/checks-pkgs/checks_codegen-pkgs/matcher-pkgs/test_core
             os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0
             os:ubuntu-latest;pub-cache-hosted
             os:ubuntu-latest
@@ -70,50 +70,10 @@
         run: dart pub upgrade
         if: "always() && steps.checkout.conclusion == 'success'"
         working-directory: integration_tests/regression
-      - name: "integration_tests/regression; dart format --output=none --set-exit-if-changed ."
-        run: "dart format --output=none --set-exit-if-changed ."
+      - name: integration_tests/regression; dart analyze
+        run: dart analyze
         if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
         working-directory: integration_tests/regression
-      - name: "integration_tests/regression; dart analyze --fatal-infos"
-        run: dart analyze --fatal-infos
-        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
-        working-directory: integration_tests/regression
-      - id: integration_tests_wasm_pub_upgrade
-        name: integration_tests/wasm; dart pub upgrade
-        run: dart pub upgrade
-        if: "always() && steps.checkout.conclusion == 'success'"
-        working-directory: integration_tests/wasm
-      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
-        run: "dart format --output=none --set-exit-if-changed ."
-        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
-        working-directory: integration_tests/wasm
-      - name: "integration_tests/wasm; dart analyze --fatal-infos"
-        run: dart analyze --fatal-infos
-        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
-        working-directory: integration_tests/wasm
-  job_003:
-    name: "analyze_and_format; linux; Dart 3.11.0; PKGS: pkgs/checks, pkgs/checks_codegen, pkgs/matcher, pkgs/test_core; `dart analyze`"
-    runs-on: ubuntu-latest
-    steps:
-      - name: Cache Pub hosted dependencies
-        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
-        with:
-          path: "~/.pub-cache/hosted"
-          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:pkgs/checks-pkgs/checks_codegen-pkgs/matcher-pkgs/test_core;commands:analyze_1"
-          restore-keys: |
-            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:pkgs/checks-pkgs/checks_codegen-pkgs/matcher-pkgs/test_core
-            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0
-            os:ubuntu-latest;pub-cache-hosted
-            os:ubuntu-latest
-      - name: Setup Dart SDK
-        uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260
-        with:
-          sdk: "3.11.0"
-      - id: checkout
-        name: Checkout repository
-        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
-        with:
-          persist-credentials: false
       - id: pkgs_checks_pub_upgrade
         name: pkgs/checks; dart pub upgrade
         run: dart pub upgrade
@@ -150,6 +110,42 @@
         run: dart analyze
         if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
         working-directory: pkgs/test_core
+  job_003:
+    name: "analyze_and_format; linux; Dart 3.11.0; PKG: integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/wasm;commands:format-analyze_0"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0;packages:integration_tests/wasm
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.11.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260
+        with:
+          sdk: "3.11.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
+        with:
+          persist-credentials: false
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
   job_004:
     name: "analyze_and_format; linux; Dart dev; PKGS: integration_tests/regression, integration_tests/spawn_hybrid, integration_tests/wasm, pkgs/checks, pkgs/matcher, pkgs/test, pkgs/test_api, pkgs/test_core; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
     runs-on: ubuntu-latest
diff --git a/integration_tests/regression/mono_pkg.yaml b/integration_tests/regression/mono_pkg.yaml
index 4a60bc1..f2186e4 100644
--- a/integration_tests/regression/mono_pkg.yaml
+++ b/integration_tests/regression/mono_pkg.yaml
@@ -1,9 +1,5 @@
 # See https://pub.dev/packages/mono_repo
 
-sdk:
-- dev
-- pubspec
-
 os:
 - linux
 
@@ -12,5 +8,11 @@
   - group:
     - format
     - analyze: --fatal-infos
+    sdk: dev
+  - group:
+    - analyze
+    sdk: pubspec
 - unit_test:
-  - test
+  - group:
+    - test
+    sdk: [dev, pubspec]
diff --git a/integration_tests/regression/pubspec.yaml b/integration_tests/regression/pubspec.yaml
index b282c8f..66923a7 100644
--- a/integration_tests/regression/pubspec.yaml
+++ b/integration_tests/regression/pubspec.yaml
@@ -4,4 +4,7 @@
   sdk: ^3.11.0
 resolution: workspace
 dependencies:
+  analyzer: any
+  package_config: any
+  path: any
   test: any
diff --git a/integration_tests/regression/test/import_restrictions_test.dart b/integration_tests/regression/test/import_restrictions_test.dart
new file mode 100644
index 0000000..05be3f5
--- /dev/null
+++ b/integration_tests/regression/test/import_restrictions_test.dart
@@ -0,0 +1,244 @@
+// 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.
+
+@TestOn('vm && !exe')
+library;
+
+import 'dart:collection';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:analyzer/dart/analysis/utilities.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  late _ImportCheck importCheck;
+  setUpAll(() async {
+    importCheck = await .create();
+  });
+
+  group('test_api', () {
+    test('backend must not import from other subdirectories', () async {
+      final entryPoints = [
+        _testApiLibrary('backend.dart'),
+        ...await _ImportCheck.findEntrypointsUnder(
+          _testApiLibrary('src/backend'),
+        ),
+      ];
+      await for (final source in importCheck.transitiveSamePackageSources(
+        entryPoints,
+      )) {
+        for (final import in source.imports) {
+          expect(import.pathSegments.skip(1).take(2), [
+            'src',
+            'backend',
+          ], reason: 'Invalid import from ${source.uri} : $import');
+        }
+      }
+    });
+  });
+
+  group('test entrypoint', () {
+    test('must not transitively import package:analyzer', () async {
+      final path = await importCheck.findPathToPackage(
+        startUri: .parse('package:test/test.dart'),
+        targetPackage: 'analyzer',
+      );
+      expect(
+        path,
+        isNull,
+        reason:
+            'package:test/test.dart must not transitively import '
+            'package:analyzer.\n'
+            'Found path:\n'
+            '${path?.map((u) => '  -> $u').join('\n')}',
+      );
+    });
+  });
+}
+
+Uri _testApiLibrary(String path) => .parse('package:test_api/$path');
+
+class _ImportCheck {
+  final PackageConfig _packageConfig;
+
+  _ImportCheck._(this._packageConfig);
+
+  static Future<_ImportCheck> create() async {
+    final packageUri = await Isolate.resolvePackageUri(
+      .parse('package:test_api/'),
+    );
+    final packageConfig = await findPackageConfig(.fromUri(packageUri!));
+    if (packageConfig == null) {
+      throw StateError('Could not find package_config.json.');
+    }
+    return ._(packageConfig);
+  }
+
+  static Future<Iterable<Uri>> findEntrypointsUnder(Uri uri) async {
+    if (!uri.path.endsWith('/')) {
+      uri = uri.replace(path: '${uri.path}/');
+    }
+    final directoryPath = p.fromUri(await Isolate.resolvePackageUri(uri));
+    final directory = Directory(directoryPath);
+    return directory
+        .listSync(recursive: true)
+        .whereType<File>()
+        .where((f) => f.path.endsWith('.dart'))
+        .map((f) {
+          final relativePath = p.relative(f.path, from: directoryPath);
+          final relativeUri = p.toUri(relativePath);
+          return uri.resolveUri(relativeUri);
+        });
+  }
+
+  Stream<_Source> transitiveSamePackageSources(
+    Iterable<Uri> entryPoints,
+  ) async* {
+    assert(entryPoints.every((e) => e.scheme == 'package'));
+    final package = entryPoints.first.pathSegments.first;
+    assert(entryPoints.skip(1).every((e) => e.pathSegments.first == package));
+
+    final queue = Queue<Uri>()..addAll(entryPoints);
+    final seen = <Uri>{...entryPoints};
+
+    while (queue.isNotEmpty) {
+      final current = queue.removeFirst();
+
+      final imports = await _findImports(current);
+      final samePackageImports = imports
+          .where(
+            (u) =>
+                u.isScheme('package') &&
+                u.pathSegments.isNotEmpty &&
+                u.pathSegments.first == package,
+          )
+          .toSet();
+
+      yield _Source(current, samePackageImports);
+
+      samePackageImports.where(seen.add).forEach(queue.add);
+    }
+  }
+
+  Future<List<Uri>?> findPathToPackage({
+    required Uri startUri,
+    required String targetPackage,
+  }) async {
+    var queue = Queue<Uri>()..add(startUri);
+    var parents = <Uri, Uri?>{startUri: null};
+
+    List<Uri> reconstructPath(Uri end) => [
+      for (Uri? current = end; current != null; current = parents[current])
+        current,
+    ].reversed.toList();
+
+    while (queue.isNotEmpty) {
+      var current = queue.removeFirst();
+
+      if (current.isScheme('package') &&
+          current.pathSegments.isNotEmpty &&
+          current.pathSegments.first == targetPackage) {
+        return reconstructPath(current);
+      }
+
+      List<Uri> neighbors;
+      try {
+        neighbors = await _findImports(current);
+      } catch (e) {
+        continue;
+      }
+
+      for (var neighbor in neighbors) {
+        if (parents.containsKey(neighbor)) continue;
+        parents[neighbor] = current;
+        queue.add(neighbor);
+
+        if (neighbor.isScheme('package') &&
+            neighbor.pathSegments.isNotEmpty &&
+            neighbor.pathSegments.first == targetPackage) {
+          return reconstructPath(neighbor);
+        }
+      }
+    }
+
+    return null;
+  }
+
+  Future<List<Uri>> _findImports(Uri from) async {
+    Uri? fileUri;
+    if (from.isScheme('package')) {
+      fileUri = _packageConfig.resolve(from);
+    } else if (from.isScheme('file')) {
+      fileUri = from;
+    } else {
+      throw StateError('Unsupported URI scheme: $from');
+    }
+
+    if (fileUri == null) {
+      throw StateError('Could not resolve: $from');
+    }
+
+    final path = fileUri.toFilePath();
+    final file = File(path);
+    if (!file.existsSync()) return [];
+
+    final parseResult = parseString(
+      content: file.readAsStringSync(),
+      path: path,
+      throwIfDiagnostics: false,
+    );
+
+    final unit = parseResult.unit;
+    var uris = <Uri>[];
+
+    for (var directive in unit.directives) {
+      if (directive is NamespaceDirective) {
+        String? selectedUri;
+        for (var config in directive.configurations) {
+          var name = config.name.toSource();
+          var value = config.value?.stringValue ?? 'true';
+
+          bool? conditionValue;
+          if (name == 'dart.library.io' ||
+              name == 'dart.library.cli' ||
+              name == 'dart.library.isolate') {
+            conditionValue = true;
+          } else if (name == 'dart.library.html' ||
+              name == 'dart.library.js' ||
+              name == 'dart.library.js_interop') {
+            conditionValue = false;
+          }
+
+          if (conditionValue != null && conditionValue.toString() == value) {
+            selectedUri = config.uri.stringValue;
+            break;
+          }
+        }
+        selectedUri ??= directive.uri.stringValue;
+        if (selectedUri != null) {
+          uris.add(from.resolve(selectedUri));
+        }
+      } else if (directive is PartDirective) {
+        if (directive.uri.stringValue != null) {
+          uris.add(from.resolve(directive.uri.stringValue!));
+        }
+      }
+    }
+
+    return uris
+        .where((u) => !u.isScheme('dart') && !u.isScheme('dart-mac'))
+        .toList();
+  }
+}
+
+class _Source {
+  final Uri uri;
+  final Set<Uri> imports;
+
+  _Source(this.uri, this.imports);
+}
diff --git a/pkgs/test_api/test/import_restrictions_test.dart b/pkgs/test_api/test/import_restrictions_test.dart
deleted file mode 100644
index 9256d4c..0000000
--- a/pkgs/test_api/test/import_restrictions_test.dart
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (c) 2021, 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.
-
-@TestOn('!exe')
-library;
-
-import 'dart:async';
-import 'dart:io';
-import 'dart:isolate';
-
-import 'package:analyzer/dart/analysis/analysis_context.dart';
-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:glob/glob.dart';
-import 'package:glob/list_local_fs.dart';
-import 'package:graphs/graphs.dart';
-import 'package:path/path.dart' as p;
-import 'package:test/test.dart';
-
-void main() {
-  late _ImportCheck importCheck;
-  setUpAll(() async {
-    importCheck = await _ImportCheck.create();
-  });
-  group('backend', () {
-    test('must not import from other subdirectories', () async {
-      final entryPoints = [
-        _testApiLibrary('backend.dart'),
-        ...await _ImportCheck.findEntrypointsUnder(
-          _testApiLibrary('src/backend'),
-        ),
-      ];
-      await for (final source in importCheck.transitiveSamePackageSources(
-        entryPoints,
-      )) {
-        for (final import in source.imports) {
-          expect(import.pathSegments.skip(1).take(2), [
-            'src',
-            'backend',
-          ], reason: 'Invalid import from ${source.uri} : $import');
-        }
-      }
-    });
-  });
-}
-
-Uri _testApiLibrary(String path) => Uri.parse('package:test_api/$path');
-
-class _ImportCheck {
-  final AnalysisContext _context;
-
-  static Future<Iterable<Uri>> findEntrypointsUnder(Uri uri) async {
-    if (!uri.path.endsWith('/')) {
-      uri = uri.replace(path: '${uri.path}/');
-    }
-    final directory = p.fromUri(await Isolate.resolvePackageUri(uri));
-    return Glob('./**')
-        .listSync(root: directory)
-        .whereType<File>()
-        .map((f) => uri.resolve(p.url.relative(f.path, from: directory)));
-  }
-
-  static Future<_ImportCheck> create() async {
-    final context = await _createAnalysisContext();
-    return _ImportCheck._(context);
-  }
-
-  static Future<AnalysisContext> _createAnalysisContext() async {
-    final libUri = Uri.parse('package:graphs/');
-    final libPath = await _pathForUri(libUri);
-    final packagePath = p.dirname(libPath);
-
-    final contexts = AnalysisContextCollection(
-      includedPaths: [packagePath],
-    ).contexts;
-    if (contexts.length != 1) {
-      throw StateError('Expected to find exactly one context, got $contexts');
-    }
-    return contexts.first;
-  }
-
-  static Future<String> _pathForUri(Uri uri) async {
-    final fileUri = await Isolate.resolvePackageUri(uri);
-    if (fileUri == null || !fileUri.isScheme('file')) {
-      throw StateError('Expected to resolve $uri to a file URI, got $fileUri');
-    }
-    return p.fromUri(fileUri);
-  }
-
-  _ImportCheck._(this._context);
-
-  Stream<_Source> transitiveSamePackageSources(Iterable<Uri> entryPoints) {
-    assert(entryPoints.every((e) => e.scheme == 'package'));
-    final package = entryPoints.first.pathSegments.first;
-    assert(entryPoints.skip(1).every((e) => e.pathSegments.first == package));
-    return crawlAsync<Uri, _Source>(
-      entryPoints,
-      (uri) async => _Source(uri, await _findImports(uri, package)),
-      (_, source) => source.imports,
-    );
-  }
-
-  Future<Set<Uri>> _findImports(Uri uri, String restrictToPackage) async {
-    var path = await _pathForUri(uri);
-    var analysisSession = _context.currentSession;
-    var parseResult = analysisSession.getParsedUnit(path) as ParsedUnitResult;
-    assert(
-      parseResult.content.isNotEmpty,
-      'Tried to read an invalid library $uri',
-    );
-    return parseResult.unit.directives
-        .whereType<UriBasedDirective>()
-        .map((d) => d.uri.stringValue!)
-        .where((uri) => !uri.startsWith('dart:'))
-        .map((import) => _resolveImport(import, uri))
-        .where((import) => import.pathSegments.first == restrictToPackage)
-        .toSet();
-  }
-
-  static Uri _resolveImport(String import, Uri from) {
-    if (import.startsWith('package:')) return Uri.parse(import);
-    assert(from.scheme == 'package');
-    final package = from.pathSegments.first;
-    final fromPath = p.joinAll(from.pathSegments.skip(1));
-    final path = p.normalize(p.join(p.dirname(fromPath), import));
-    return Uri.parse('package:${p.join(package, path)}');
-  }
-}
-
-class _Source {
-  final Uri uri;
-  final Set<Uri> imports;
-
-  _Source(this.uri, this.imports);
-}
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index a2a81b4..7f7eb35 100644
--- a/pkgs/test_core/lib/src/runner/reporter/json.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -9,6 +9,7 @@
 import 'package:collection/collection.dart';
 import 'package:path/path.dart' as p;
 import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/backend.dart' show SuitePlatform;
 import 'package:test_api/hooks.dart' show TestFailure;
 import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
@@ -18,10 +19,11 @@
 import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/test_location.dart'; // ignore: implementation_imports
 
-import '../../platform.dart';
 import '../engine.dart';
 import '../load_suite.dart';
 import '../reporter.dart';
+import '../runner_suite.dart' show RunnerSuite;
+import '../suite.dart' show SuiteConfiguration;
 import '../version.dart';
 
 /// A reporter that prints machine-readable JSON-formatted test results.