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.