feat(api_summary): Move api_summary package into the tools monorepo (#2412)
diff --git a/.github/workflows/api_summary.yaml b/.github/workflows/api_summary.yaml
new file mode 100644
index 0000000..1213569
--- /dev/null
+++ b/.github/workflows/api_summary.yaml
@@ -0,0 +1,44 @@
+name: package:api_summary
+permissions: read-all
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/api_summary.yaml'
+ - 'pkgs/api_summary/**'
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/api_summary.yaml'
+ - 'pkgs/api_summary/**'
+ schedule:
+ - cron: '0 0 * * 0' # weekly
+
+defaults:
+ run:
+ working-directory: pkgs/api_summary
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: ['3.12', dev]
+ include:
+ - sdk: dev
+ check-formatting: true
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260
+ with:
+ sdk: ${{matrix.sdk}}
+
+ - run: dart pub get
+
+ - run: dart analyze --fatal-infos
+
+ - run: dart format --output=none --set-exit-if-changed .
+ if: ${{matrix.check-formatting}}
+
+ - run: dart test
diff --git a/README.md b/README.md
index 90c4818..f372290 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
| Package | Description | Issues | Version |
| --- | --- | --- | --- |
+| [api_summary](pkgs/api_summary/) | Creates an API summary for a package. | [][api_summary_issues] | [](https://pub.dev/packages/api_summary) |
| [bazel_worker](pkgs/bazel_worker/) | Protocol and utilities to implement or invoke persistent bazel workers. | [][bazel_worker_issues] | [](https://pub.dev/packages/bazel_worker) |
| [benchmark_harness](pkgs/benchmark_harness/) | The official Dart project benchmark harness. | [][benchmark_harness_issues] | [](https://pub.dev/packages/benchmark_harness) |
| [boolean_selector](pkgs/boolean_selector/) | A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax. | [][boolean_selector_issues] | [](https://pub.dev/packages/boolean_selector) |
@@ -56,6 +57,7 @@
| [yaml](pkgs/yaml/) | A parser for YAML, a human-friendly data serialization standard | [][yaml_issues] | [](https://pub.dev/packages/yaml) |
| [yaml_edit](pkgs/yaml_edit/) | A library for YAML manipulation with comment and whitespace preservation. | [][yaml_edit_issues] | [](https://pub.dev/packages/yaml_edit) |
+[api_summary_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aapi_summary
[bazel_worker_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Abazel_worker
[benchmark_harness_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Abenchmark_harness
[boolean_selector_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aboolean_selector
diff --git a/pkgs/api_summary/CHANGELOG.md b/pkgs/api_summary/CHANGELOG.md
new file mode 100644
index 0000000..d4f1a2a
--- /dev/null
+++ b/pkgs/api_summary/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0-wip
+
+- First release.
diff --git a/pkgs/api_summary/LICENSE b/pkgs/api_summary/LICENSE
new file mode 100644
index 0000000..9035a41
--- /dev/null
+++ b/pkgs/api_summary/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2026, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/api_summary/README.md b/pkgs/api_summary/README.md
new file mode 100644
index 0000000..2f9a7a5
--- /dev/null
+++ b/pkgs/api_summary/README.md
@@ -0,0 +1,161 @@
+[](https://github.com/dart-lang/tools/actions/workflows/api_summary.yaml)
+[](https://pub.dev/packages/api_summary)
+[](https://pub.dev/packages/api_summary/publisher)
+
+A library and command-line tool to create a human-readable text summary of the
+public API of a Dart package. This is highly suitable for tracking the public
+API footprints and ensuring that API modifications are visible during code
+reviews (e.g. using `diff` tests).
+
+> [!NOTE]
+> For robust breaking change tracking, you should use
+> [dart_apitool](https://pub.dev/packages/dart_apitool).
+
+## Command Line Usage
+
+You can use the command-line tool to generate an API summary for any package
+containing a `pubspec.yaml` file.
+
+### Installing Globally
+
+Activate the package using `dart pub global`:
+
+```bash
+dart pub global activate api_summary
+```
+
+Then run the tool:
+
+```bash
+api_summary --package-path /path/to/your/package
+```
+
+Or, to run against the package in the current working directory, just run:
+
+```bash
+api_summary
+```
+
+### Running via `dart run`
+
+Alternatively, you can run the executable from within a package directory if
+`api_summary` is a dependency:
+
+```bash
+dart run api_summary
+```
+
+### Options
+
+* `-p, --package-path`: The path to the package directory to summarize
+ (defaults to the current working directory).
+* `-h, --help`: Prints usage instructions.
+
+## Programmatic Usage
+
+You can also use this package programmatically inside your Dart projects, such
+as in automated testing or continuous integration scripts.
+
+Add `api_summary` to your `pubspec.yaml`:
+
+```yaml
+dependencies:
+ api_summary: ^0.1.0-wip
+```
+
+### Basic Example
+
+Call the `summarizePackage` function to generate a package's public API
+representation:
+
+```dart
+import 'dart:io';
+import 'package:api_summary/api_summary.dart';
+
+void main() async {
+ final summary = await summarizePackage('/path/to/package', 'my_package');
+ print(summary);
+}
+```
+
+An executable programmatic example is also available in the
+[example/example.dart](example/example.dart) file.
+
+### Customizing the Summary
+
+Extend the `ApiSummaryCustomizer` class to customize what is displayed in the
+API summary. For example, to exclude specific public classes or only display
+details of certain elements:
+
+```dart
+import 'package:api_summary/api_summary.dart';
+import 'package:analyzer/dart/element/element.dart';
+
+base class MyCustomizer extends ApiSummaryCustomizer {
+ @override
+ bool shouldShowDetails(Element element) {
+ // Exclude elements named 'InternalHelper' from details printout
+ if (element.name == 'InternalHelper') {
+ return false;
+ }
+ return super.shouldShowDetails(element);
+ }
+}
+
+void main() async {
+ final summary = await summarizePackage(
+ '/path/to/package',
+ 'my_package',
+ createCustomizer: () => MyCustomizer(),
+ );
+ print(summary);
+}
+```
+
+## Golden File / Diff Testing
+
+A common best practice with `api_summary` is to verify in a unit test that the
+generated summary matches a checked-in golden file (e.g. `api.txt`). If a
+developer introduces an accidental breaking change or adds a new public API
+element, the test will fail on the `diff`, prompting them to audit and
+intentionally update the golden file.
+
+Below is an example of such a test (available in the
+[test/app_test.dart](test/app_test.dart) file):
+
+```dart
+import 'dart:io';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:api_summary/api_summary.dart';
+
+void main() {
+ test('public API has not changed unexpectedly', () async {
+ final packageDir = Directory.current.path;
+ final goldenFile = File(p.join(packageDir, 'api.txt'));
+
+ final actualOutput = await summarizePackage(packageDir, 'my_package');
+
+ if (!goldenFile.existsSync()) {
+ // In a new setup or after updates, generate the golden file first
+ goldenFile.writeAsStringSync(actualOutput);
+ fail('Golden file api.txt did not exist and has been generated. Please review and commit it.');
+ }
+
+ final expectedOutput = goldenFile.readAsStringSync();
+ expect(actualOutput, equals(expectedOutput));
+ });
+}
+```
+
+## Status: experimental
+
+**NOTE**: This package is currently experimental and published under the
+[tools.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to
+solicit feedback.
+
+These packages have a much higher expected rate of API and breaking changes.
+
+Your feedback is valuable and will help us evolve this package. For general
+feedback, suggestions, and comments, please file an issue in the
+[bug tracker](https://github.com/dart-lang/tools/issues).
diff --git a/pkgs/api_summary/analysis_options.yaml b/pkgs/api_summary/analysis_options.yaml
new file mode 100644
index 0000000..ae9be57
--- /dev/null
+++ b/pkgs/api_summary/analysis_options.yaml
@@ -0,0 +1,22 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_catches_without_on_clauses
+ - avoid_unused_constructor_parameters
+ - cancel_subscriptions
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_in_for_each
+ - prefer_final_locals
+ - simple_directive_paths
+ - unnecessary_await_in_return
+ - unnecessary_ignore
diff --git a/pkgs/api_summary/api.txt b/pkgs/api_summary/api.txt
new file mode 100644
index 0000000..7f6a593
--- /dev/null
+++ b/pkgs/api_summary/api.txt
@@ -0,0 +1,25 @@
+package:api_summary/api_summary.dart:
+ summarizePackage (function: Future<String> Function(String, String, {ApiSummaryCustomizer Function()? createCustomizer}))
+ ApiSummaryCustomizer (class extends Object, base):
+ new (constructor: ApiSummaryCustomizer Function())
+ analysisContext= (setter: AnalysisContext)
+ packageName= (setter: String)
+ publicApiLibraries= (setter: Iterable<LibraryElement>)
+ topLevelPublicElements (getter: Set<Element>)
+ topLevelPublicElements= (setter: Set<Element>)
+ initialScanComplete (method: Future<void> Function())
+ setupComplete (method: Future<void> Function())
+ shouldShowDetails (method: bool Function(Element))
+dart:async:
+ Future (referenced)
+dart:core:
+ Iterable (referenced)
+ Object (referenced)
+ Set (referenced)
+ String (referenced)
+ bool (referenced)
+package:analyzer/dart/analysis/analysis_context.dart:
+ AnalysisContext (referenced)
+package:analyzer/dart/element/element.dart:
+ Element (referenced)
+ LibraryElement (referenced)
diff --git a/pkgs/api_summary/bin/api_summary.dart b/pkgs/api_summary/bin/api_summary.dart
new file mode 100644
index 0000000..54002b0
--- /dev/null
+++ b/pkgs/api_summary/bin/api_summary.dart
@@ -0,0 +1,81 @@
+// 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:io';
+
+import 'package:api_summary/api_summary.dart';
+import 'package:args/args.dart';
+import 'package:path/path.dart' as p;
+import 'package:yaml/yaml.dart';
+
+Future<void> main(List<String> arguments) async {
+ try {
+ final results = parser.parse(arguments);
+
+ if (results.flag('help')) {
+ print('Usage: api_summary [options]');
+ print(parser.usage);
+ return;
+ }
+
+ final packagePath =
+ results.option('package-path') ?? Directory.current.path;
+ final absolutePath = p.normalize(p.absolute(packagePath));
+ final pubspecFile = File(p.join(absolutePath, 'pubspec.yaml'));
+
+ if (!pubspecFile.existsSync()) {
+ stderr.writeln('Error: No pubspec.yaml found at "$absolutePath".');
+ exitCode = 1;
+ return;
+ }
+
+ final packageName = _extractPackageName(pubspecFile);
+ final summary = await summarizePackage(absolutePath, packageName);
+ stdout.write(summary);
+ } on FormatException catch (e) {
+ stderr.writeln('Error: ${e.message}');
+ stderr.writeln('\nUsage: api_summary [options]');
+ stderr.writeln(parser.usage);
+ exitCode = 64;
+ return;
+ }
+}
+
+final parser = ArgParser()
+ ..addOption(
+ 'package-path',
+ abbr: 'p',
+ help:
+ 'The path to the package to summarize. Defaults to the current '
+ 'directory.',
+ )
+ ..addFlag(
+ 'help',
+ abbr: 'h',
+ help: 'Print this usage information.',
+ negatable: false,
+ );
+
+String _extractPackageName(File pubspecFile) {
+ final content = pubspecFile.readAsStringSync();
+ final yaml = loadYaml(content);
+ if (yaml is! Map) {
+ throw ArgumentError(
+ 'Expected pubspec.yaml at ${pubspecFile.path} to be a YAML map.',
+ );
+ }
+ final name = yaml['name'];
+ if (name == null) {
+ throw ArgumentError(
+ 'Could not find a "name" field in pubspec.yaml at ${pubspecFile.path}.',
+ );
+ }
+ if (name is! String) {
+ throw ArgumentError(
+ 'The "name" field in pubspec.yaml at ${pubspecFile.path} must be a '
+ 'String.',
+ );
+ }
+ return name;
+}
diff --git a/pkgs/api_summary/example/example.dart b/pkgs/api_summary/example/example.dart
new file mode 100644
index 0000000..e84ec90
--- /dev/null
+++ b/pkgs/api_summary/example/example.dart
@@ -0,0 +1,20 @@
+// 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:io';
+import 'package:api_summary/api_summary.dart';
+import 'package:path/path.dart' as p;
+
+void main() async {
+ // Locate this package's root directory (current working directory)
+ final packagePath = p.normalize(p.absolute(Directory.current.path));
+
+ print('Generating API summary for the api_summary package...\n');
+
+ // Call summarizePackage to get the public API footprints
+ final summary = await summarizePackage(packagePath, 'api_summary');
+
+ // Output the generated summary to stdout
+ print(summary);
+}
diff --git a/pkgs/api_summary/lib/api_summary.dart b/pkgs/api_summary/lib/api_summary.dart
new file mode 100644
index 0000000..f38e53f
--- /dev/null
+++ b/pkgs/api_summary/lib/api_summary.dart
@@ -0,0 +1,52 @@
+// 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/file_system/physical_file_system.dart';
+import 'src/api_description.dart';
+import 'src/api_summary_customizer.dart';
+import 'src/node.dart';
+
+export 'src/api_summary_customizer.dart' show ApiSummaryCustomizer;
+
+/// Creates a human-readable text summary of the public API of a package, in a
+/// format suitable for auditing with a `diff` tool.
+///
+/// [packagePath] is the path to the directory containing the package's
+/// `pubspec.yaml` file.
+///
+/// [packageName] is the name of the package.
+///
+/// If [createCustomizer] is provided, it will be called to create an instance
+/// of [ApiSummaryCustomizer] which will be used to customize the behavior of
+/// the tool.
+Future<String> summarizePackage(
+ String packagePath,
+ String packageName, {
+ ApiSummaryCustomizer Function()? createCustomizer,
+}) async {
+ final provider = PhysicalResourceProvider.INSTANCE;
+ final libPath = provider.pathContext.join(packagePath, 'lib');
+ final collection = AnalysisContextCollection(
+ resourceProvider: provider,
+ includedPaths: [libPath],
+ );
+ if (collection.contexts.isEmpty) {
+ throw ArgumentError('No analysis context found for "$packagePath".');
+ }
+ if (collection.contexts.length > 1) {
+ throw ArgumentError(
+ 'Multiple analysis contexts found for "$packagePath". '
+ 'Only a single package is supported.',
+ );
+ }
+ final context = collection.contexts.single;
+ final publicApi = ApiDescription(
+ packageName,
+ createCustomizer?.call() ?? ApiSummaryCustomizer(),
+ );
+ final stringBuffer = StringBuffer();
+ printNodes(stringBuffer, await publicApi.build(context));
+ return stringBuffer.toString();
+}
diff --git a/pkgs/api_summary/lib/src/api_description.dart b/pkgs/api_summary/lib/src/api_description.dart
new file mode 100644
index 0000000..7f88e35
--- /dev/null
+++ b/pkgs/api_summary/lib/src/api_description.dart
@@ -0,0 +1,492 @@
+// 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:collection';
+
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:collection/collection.dart';
+
+import 'api_summary_customizer.dart';
+import 'extensions.dart';
+import 'member_sorting.dart';
+import 'node.dart';
+import 'unique_namer.dart';
+import 'uri_sorting.dart';
+
+/// Data structure keeping track of a package's API while walking it to produce
+/// `api.txt`.
+class ApiDescription {
+ final ApiSummaryCustomizer _customizer;
+
+ final String _pkgName;
+
+ /// Top level elements that have already had their child elements dumped.
+ ///
+ /// If an element is seen again in a different library, it will be followed
+ /// with `(see above)` (rather than having its child elements dumped twice).
+ final _dumpedTopLevelElements = <Element>{};
+
+ /// Top level elements that have been referenced so far and haven't yet been
+ /// processed by [build].
+ ///
+ /// This is used to ensure that all elements referred to by the public API
+ /// (e.g., by being mentioned in the type of an API element) also show up in
+ /// the output.
+ final _potentiallyDanglingReferences = Queue<Element>();
+
+ final _uniqueNamer = UniqueNamer();
+
+ /// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to
+ /// avoid unnecessary recomputation.
+ final _immediateSubinterfaceCache =
+ <LibraryElement, Map<ClassElement, Set<InterfaceElement>>>{};
+
+ ApiDescription(this._pkgName, this._customizer);
+
+ /// Builds a list of [Node] objects representing all the libraries that are
+ /// relevant to the package's public API.
+ ///
+ /// This includes libraries that are in the package's public API as well as
+ /// libraries that are referenced by the package's public API (either by being
+ /// re-exported as part of the package's public API, or by being used as part
+ /// of the type of something in the public API).
+ ///
+ /// Each library node is pared with a [UriSortKey] indicating the order in
+ /// which the nodes should be output.
+ Future<List<(UriSortKey, Node)>> build(AnalysisContext context) async {
+ _customizer.packageName = _pkgName;
+ _customizer.analysisContext = context;
+ await _customizer.setupComplete();
+
+ // First, find all the libraries comprising the package's public API, and
+ // all the top level elements they export.
+ final publicApiLibraries = <LibraryElement>[];
+ final topLevelPublicElements = <Element>{};
+ for (final file in context.contextRoot.analyzedFiles().sorted()) {
+ if (!file.endsWith('.dart')) continue;
+ final someFileResult = context.currentSession.getFile(file);
+ if (someFileResult is! FileResult) continue;
+ final fileResult = someFileResult;
+ final uri = fileResult.uri;
+ if (fileResult.isLibrary && uri.isInPublicLibOf(_pkgName)) {
+ final someLibraryResult = await context.currentSession
+ .getResolvedLibrary(file);
+ if (someLibraryResult is! ResolvedLibraryResult) continue;
+ final library = someLibraryResult.element;
+ topLevelPublicElements.addAll(
+ library.exportNamespace.definedNames2.values,
+ );
+ publicApiLibraries.add(library);
+ }
+ }
+ _customizer.publicApiLibraries = publicApiLibraries;
+ _customizer.topLevelPublicElements = topLevelPublicElements;
+ await _customizer.initialScanComplete();
+
+ // Then, dump all the libraries in the package's public API.
+ final nodes = <Uri, Node<MemberSortKey>>{};
+ for (final library in publicApiLibraries) {
+ final node = nodes[library.uri] = Node<MemberSortKey>();
+ _dumpLibrary(library, node);
+ }
+
+ // Finally, dump anything referenced by those public libraries.
+ while (_potentiallyDanglingReferences.isNotEmpty) {
+ final element = _potentiallyDanglingReferences.removeFirst();
+ if (!_dumpedTopLevelElements.add(element)) continue;
+ final containingLibraryUri = element.library!.uri;
+ final childNode = Node<MemberSortKey>()
+ ..text.add(_uniqueNamer.name(element));
+ _dumpElement(element, childNode);
+ (nodes[containingLibraryUri] ??= Node<MemberSortKey>()
+ ..text.add('$containingLibraryUri:'))
+ .childNodes
+ .add((MemberSortKey(element), childNode));
+ }
+ return [
+ for (final entry in nodes.entries)
+ (UriSortKey(entry.key, _pkgName), entry.value),
+ ];
+ }
+
+ /// Creates a list of objects which, when their string representations are
+ /// concatenated, describes [type].
+ ///
+ /// The reason we use this method rather than [DartType.toString] is to make
+ /// sure that (a) every element mentioned by the type is added to
+ /// [_potentiallyDanglingReferences], and (b) if an ambiguous name is used,
+ /// the ambiguity will be taken care of by [_uniqueNamer].
+ List<Object?> _describeType(DartType type) {
+ final suffix = switch (type.nullabilitySuffix) {
+ NullabilitySuffix.none => '',
+ NullabilitySuffix.star => '*',
+ NullabilitySuffix.question => '?',
+ };
+ switch (type) {
+ case DynamicType():
+ return ['dynamic'];
+ case FunctionType(
+ :final returnType,
+ :final typeParameters,
+ :final formalParameters,
+ ):
+ final params = <List<Object?>>[];
+ final optionalParams = <List<Object?>>[];
+ final namedParams = <String, List<Object?>>{};
+ for (final formalParameter in formalParameters) {
+ if (formalParameter.isNamed) {
+ namedParams[formalParameter.name!] = [
+ if (formalParameter.isDeprecated) 'deprecated ',
+ if (formalParameter.isRequired) 'required ',
+ ..._describeType(formalParameter.type),
+ ];
+ } else if (formalParameter.isOptional) {
+ optionalParams.add([
+ if (formalParameter.isDeprecated) 'deprecated ',
+ ..._describeType(formalParameter.type),
+ ]);
+ } else {
+ params.add([
+ if (formalParameter.isDeprecated) 'deprecated ',
+ ..._describeType(formalParameter.type),
+ ]);
+ }
+ }
+ if (optionalParams.isNotEmpty) {
+ params.add(optionalParams.separatedBy(prefix: '[', suffix: ']'));
+ }
+ if (namedParams.isNotEmpty) {
+ params.add(
+ namedParams.entries
+ .sortedBy((e) => e.key)
+ .map((e) => [...e.value, ' ${e.key}'])
+ .separatedBy(prefix: '{', suffix: '}'),
+ );
+ }
+ return <Object?>[
+ ..._describeType(returnType),
+ ' Function',
+ if (typeParameters.isNotEmpty)
+ ...typeParameters
+ .map(_describeTypeParameter)
+ .separatedBy(prefix: '<', suffix: '>'),
+ '(',
+ ...params.separatedBy(),
+ ')',
+ suffix,
+ ];
+ case InterfaceType(:final element, :final typeArguments):
+ _potentiallyDanglingReferences.addLast(element);
+ return [
+ _uniqueNamer.name(element),
+ if (typeArguments.isNotEmpty)
+ ...typeArguments
+ .map(_describeType)
+ .separatedBy(prefix: '<', suffix: '>'),
+ suffix,
+ ];
+ case RecordType(:final positionalFields, :final namedFields):
+ if (positionalFields.length == 1 && namedFields.isEmpty) {
+ return [
+ '(',
+ ..._describeType(positionalFields[0].type),
+ ',)',
+ suffix,
+ ];
+ }
+ return [
+ ...[
+ for (final positionalField in positionalFields)
+ _describeType(positionalField.type),
+ if (namedFields.isNotEmpty)
+ namedFields
+ .sortedBy((f) => f.name)
+ .map((f) => [..._describeType(f.type), ' ', f.name])
+ .separatedBy(prefix: '{', suffix: '}'),
+ ].separatedBy(prefix: '(', suffix: ')'),
+ suffix,
+ ];
+ case TypeParameterType(:final element):
+ return [element.name!, suffix];
+ case VoidType():
+ return ['void'];
+ case dynamic(:final runtimeType):
+ throw UnimplementedError('Unexpected type: $runtimeType');
+ }
+ }
+
+ /// Creates a list of objects which, when their string representations are
+ /// concatenated, describes [typeParameter].
+ List<Object?> _describeTypeParameter(TypeParameterElement typeParameter) => [
+ typeParameter.name!,
+ if (typeParameter.bound case final bound?) ...[
+ ' extends ',
+ ..._describeType(bound),
+ ],
+ ];
+
+ /// Appends information to [node] describing [element].
+ void _dumpElement(Element element, Node<MemberSortKey> node) {
+ final enclosingElement = element.enclosingElement;
+ if (enclosingElement is LibraryElement &&
+ !_customizer.shouldShowDetails(element)) {
+ if (!enclosingElement.uri.isIn(_pkgName)) {
+ node.text.add(' (referenced)');
+ } else {
+ node.text.add(' (non-public)');
+ }
+ return;
+ }
+ final parentheticals = <List<Object?>>[];
+ switch (element) {
+ case TypeAliasElement(:final aliasedType, :final typeParameters):
+ final description = <Object?>['type alias'];
+ if (typeParameters.isNotEmpty) {
+ description.addAll(
+ typeParameters
+ .map(_describeTypeParameter)
+ .separatedBy(prefix: '<', suffix: '>'),
+ );
+ }
+ description.addAll([' for ', ..._describeType(aliasedType)]);
+ parentheticals.add(description);
+ case InstanceElement():
+ switch (element) {
+ case InterfaceElement(
+ :final typeParameters,
+ :final supertype,
+ :final interfaces,
+ ):
+ final instanceDescription = <Object?>[
+ switch (element) {
+ ClassElement() => 'class',
+ EnumElement() => 'enum',
+ MixinElement() => 'mixin',
+ ExtensionTypeElement() => 'extension type',
+ dynamic(:final runtimeType) => 'TODO: $runtimeType',
+ },
+ ];
+ if (typeParameters.isNotEmpty) {
+ instanceDescription.addAll(
+ typeParameters
+ .map(_describeTypeParameter)
+ .separatedBy(prefix: '<', suffix: '>'),
+ );
+ }
+ if (element is! EnumElement && supertype != null) {
+ instanceDescription.addAll([
+ ' extends ',
+ ..._describeType(supertype),
+ ]);
+ }
+ if (element is MixinElement &&
+ element.superclassConstraints.isNotEmpty) {
+ instanceDescription.addAll(
+ element.superclassConstraints
+ .map(_describeType)
+ .separatedBy(prefix: ' on '),
+ );
+ }
+ if (interfaces.isNotEmpty) {
+ instanceDescription.addAll(
+ interfaces
+ .map(_describeType)
+ .separatedBy(prefix: ' implements '),
+ );
+ }
+ parentheticals.add(instanceDescription);
+ if (element is ClassElement) {
+ if (element.isSealed) {
+ final parenthetical = <Object>['sealed'];
+ parentheticals.add(parenthetical);
+ if (_getOrComputeImmediateSubinterfaceMap(
+ element.library,
+ )[element]
+ case final subinterfaces?) {
+ parenthetical.add(' (immediate subtypes: ');
+ // Note: it's tempting to just do
+ // `subinterfaces.map(_uniqueNamer.name).join(', ')`, but that
+ // won't work, because the names returned by
+ // `UniqueName.toString()` aren't finalized until we've
+ // visited the entire API and seen if there are class names
+ // that need to be disambiguated. So we accumulate the
+ // `UniqueName` objects into the `parenthetical` list and rely
+ // on `printNodes` converting everything to a string when the
+ // final API description is being output.
+ var commaNeeded = false;
+ for (final subinterface in subinterfaces) {
+ if (commaNeeded) {
+ parenthetical.add(', ');
+ } else {
+ commaNeeded = true;
+ }
+ parenthetical.add(_uniqueNamer.name(subinterface));
+ }
+ parenthetical.add(')');
+ }
+ } else {
+ if (element.isAbstract) {
+ parentheticals.add(['abstract']);
+ }
+ if (element.isBase) {
+ parentheticals.add(['base']);
+ }
+ if (element.isMixinClass) {
+ parentheticals.add(['mixin']);
+ }
+ if (element.isInterface) {
+ parentheticals.add(['interface']);
+ }
+ if (element.isFinal) {
+ parentheticals.add(['final']);
+ }
+ }
+ } else if (element is MixinElement) {
+ if (element.isBase) {
+ parentheticals.add(['base']);
+ }
+ }
+ case ExtensionElement(:final extendedType):
+ parentheticals.add([
+ 'extension on ',
+ ..._describeType(extendedType),
+ ]);
+ case dynamic(:final runtimeType):
+ throw UnimplementedError('Unexpected element: $runtimeType');
+ }
+ for (final member in element.children.sortedBy((m) => m.name ?? '')) {
+ if (member.name case final name? when name.startsWith('_')) {
+ // Ignore private members
+ continue;
+ }
+ if (member is FieldElement) {
+ // Ignore fields; we care about the getters and setters they induce.
+ continue;
+ }
+ if (member is ConstructorElement &&
+ !member.isFactory &&
+ element is ClassElement &&
+ element.isAbstract &&
+ (element.isFinal || element.isInterface || element.isSealed)) {
+ // The class can't be constructed from outside of the library that
+ // declares it, so its generative constructors aren't part of the
+ // public API.
+ continue;
+ }
+ if (member is ConstructorElement &&
+ !member.isFactory &&
+ element is EnumElement) {
+ // Enum generative constructors can't be called from outside the
+ // enum itself, so they aren't part of the public API.
+ continue;
+ }
+ final childNode = Node<MemberSortKey>();
+ childNode.text.add(member.apiName);
+ _dumpElement(member, childNode);
+ node.childNodes.add((MemberSortKey(member), childNode));
+ }
+ case TopLevelFunctionElement(:final type):
+ parentheticals.add(['function: ', ..._describeType(type)]);
+ case ExecutableElement(:final isStatic):
+ final maybeStatic = isStatic ? 'static ' : '';
+ switch (element) {
+ case GetterElement(:final type):
+ parentheticals.add([
+ '${maybeStatic}getter: ',
+ ..._describeType(type.returnType),
+ ]);
+ case SetterElement(:final type):
+ parentheticals.add([
+ '${maybeStatic}setter: ',
+ ..._describeType(type.formalParameters.single.type),
+ ]);
+ case MethodElement(:final type):
+ parentheticals.add([
+ '${maybeStatic}method: ',
+ ..._describeType(type),
+ ]);
+ case ConstructorElement(:final type):
+ parentheticals.add(['constructor: ', ..._describeType(type)]);
+ case dynamic(:final runtimeType):
+ throw UnimplementedError('Unexpected element: $runtimeType');
+ }
+ case dynamic(:final runtimeType):
+ throw UnimplementedError('Unexpected element: $runtimeType');
+ }
+
+ // For synthetic elements such as getters/setters induced by top level
+ // variables and fields, annotations can be found on the corresponding
+ // non-synthetic element.
+ final nonSyntheticElement = element.nonSynthetic;
+ if (nonSyntheticElement.metadata.hasDeprecated) {
+ parentheticals.add(['deprecated']);
+ }
+ if (nonSyntheticElement.metadata.hasExperimental) {
+ parentheticals.add(['experimental']);
+ }
+
+ if (parentheticals.isNotEmpty) {
+ node.text.addAll(parentheticals.separatedBy(prefix: ' (', suffix: ')'));
+ }
+ if (node.childNodes.isNotEmpty) {
+ node.text.add(':');
+ }
+ }
+
+ /// Appends information to [node] describing [library].
+ void _dumpLibrary(LibraryElement library, Node<MemberSortKey> node) {
+ final uri = library.uri;
+ node.text.addAll([uri, ':']);
+ final definedNames = library.exportNamespace.definedNames2;
+ for (final key in definedNames.keys.sorted()) {
+ final element = definedNames[key]!;
+ final childNode = Node<MemberSortKey>()
+ ..text.add(_uniqueNamer.name(element));
+ if (!_dumpedTopLevelElements.add(element)) {
+ childNode.text.add(' (see above)');
+ } else {
+ _dumpElement(element, childNode);
+ }
+ node.childNodes.add((MemberSortKey(element), childNode));
+ }
+ }
+
+ /// Returns a map from each sealed class in [library] to the set of its
+ /// immediate sub-interfaces.
+ ///
+ /// If this method has been called before with the same [library], a cached
+ /// map is returned from [_immediateSubinterfaceCache]. Otherwise a fresh map
+ /// is computed.
+ Map<ClassElement, Set<InterfaceElement>>
+ _getOrComputeImmediateSubinterfaceMap(LibraryElement library) {
+ if (_immediateSubinterfaceCache[library] case final m?) return m;
+ final result = <ClassElement, Set<InterfaceElement>>{};
+ for (final interface in [
+ ...library.classes,
+ ...library.mixins,
+ ...library.enums,
+ ...library.extensionTypes,
+ ]..sortBy((e) => e.name!)) {
+ for (final superinterface in [
+ interface.supertype,
+ ...interface.interfaces,
+ ...interface.mixins,
+ if (interface is MixinElement) ...interface.superclassConstraints,
+ ]) {
+ if (superinterface == null) continue;
+ final superinterfaceElement = superinterface.element;
+ if (superinterfaceElement is ClassElement &&
+ superinterfaceElement.isSealed) {
+ (result[superinterfaceElement] ??= {}).add(interface);
+ }
+ }
+ }
+ _immediateSubinterfaceCache[library] = result;
+ return result;
+ }
+}
diff --git a/pkgs/api_summary/lib/src/api_summary_customizer.dart b/pkgs/api_summary/lib/src/api_summary_customizer.dart
new file mode 100644
index 0000000..34c6845
--- /dev/null
+++ b/pkgs/api_summary/lib/src/api_summary_customizer.dart
@@ -0,0 +1,53 @@
+// 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.dart';
+import 'package:analyzer/dart/element/element.dart';
+
+/// Clients of the API summary tool may extend this class to customize its
+/// behavior.
+///
+/// Clients should not *implement* this class, however, because additional
+/// methods may be added in the future.
+base class ApiSummaryCustomizer {
+ /// The top level elements exported by the libraries in [publicApiLibraries].
+ ///
+ /// This value is set by the tool before [initialScanComplete] is called.
+ late final Set<Element> topLevelPublicElements;
+
+ /// The analysis context for the package being summarized.
+ ///
+ /// This value is set by the tool before [setupComplete] is called.
+ set analysisContext(AnalysisContext analysisContext) {}
+
+ /// The name of the package whose API is being summarized.
+ ///
+ /// This value is set by the tool before [setupComplete] is called.
+ set packageName(String value) {}
+
+ /// The libraries that comprise the package's public API.
+ ///
+ /// This value is set by the tool before [initialScanComplete] is called.
+ set publicApiLibraries(Iterable<LibraryElement> value) {}
+
+ /// Called after [publicApiLibraries] and [topLevelPublicElements] have been
+ /// set, but before any analysis has been performed.
+ ///
+ /// Further analysis won't be performed until the returned Future completes.
+ Future<void> initialScanComplete() async {}
+
+ /// Called after [packageName] and [analysisContext] have been set, but before
+ /// any analysis has been performed.
+ ///
+ /// The initial scan won't be performed until the returned Future completes.
+ Future<void> setupComplete() async {}
+
+ /// Called after [initialScanComplete] to determine if details about an
+ /// element should be shown in the API summary.
+ ///
+ /// The default behavior is to show details about elements in
+ /// [topLevelPublicElements].
+ bool shouldShowDetails(Element element) =>
+ topLevelPublicElements.contains(element);
+}
diff --git a/pkgs/api_summary/lib/src/extensions.dart b/pkgs/api_summary/lib/src/extensions.dart
new file mode 100644
index 0000000..a683382
--- /dev/null
+++ b/pkgs/api_summary/lib/src/extensions.dart
@@ -0,0 +1,68 @@
+// 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/element/element.dart';
+
+extension ElementExtension on Element {
+ /// Returns the appropriate name for describing the element in `api.txt`.
+ ///
+ /// The name is the same as [name], but with `=` appended for setters.
+ String get apiName {
+ var apiName = name!;
+ if (this is SetterElement) {
+ apiName += '=';
+ }
+ return apiName;
+ }
+}
+
+extension FormalParameterElementExtension on FormalParameterElement {
+ bool get isDeprecated =>
+ // TODO(paulberry): add this to the analyzer public API
+ metadata.hasDeprecated;
+}
+
+extension IterableIterableExtension on Iterable<Iterable<Object?>> {
+ /// Forms a list containing [prefix], followed by the elements of `this`
+ /// (separated by [separator]), followed by [suffix].
+ ///
+ /// Each element of `this` is also an iterable; these elements are added to
+ /// the resulting list using `.addAll`, so one level of iterable nesting is
+ /// removed.
+ List<Object?> separatedBy({
+ String separator = ', ',
+ String prefix = '',
+ String suffix = '',
+ }) {
+ final result = <Object?>[prefix];
+ var first = true;
+ for (final item in this) {
+ if (first) {
+ first = false;
+ } else {
+ result.add(separator);
+ }
+ result.addAll(item);
+ }
+ result.add(suffix);
+ return result;
+ }
+}
+
+extension StringExtension on String {
+ bool get isPublic => !startsWith('_');
+}
+
+extension UriExtension on Uri {
+ bool isIn(String packageName) =>
+ scheme == 'package' &&
+ pathSegments.isNotEmpty &&
+ pathSegments[0] == packageName;
+
+ bool isInPublicLibOf(String packageName) =>
+ scheme == 'package' &&
+ pathSegments.length > 1 &&
+ pathSegments[0] == packageName &&
+ pathSegments[1] != 'src';
+}
diff --git a/pkgs/api_summary/lib/src/member_sorting.dart b/pkgs/api_summary/lib/src/member_sorting.dart
new file mode 100644
index 0000000..f53b0cc
--- /dev/null
+++ b/pkgs/api_summary/lib/src/member_sorting.dart
@@ -0,0 +1,67 @@
+// 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/element/element.dart';
+
+/// Element categorization used by [MemberSortKey].
+enum MemberCategory {
+ constructor,
+ propertyAccessor,
+ topLevelFunctionOrMethod,
+ interface,
+ extension,
+ typeAlias,
+}
+
+/// Sort key used to sort elements in the output.
+class MemberSortKey implements Comparable<MemberSortKey> {
+ final bool _isInstanceMember;
+ final MemberCategory _category;
+ final String _name;
+ final bool _isSetter;
+
+ MemberSortKey(Element element)
+ : _isInstanceMember = _computeIsInstanceMember(element),
+ _category = _computeCategory(element),
+ _name = element.displayName,
+ _isSetter = element is SetterElement;
+
+ @override
+ int compareTo(MemberSortKey other) {
+ if ((_isInstanceMember ? 1 : 0).compareTo(other._isInstanceMember ? 1 : 0)
+ case final value when value != 0) {
+ return value;
+ }
+ if (_category.index.compareTo(other._category.index) case final value
+ when value != 0) {
+ return value;
+ }
+ if (_name.compareTo(other._name) case final value when value != 0) {
+ return value;
+ }
+ return (_isSetter ? 1 : 0).compareTo(other._isSetter ? 1 : 0);
+ }
+
+ static MemberCategory _computeCategory(Element element) => switch (element) {
+ ConstructorElement() => MemberCategory.constructor,
+ PropertyAccessorElement() => MemberCategory.propertyAccessor,
+ TopLevelFunctionElement() => MemberCategory.topLevelFunctionOrMethod,
+ MethodElement() => MemberCategory.topLevelFunctionOrMethod,
+ InterfaceElement() => MemberCategory.interface,
+ ExtensionElement() => MemberCategory.extension,
+ TypeAliasElement() => MemberCategory.typeAlias,
+ dynamic(:final runtimeType) => throw UnimplementedError(
+ 'Unexpected element: $runtimeType',
+ ),
+ };
+
+ static bool _computeIsInstanceMember(Element element) =>
+ element.enclosingElement is InstanceElement &&
+ switch (element) {
+ ExecutableElement(:final isStatic) => !isStatic,
+ dynamic(:final runtimeType) => throw UnimplementedError(
+ 'Unexpected element: $runtimeType',
+ ),
+ };
+}
diff --git a/pkgs/api_summary/lib/src/node.dart b/pkgs/api_summary/lib/src/node.dart
new file mode 100644
index 0000000..083d037
--- /dev/null
+++ b/pkgs/api_summary/lib/src/node.dart
@@ -0,0 +1,43 @@
+// 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.
+
+/// @docImport 'unique_namer.dart';
+library;
+
+import 'package:collection/collection.dart';
+
+/// Outputs the contents of [nodes] to [sink], prepending [prefix] to every
+/// line.
+void printNodes<SortKey extends Comparable<SortKey>>(
+ StringSink sink,
+ List<(SortKey, Node)> nodes, {
+ String prefix = '',
+}) {
+ for (final entry in nodes.sortedBy((n) => n.$1)) {
+ final node = entry.$2;
+ sink.writeln('$prefix${node.text.join()}');
+ node.printChildren(sink, prefix: '$prefix ');
+ }
+}
+
+/// A node to be printed to the output.
+class Node<ChildSortKey extends Comparable<ChildSortKey>> {
+ /// A list of objects which, when their string representations are
+ /// concatenated, is the text that should be displayed on the first line of
+ /// the node.
+ ///
+ /// The reason this is a list rather than a single string is to allow elements
+ /// of the list to be [UniqueName] objects, which may acquire a disambiguation
+ /// suffix at a later time.
+ final text = <Object?>[];
+
+ /// A list of child nodes, paired with a sort key indicating the order in
+ /// which they should be output.
+ final childNodes = <(ChildSortKey, Node)>[];
+
+ /// Outputs [childNodes], prepending [prefix] to every line.
+ void printChildren(StringSink sink, {required String prefix}) {
+ printNodes(sink, childNodes, prefix: prefix);
+ }
+}
diff --git a/pkgs/api_summary/lib/src/unique_namer.dart b/pkgs/api_summary/lib/src/unique_namer.dart
new file mode 100644
index 0000000..ccd59dc
--- /dev/null
+++ b/pkgs/api_summary/lib/src/unique_namer.dart
@@ -0,0 +1,53 @@
+// 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/element/element.dart';
+import 'extensions.dart';
+
+/// Object that will have a unique string representation within the context of a
+/// given [UniqueNamer] instance.
+///
+/// If two or more [UniqueName] objects are constructed with reference to the
+/// same [UniqueNamer], and they have the same [_nameHint], then all such
+/// objects' [toString] methods will append a unique suffix of the form
+/// `@INTEGER`, so that the resulting strings are unique.
+class UniqueName {
+ /// The name that will be returned by [toString] if no disambiguation is
+ /// needed.
+ final String _nameHint;
+
+ /// If not `Null`, the integer that [toString] will use to disambiguate this
+ /// [UniqueName] from other ones with the same [_nameHint].
+ int? _disambiguator;
+
+ UniqueName(UniqueNamer uniqueNamer, this._nameHint)
+ // The uniqueness guarantee depends on `_nameHint` not containing an `@`.
+ : assert(!_nameHint.contains('@')) {
+ final conflicts = uniqueNamer._conflicts[_nameHint] ??= [];
+ if (conflicts.length == 1) {
+ conflicts[0]._disambiguator = 1;
+ }
+ conflicts.add(this);
+ if (conflicts.length > 1) {
+ _disambiguator = conflicts.length;
+ }
+ }
+
+ @override
+ String toString() => [
+ _nameHint,
+ if (_disambiguator case final disambiguator?) '@$disambiguator',
+ ].join();
+}
+
+/// Manager of unique names for elements.
+class UniqueNamer {
+ final _names = <Element, UniqueName>{};
+ final _conflicts = <String, List<UniqueName>>{};
+
+ /// Returns a [UniqueName] object whose [toString] method will produce a
+ /// unique name for [element].
+ UniqueName name(Element element) =>
+ _names[element] ??= UniqueName(this, element.apiName);
+}
diff --git a/pkgs/api_summary/lib/src/uri_sorting.dart b/pkgs/api_summary/lib/src/uri_sorting.dart
new file mode 100644
index 0000000..1f84462
--- /dev/null
+++ b/pkgs/api_summary/lib/src/uri_sorting.dart
@@ -0,0 +1,32 @@
+// 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 'extensions.dart';
+
+/// URI categorization used by [UriSortKey].
+enum UriCategory { inPackage, notInPackage }
+
+/// Sort key used to sort libraries in the output.
+///
+/// Libraries in the specified package will be output first (sorted by URI),
+/// followed by libraries not in the package.
+class UriSortKey implements Comparable<UriSortKey> {
+ final UriCategory _category;
+ final String _uriString;
+
+ UriSortKey(Uri uri, String pkgName)
+ : _category = uri.isIn(pkgName)
+ ? UriCategory.inPackage
+ : UriCategory.notInPackage,
+ _uriString = uri.toString();
+
+ @override
+ int compareTo(UriSortKey other) {
+ if (_category.index.compareTo(other._category.index) case final value
+ when value != 0) {
+ return value;
+ }
+ return _uriString.compareTo(other._uriString);
+ }
+}
diff --git a/pkgs/api_summary/pubspec.yaml b/pkgs/api_summary/pubspec.yaml
new file mode 100644
index 0000000..b664c63
--- /dev/null
+++ b/pkgs/api_summary/pubspec.yaml
@@ -0,0 +1,22 @@
+name: api_summary
+version: 0.1.0-wip
+description: Creates an API summary for a package.
+
+environment:
+ sdk: ^3.12.0
+
+dependencies:
+ analyzer: ^13.0.0
+ args: ^2.6.0
+ collection: ^1.19.0
+ path: ^1.9.0
+ yaml: ^3.1.2
+
+dev_dependencies:
+ analyzer_testing: ^0.2.6
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.28.0
+ test_reflective_loader: ^0.4.0
+
+executables:
+ api_summary:
diff --git a/pkgs/api_summary/test/api_description_test.dart b/pkgs/api_summary/test/api_description_test.dart
new file mode 100644
index 0000000..0fa98b4
--- /dev/null
+++ b/pkgs/api_summary/test/api_description_test.dart
@@ -0,0 +1,820 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'dart:core';
+
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:api_summary/src/api_description.dart';
+import 'package:api_summary/src/api_summary_customizer.dart';
+import 'package:api_summary/src/node.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ApiDescriptionTest);
+ });
+}
+
+@reflectiveTest
+class ApiDescriptionTest extends ApiSummaryTest {
+ @override
+ bool get addMetaPackageDep => true;
+
+ @override
+ void setUp() {
+ newPackage('foo').addFile('lib/foo.dart', r'''
+foo() {}
+class Foo {}
+''');
+ super.setUp();
+ }
+
+ Future<void> test_class_modifiers() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {}
+abstract class A {}
+base class B {}
+base mixin class BM {}
+mixin class M {}
+interface class I {}
+final class F {}
+abstract base class AB {}
+abstract base mixin class ABM {}
+abstract interface class AI {}
+abstract final class AF {}
+abstract mixin class AM {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ A (class extends Object, abstract):
+ new (constructor: A Function())
+ AB (class extends Object, abstract, base):
+ new (constructor: AB Function())
+ ABM (class extends Object, abstract, base, mixin):
+ new (constructor: ABM Function())
+ AF (class extends Object, abstract, final)
+ AI (class extends Object, abstract, interface)
+ AM (class extends Object, abstract, mixin):
+ new (constructor: AM Function())
+ B (class extends Object, base):
+ new (constructor: B Function())
+ BM (class extends Object, base, mixin):
+ new (constructor: BM Function())
+ C (class extends Object):
+ new (constructor: C Function())
+ F (class extends Object, final):
+ new (constructor: F Function())
+ I (class extends Object, interface):
+ new (constructor: I Function())
+ M (class extends Object, mixin):
+ new (constructor: M Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_customize_shouldShowDetails() async {
+ final summary = await _build(
+ {
+ '$testPackageLibPath/public.dart': '''
+import 'src/private.dart';
+
+void shown1(Shown2 x, Hidden2 y) {}
+void hidden1(Shown3 x, Hidden3 y) {}
+''',
+ '$testPackageLibPath/src/private.dart': '''
+class Shown2 {}
+class Hidden2 {}
+class Shown3 {}
+class Hidden3 {}
+''',
+ },
+ createCustomizer: () => _ShouldShowDetailsCustomizer(
+ (e) => e.name!.toLowerCase().contains('shown'),
+ ),
+ );
+ // Note: Shown2 and Hidden2 are included in the summary because they are
+ // referenced by shown1. Details are only shown for shown1 and Shown2.
+ expect(summary, '''
+package:test/public.dart:
+ hidden1 (non-public)
+ shown1 (function: void Function(Shown2, Hidden2))
+package:test/src/private.dart:
+ Hidden2 (non-public)
+ Shown2 (class extends Object):
+ new (constructor: Shown2 Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_field_deprecated() async {
+ // Marking a field as deprecated causes its corresponding getter and setter
+ // to be marked as deprecated in the summary.
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {
+ @deprecated
+ int x = 0;
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ x (getter: int, deprecated)
+ x= (setter: int, deprecated)
+dart:core:
+ Object (referenced)
+ int (referenced)
+''');
+ }
+
+ Future<void> test_field_experimental() async {
+ // Marking a field as experimental causes its corresponding getter and
+ // setter to be marked as experimental in the summary.
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+import 'package:meta/meta.dart';
+
+class C {
+ @experimental
+ int x = 0;
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ x (getter: int, experimental)
+ x= (setter: int, experimental)
+dart:core:
+ Object (referenced)
+ int (referenced)
+''');
+ }
+
+ Future<void> test_member_field() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {
+ int x = 0;
+}
+''',
+ });
+ // The summary output contains the getters and setters induced by the field,
+ // not the field itself.
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ x (getter: int)
+ x= (setter: int)
+dart:core:
+ Object (referenced)
+ int (referenced)
+''');
+ }
+
+ Future<void> test_member_getterSetterPair() async {
+ // This test verifies that even if a getter and a setter have the same name,
+ // both are included in the summary output.
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {
+ get x => 0;
+ set x(value) {}
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ x (getter: dynamic)
+ x= (setter: dynamic)
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_member_method() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {
+ void f() {}
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ f (method: void Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_member_privateName() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class C {
+ f() {
+ _f();
+ }
+ _f() {}
+}
+''',
+ });
+ // The private member _f is not included in the summary output.
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+ f (method: dynamic Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_minimallyDescribesReferencedNamesInOtherPackages() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+import 'package:foo/foo.dart';
+
+void f(Foo foo) {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ f (function: void Function(Foo))
+package:foo/foo.dart:
+ Foo (referenced)
+''');
+ }
+
+ Future<void> test_minimallyDescribesReferencedNonPublicNames() async {
+ final summary = await _build({
+ '$testPackageLibPath/public.dart': '''
+import 'src/private.dart';
+
+void f(Foo foo) {}
+''',
+ '$testPackageLibPath/src/private.dart': 'class Foo {}',
+ });
+ expect(summary, '''
+package:test/public.dart:
+ f (function: void Function(Foo))
+package:test/src/private.dart:
+ Foo (non-public)
+''');
+ }
+
+ Future<void> test_mixin_modifiers() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+mixin M {}
+base mixin BM {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ BM (mixin on Object, base)
+ M (mixin on Object)
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_nonConstructibleClass() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+abstract final class C1 {}
+abstract interface class C2 {}
+sealed class C3 {}
+''',
+ });
+ // These classes can't be constructed from outside the library, so their
+ // constructors aren't included in the summary output.
+ expect(summary, '''
+package:test/file.dart:
+ C1 (class extends Object, abstract, final)
+ C2 (class extends Object, abstract, interface)
+ C3 (class extends Object, sealed)
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_nonConstructibleClass_withFactory() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+abstract final class C1 {
+ factory C1.f() => throw '';
+}
+abstract interface class C2 {
+ factory C2.f() => throw '';
+}
+sealed class C3 {
+ factory C3.f() => throw '';
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C1 (class extends Object, abstract, final):
+ f (constructor: C1 Function())
+ C2 (class extends Object, abstract, interface):
+ f (constructor: C2 Function())
+ C3 (class extends Object, sealed):
+ f (constructor: C3 Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_enum_withFactory() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+enum E {
+ v;
+ factory E.f() => throw '';
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ E (enum):
+ v (static getter: E)
+ values (static getter: List<E>)
+ f (constructor: E Function())
+dart:core:
+ List (referenced)
+''');
+ }
+
+ Future<void> test_sealedClass() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+sealed class C {}
+// Note: immediate subinterfaces will be sorted in summary output
+class C2 extends C {}
+class C1 extends C {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C (class extends Object, sealed (immediate subtypes: C1, C2))
+ C1 (class extends C):
+ new (constructor: C1 Function())
+ C2 (class extends C):
+ new (constructor: C2 Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_sealedClass_allKindsAndRelationships() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+sealed class S {}
+class C1 extends S {}
+class C2 implements S {}
+mixin M1 on S {}
+mixin M2 implements S {}
+enum E1 implements S { v }
+extension type T(C1 c) implements S {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ C1 (class extends S):
+ new (constructor: C1 Function())
+ C2 (class extends Object implements S):
+ new (constructor: C2 Function())
+ E1 (enum implements S):
+ v (static getter: E1)
+ values (static getter: List<E1>)
+ M1 (mixin on S)
+ M2 (mixin on Object implements S)
+ S (class extends Object, sealed (immediate subtypes: C1, C2, E1, M1, M2, T))
+ T (extension type implements S):
+ new (constructor: T Function(C1))
+ c (getter: C1)
+dart:core:
+ List (referenced)
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_collapsesRedundantElements() async {
+ // When an element is exported by multiple libraries, it is only described
+ // once; later references use the text "(see above)".
+ final summary = await _build({
+ '$testPackageLibPath/file1.dart': 'export "file2.dart";',
+ '$testPackageLibPath/file2.dart': 'class C {}',
+ '$testPackageLibPath/file3.dart': 'export "file2.dart";',
+ });
+ expect(summary, '''
+package:test/file1.dart:
+ C (class extends Object):
+ new (constructor: C Function())
+package:test/file2.dart:
+ C (see above)
+package:test/file3.dart:
+ C (see above)
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_disambiguatesNames() async {
+ // If two libraries declare top level elements with the same name, the names
+ // are disambiguated so that references are clear.
+ final summary = await _build({
+ '$testPackageLibPath/file1.dart': '''
+class A {}
+class B extends A {}
+''',
+ '$testPackageLibPath/file2.dart': '''
+class A {}
+class B extends A {}
+''',
+ });
+ expect(summary, '''
+package:test/file1.dart:
+ A@1 (class extends Object):
+ new (constructor: A@1 Function())
+ B@1 (class extends A@1):
+ new (constructor: B@1 Function())
+package:test/file2.dart:
+ A@2 (class extends Object):
+ new (constructor: A@2 Function())
+ B@2 (class extends A@2):
+ new (constructor: B@2 Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_extension() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+extension E on int {
+ void f() {}
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ E (extension on int):
+ f (method: void Function())
+dart:core:
+ int (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_filesInSrc() async {
+ final summary = await _build({
+ '$testPackageLibPath/public.dart': 'export "src/private1.dart";',
+ '$testPackageLibPath/src/private1.dart': 'f() {}',
+ '$testPackageLibPath/src/private2.dart': 'g() {}',
+ });
+ // `f` is considered part of the public API because it is exported by
+ // `public.dart`.
+ expect(summary, '''
+package:test/public.dart:
+ f (function: dynamic Function())
+''');
+ }
+
+ Future<void> test_topLevel_getterSetterPair() async {
+ // This test verifies that even if getter and a setter have the same name,
+ // both are included in the summary output.
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+get x => 0;
+set x(value) {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ x (static getter: dynamic)
+ x= (static setter: dynamic)
+''');
+ }
+
+ Future<void> test_topLevel_interfaceType() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+class I {}
+class B {}
+class C<T> extends B implements I {}
+enum E implements I { e1 }
+mixin M on B implements I {}
+extension type T(int i) {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ B (class extends Object):
+ new (constructor: B Function())
+ C (class<T> extends B implements I):
+ new (constructor: C<T> Function())
+ E (enum implements I):
+ e1 (static getter: E)
+ values (static getter: List<E>)
+ I (class extends Object):
+ new (constructor: I Function())
+ M (mixin on B implements I)
+ T (extension type):
+ new (constructor: T Function(int))
+ i (getter: int)
+dart:core:
+ List (referenced)
+ Object (referenced)
+ int (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_nonDartFile() async {
+ final summary = await _build({'$testPackageLibPath/file.dar': 'f() {}'});
+ expect(summary, '');
+ }
+
+ Future<void> test_topLevel_otherPackagePublicApi() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+import 'package:foo/foo.dart';
+
+main() {
+ foo();
+}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ main (function: dynamic Function())
+''');
+ }
+
+ Future<void> test_topLevel_partFile() async {
+ // Declarations in a part file are considered part of the public API of the
+ // containing library.
+ final summary = await _build({
+ '$testPackageLibPath/lib.dart': 'part "part.dart";',
+ '$testPackageLibPath/part.dart': '''
+part of "lib.dart";
+
+f() {}
+''',
+ });
+ expect(summary, '''
+package:test/lib.dart:
+ f (function: dynamic Function())
+''');
+ }
+
+ Future<void> test_topLevel_privateName() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+f() {
+ _g();
+}
+
+_g() {}
+''',
+ });
+ // The private member _g is not included in the summary output.
+ expect(summary, '''
+package:test/file.dart:
+ f (function: dynamic Function())
+''');
+ }
+
+ Future<void> test_topLevel_sorted() async {
+ // This test just verifies that sorting occurs. See `member_test.dart` for
+ // tests of the precise nature of the sort order.
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+g() {}
+f() {}
+get x => 0;
+class C {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ x (static getter: dynamic)
+ f (function: dynamic Function())
+ g (function: dynamic Function())
+ C (class extends Object):
+ new (constructor: C Function())
+dart:core:
+ Object (referenced)
+''');
+ }
+
+ Future<void> test_topLevel_sortsLibrariesByUri() async {
+ final summary = await _build({
+ '$testPackageLibPath/file2.dart': '',
+ '$testPackageLibPath/file1.dart': '',
+ '$testPackageLibPath/file3.dart': '',
+ });
+ expect(summary, '''
+package:test/file1.dart:
+package:test/file2.dart:
+package:test/file3.dart:
+''');
+ }
+
+ Future<void> test_topLevel_typedef() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+typedef void oldStyleFunctionTypedef();
+typedef void oldStyleFunctionTypedefGeneric<T>(T t);
+typedef newStyleFunctionTypedef = void Function();
+typedef newStyleFunctionTypedefGeneric = void Function<T>(T);
+typedef nonFunctionTypedef = int;
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ newStyleFunctionTypedef (type alias for void Function())
+ newStyleFunctionTypedefGeneric (type alias for void Function<T>(T))
+ nonFunctionTypedef (type alias for int)
+ oldStyleFunctionTypedef (type alias for void Function())
+ oldStyleFunctionTypedefGeneric (type alias<T> for void Function(T))
+dart:core:
+ int (referenced)
+''');
+ }
+
+ Future<void> test_types() async {
+ final summary = await _build({
+ '$testPackageLibPath/file.dart': '''
+import 'dart:async';
+
+// Dynamic type
+dynamic get d => 0;
+
+// Null type
+Null get n => null;
+
+// FutureOr type
+FutureOr<int>? get fo => null;
+
+// Function types
+void Function(int requiredPositionalParam, [int? optionalPositionalParam])
+ get f1 => throw '';
+void Function({
+ // Note: named params will be sorted by name
+ required int requiredNamedParam, int? optionalNamedParam})? get f2 => null;
+void f3(@deprecated int i, [@deprecated int? j]) {}
+void f4({@deprecated int? i}) {}
+void f5<T>(T t1, T? t2) {} // Also tests type parameter types
+void f6<T extends num>(T t) {}
+
+// Interface types
+void f7(Map<String, int> m1, Map<String, int>? m2) {}
+
+// Record types
+void f8((int, String) r1, (int, {String s})? r2,
+ // Note: named record fields will be sorted by name
+ ({String s, int i}) r3) {}
+''',
+ });
+ expect(summary, '''
+package:test/file.dart:
+ d (static getter: dynamic)
+ f1 (static getter: void Function(int, [int?]))
+ f2 (static getter: void Function({int? optionalNamedParam, required int requiredNamedParam})?)
+ fo (static getter: FutureOr<int>?)
+ n (static getter: Null)
+ f3 (function: void Function(deprecated int, [deprecated int?]))
+ f4 (function: void Function({deprecated int? i}))
+ f5 (function: void Function<T>(T, T?))
+ f6 (function: void Function<T extends num>(T))
+ f7 (function: void Function(Map<String, int>, Map<String, int>?))
+ f8 (function: void Function((int, String), (int, {String s})?, ({int i, String s})))
+dart:async:
+ FutureOr (referenced)
+dart:core:
+ Map (referenced)
+ Null (referenced)
+ String (referenced)
+ int (referenced)
+ num (referenced)
+''');
+ }
+
+ Future<String> _build(
+ Map<String, String> files, {
+ _ValidatingCustomizer Function()? createCustomizer,
+ }) async {
+ // Create all the files.
+ files.forEach(newFile);
+
+ // As a sanity check, make sure there are no errors in any of the files.
+ for (final file in files.keys) {
+ if (file.endsWith('.dart')) await assertNoDiagnosticsInFile(file);
+ }
+
+ // Generate the API description.
+ final context = contextCollection.contextFor(
+ convertPath(testPackageLibPath),
+ );
+ final customizer = createCustomizer?.call() ?? _ValidatingCustomizer();
+ final apiDescription = ApiDescription('test', customizer);
+ final stringBuffer = StringBuffer();
+ final nodes = await apiDescription.build(context);
+ expect(customizer.initialScanCompleteCalled, isTrue);
+ printNodes(stringBuffer, nodes);
+ return stringBuffer.toString();
+ }
+}
+
+final class _ShouldShowDetailsCustomizer extends _ValidatingCustomizer {
+ final bool Function(Element) _shouldShowDetails;
+
+ _ShouldShowDetailsCustomizer(this._shouldShowDetails);
+
+ @override
+ bool shouldShowDetails(Element element) {
+ expect(initialScanCompleteCalled, isTrue);
+ return _shouldShowDetails(element);
+ }
+}
+
+base class _ValidatingCustomizer extends ApiSummaryCustomizer {
+ bool topLevelPublicElementsCalled = false;
+ bool analysisContextCalled = false;
+ bool packageNameCalled = false;
+ bool publicApiLibrariesCalled = false;
+ bool initialScanCompleteCalled = false;
+ bool setupCompleteCalled = false;
+
+ @override
+ set analysisContext(AnalysisContext value) {
+ expect(analysisContextCalled, isFalse);
+ analysisContextCalled = true;
+ super.analysisContext = value;
+ }
+
+ @override
+ set packageName(String value) {
+ expect(packageNameCalled, isFalse);
+ packageNameCalled = true;
+ super.packageName = value;
+ }
+
+ @override
+ set publicApiLibraries(Iterable<LibraryElement> value) {
+ expect(setupCompleteCalled, isTrue);
+ expect(publicApiLibrariesCalled, isFalse);
+ publicApiLibrariesCalled = true;
+ super.publicApiLibraries = value;
+ }
+
+ @override
+ set topLevelPublicElements(Set<Element> value) {
+ expect(setupCompleteCalled, isTrue);
+ expect(topLevelPublicElementsCalled, isFalse);
+ topLevelPublicElementsCalled = true;
+ super.topLevelPublicElements = value;
+ }
+
+ @override
+ Future<void> initialScanComplete() async {
+ expect(topLevelPublicElementsCalled, isTrue);
+ expect(publicApiLibrariesCalled, isTrue);
+ initialScanCompleteCalled = true;
+ await super.initialScanComplete();
+ }
+
+ @override
+ Future<void> setupComplete() async {
+ expect(packageNameCalled, isTrue);
+ expect(analysisContextCalled, isTrue);
+ setupCompleteCalled = true;
+ await super.setupComplete();
+ }
+
+ @override
+ bool shouldShowDetails(Element element) {
+ expect(initialScanCompleteCalled, isTrue);
+ return super.shouldShowDetails(element);
+ }
+}
diff --git a/pkgs/api_summary/test/app_test.dart b/pkgs/api_summary/test/app_test.dart
new file mode 100644
index 0000000..5ea6390
--- /dev/null
+++ b/pkgs/api_summary/test/app_test.dart
@@ -0,0 +1,42 @@
+// 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:convert';
+import 'dart:io';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+ test(
+ 'api_summary output matches api.txt',
+ timeout: const Timeout.factor(3),
+ () async {
+ final packageDir = p.current;
+
+ final result = await Process.run(Platform.resolvedExecutable, [
+ if (Platform.packageConfig != null)
+ '--packages=${Platform.packageConfig}',
+ p.join(packageDir, 'bin', 'api_summary.dart'),
+ '-p',
+ packageDir,
+ ], workingDirectory: packageDir);
+
+ expect(
+ result.exitCode,
+ equals(0),
+ reason: 'CLI run failed with stderr:\n${result.stderr}',
+ );
+
+ final goldenFile = File(p.join(packageDir, 'api.txt'));
+ final expectedOutput = LineSplitter.split(
+ goldenFile.readAsStringSync(),
+ ).join('\n');
+ final actualOutput = LineSplitter.split(
+ result.stdout.toString(),
+ ).join('\n');
+
+ expect(actualOutput, equals(expectedOutput));
+ },
+ );
+}
diff --git a/pkgs/api_summary/test/extensions_test.dart b/pkgs/api_summary/test/extensions_test.dart
new file mode 100644
index 0000000..651673b
--- /dev/null
+++ b/pkgs/api_summary/test/extensions_test.dart
@@ -0,0 +1,128 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:api_summary/src/extensions.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ExtensionsTest);
+ });
+}
+
+@reflectiveTest
+class ExtensionsTest extends ApiSummaryTest {
+ Future<void> test_element_apiName_classMember() async {
+ final class_ = (await analyzeLibrary('''
+class C {
+ void method() {}
+ int get getter => 0;
+ set setter(int value) {}
+ int field = 0;
+ static const constant = 0;
+}
+''')).getClass('C')!;
+ expect(class_.getMethod('method')!.apiName, 'method');
+ expect(class_.getGetter('getter')!.apiName, 'getter');
+ expect(class_.getSetter('setter')!.apiName, 'setter=');
+ expect(class_.getField('field')!.apiName, 'field');
+ expect(class_.getField('constant')!.apiName, 'constant');
+ }
+
+ Future<void> test_element_apiName_topLevel() async {
+ final lib = await analyzeLibrary('''
+void function() {}
+int get getter => 0;
+set setter(int value) {}
+int variable = 0;
+const constant = 0;
+class Class {}
+mixin Mixin {}
+enum Enum { e }
+extension Extension on int {}
+extension type ExtensionType(int i) {}
+''');
+ expect(lib.getTopLevelFunction('function')!.apiName, 'function');
+ expect(lib.getGetter('getter')!.apiName, 'getter');
+ expect(lib.getSetter('setter')!.apiName, 'setter=');
+ expect(lib.getTopLevelVariable('variable')!.apiName, 'variable');
+ expect(lib.getTopLevelVariable('constant')!.apiName, 'constant');
+ expect(lib.getClass('Class')!.apiName, 'Class');
+ expect(lib.getMixin('Mixin')!.apiName, 'Mixin');
+ expect(lib.getEnum('Enum')!.apiName, 'Enum');
+ expect(lib.getExtension('Extension')!.apiName, 'Extension');
+ expect(lib.getExtensionType('ExtensionType')!.apiName, 'ExtensionType');
+ }
+
+ Future<void> test_formalParameterElement_isDeprecated() async {
+ final f = (await analyzeLibrary(
+ 'f({int? i, @deprecated int? j}) {}',
+ )).getTopLevelFunction('f')!;
+ expect(f.formalParameters[0].isDeprecated, isFalse);
+ expect(f.formalParameters[1].isDeprecated, isTrue);
+ }
+
+ void test_iterableIterable_separatedBy() {
+ expect(
+ [
+ ['a', 'b'],
+ ['c', 'd'],
+ ].separatedBy(),
+ ['', 'a', 'b', ', ', 'c', 'd', ''],
+ );
+ expect(
+ [
+ ['a', 'b'],
+ ['c', 'd'],
+ ].separatedBy(prefix: '[', separator: '|', suffix: ']'),
+ ['[', 'a', 'b', '|', 'c', 'd', ']'],
+ );
+ expect(
+ <Iterable<Object?>>[].separatedBy(
+ prefix: '[',
+ separator: '|',
+ suffix: ']',
+ ),
+ ['[', ']'],
+ );
+ }
+
+ void test_string_isPublic() {
+ expect('_'.isPublic, isFalse);
+ expect('foo'.isPublic, isTrue);
+ expect('_foo'.isPublic, isFalse);
+ }
+
+ void test_uri_isIn() {
+ expect(Uri.parse('package:foo/bar.dart').isIn('foo'), isTrue);
+ expect(Uri.parse('package:foo/bar.dart').isIn('bar.dart'), isFalse);
+ expect(Uri.parse('dart:core').isIn('foo'), isFalse);
+ expect(Uri.parse('dart:core').isIn('dart'), isFalse);
+ expect(Uri.parse('dart:core').isIn('core'), isFalse);
+ }
+
+ void test_uri_isInPublicLibOf() {
+ expect(Uri.parse('package:foo/bar.dart').isInPublicLibOf('foo'), isTrue);
+ expect(
+ Uri.parse('package:foo/bar.dart').isInPublicLibOf('bar.dart'),
+ isFalse,
+ );
+ expect(
+ Uri.parse('package:foo/src/bar.dart').isInPublicLibOf('foo'),
+ isFalse,
+ );
+ expect(
+ Uri.parse('package:foo/src/bar.dart').isInPublicLibOf('src'),
+ isFalse,
+ );
+ expect(Uri.parse('dart:core').isInPublicLibOf('foo'), isFalse);
+ expect(Uri.parse('dart:core').isInPublicLibOf('dart'), isFalse);
+ expect(Uri.parse('dart:core').isInPublicLibOf('core'), isFalse);
+ }
+}
diff --git a/pkgs/api_summary/test/member_sorting_test.dart b/pkgs/api_summary/test/member_sorting_test.dart
new file mode 100644
index 0000000..fe1d8f7
--- /dev/null
+++ b/pkgs/api_summary/test/member_sorting_test.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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:analyzer/dart/element/element.dart';
+import 'package:api_summary/src/extensions.dart';
+import 'package:api_summary/src/member_sorting.dart';
+import 'package:collection/collection.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(MemberTest);
+ });
+}
+
+@reflectiveTest
+class MemberTest extends ApiSummaryTest {
+ Future<void> test_sortOrder_member_categoryBeforeName() async {
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+class C {
+ C.a1();
+ get a2 => 0;
+ a3() {}
+ C.z1();
+ get z2 => 0;
+ z3() {}
+ }
+''')).getClass('C')!.childrenExcludingPropertyInducingElements,
+ expectedOrder: ['a1', 'z1', 'a2', 'z2', 'a3', 'z3'],
+ );
+ }
+
+ Future<void> test_sortOrder_member_gettersAndSettersTogether() async {
+ // Note that it's not good enough for the implementation to sort by apiName,
+ // because `9` is ASCII 0x39 and `=` is ASCII 0x3d, so sorting by apiName
+ // would put `a9` and `a9=` between `a` and `a=`.
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+class C {
+ get a => 0;
+ get a9 => 0;
+ get aA => 0;
+ set a(value) {}
+ set a9(value) {}
+ set aA(value) {}
+}
+''')).getClass('C')!.childrenExcludingPropertyInducingElements,
+ expectedOrder: ['new', 'a', 'a=', 'a9', 'a9=', 'aA', 'aA='],
+ );
+ }
+
+ Future<void> test_sortOrder_member_staticBeforeInstance() async {
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+class C {
+ C.a1();
+ C.z1();
+ get a2 => 0;
+ get z2 => 0;
+ static get a3 => 0;
+ static get z3 => 0;
+ a4() {}
+ z4() {}
+ static a5() {}
+ static z5() {}
+}
+''')).getClass('C')!.childrenExcludingPropertyInducingElements,
+ expectedOrder: [
+ 'a3',
+ 'z3',
+ 'a5',
+ 'z5',
+ 'a1',
+ 'z1',
+ 'a2',
+ 'z2',
+ 'a4',
+ 'z4',
+ ],
+ );
+ }
+
+ Future<void> test_sortOrder_member_unnamedConstructorBeforeOthers() async {
+ // Note that it's not good enough for the implementation to sort by apiName,
+ // because that would put `new` after `A`.
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+class C {
+ C();
+ C.A();
+ C.z();
+}
+''')).getClass('C')!.childrenExcludingPropertyInducingElements,
+ expectedOrder: ['new', 'A', 'z'],
+ );
+ }
+
+ Future<void> test_sortOrder_topLevel_categoryBeforeName() async {
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+get a1 => 0;
+a2() {}
+class a3 {}
+extension a4 on int {}
+typedef a5 = int;
+get z1 => 0;
+z2() {}
+class z3 {}
+extension z4 on int {}
+typedef z5 = int;
+''')).childrenExcludingPropertyInducingElements,
+ expectedOrder: [
+ 'a1',
+ 'z1',
+ 'a2',
+ 'z2',
+ 'a3',
+ 'z3',
+ 'a4',
+ 'z4',
+ 'a5',
+ 'z5',
+ ],
+ );
+ }
+
+ Future<void> test_sortOrder_topLevel_gettersAndSettersTogether() async {
+ // Note that it's not good enough for the implementation to sort by apiName,
+ // because `9` is ASCII 0x39 and `=` is ASCII 0x3d, so sorting by apiName
+ // would put `a9` and `a9=` between `a` and `a=`.
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+get a => 0;
+get a9 => 0;
+get aA => 0;
+set a(value) {}
+set a9(value) {}
+set aA(value) {}
+''')).childrenExcludingPropertyInducingElements,
+ expectedOrder: ['a', 'a=', 'a9', 'a9=', 'aA', 'aA='],
+ );
+ }
+
+ Future<void> test_sortOrder_topLevel_interfaceTypesTogether() async {
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+class A1 {}
+class Z1 {}
+mixin A2 {}
+mixin Z2 {}
+enum A3 { v }
+enum Z3 { v }
+extension type A4(int i) {}
+extension type Z4(int i) {}
+''')).childrenExcludingPropertyInducingElements,
+ expectedOrder: ['A1', 'A2', 'A3', 'A4', 'Z1', 'Z2', 'Z3', 'Z4'],
+ );
+ }
+
+ Future<void> test_sortOrder_topLevel_oldAndNewTypedefsTogether() async {
+ _checkSorting(
+ elements: (await analyzeLibrary('''
+typedef void A1();
+typedef void Z1();
+typedef A2 = void Function();
+typedef Z2 = void Function();
+typedef A3 = int;
+typedef Z3 = int;
+''')).childrenExcludingPropertyInducingElements,
+ expectedOrder: ['A1', 'A2', 'A3', 'Z1', 'Z2', 'Z3'],
+ );
+ }
+
+ void _checkSorting({
+ required List<Element> elements,
+ required List<String> expectedOrder,
+ }) {
+ expect(
+ elements.sortedBy(MemberSortKey.new).map((e) => e.apiName).toList(),
+ expectedOrder,
+ );
+ expect(
+ elements.reversed
+ .sortedBy(MemberSortKey.new)
+ .map((e) => e.apiName)
+ .toList(),
+ expectedOrder,
+ );
+ }
+}
+
+extension on Element {
+ /// All children of `this` excluding [PropertyInducingElement]s.
+ ///
+ /// This is used for testing the sort order of class members, since the API
+ /// summary only considers getters and setters; it ignores the fields and top
+ /// level variables that induce them.
+ List<Element> get childrenExcludingPropertyInducingElements =>
+ children.whereNot((e) => e is PropertyInducingElement).toList();
+}
diff --git a/pkgs/api_summary/test/node_test.dart b/pkgs/api_summary/test/node_test.dart
new file mode 100644
index 0000000..a4e208a
--- /dev/null
+++ b/pkgs/api_summary/test/node_test.dart
@@ -0,0 +1,97 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:api_summary/src/node.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(NodeTest);
+ });
+}
+
+@reflectiveTest
+class NodeTest {
+ void test_printNodes_indentChildNodes() {
+ final buf = StringBuffer();
+ printNodes(buf, [
+ (
+ 1,
+ _SimpleNode('one', [
+ (2, _SimpleNode('two')),
+ (3, _SimpleNode('three')),
+ ]),
+ ),
+ (
+ 4,
+ _SimpleNode('four', [
+ (5, _SimpleNode('five')),
+ (6, _SimpleNode('six')),
+ ]),
+ ),
+ ]);
+ expect(buf.toString(), '''
+one
+ two
+ three
+four
+ five
+ six
+''');
+ }
+
+ void test_printNodes_joinTextStrings() {
+ final buf = StringBuffer();
+ printNodes(buf, [
+ (1, Node<num>()..text.addAll(['x', 0])),
+ ]);
+ expect(buf.toString(), '''
+x0
+''');
+ }
+
+ void test_printNodes_sortChildNodesByKey() {
+ final buf = StringBuffer();
+ printNodes(buf, [
+ (
+ 0,
+ _SimpleNode('zero', [
+ (2, _SimpleNode('two')),
+ (1, _SimpleNode('one')),
+ (3, _SimpleNode('three')),
+ ]),
+ ),
+ ]);
+ expect(buf.toString(), '''
+zero
+ one
+ two
+ three
+''');
+ }
+
+ void test_printNodes_sortedByKey() {
+ final buf = StringBuffer();
+ printNodes(buf, [
+ (2, _SimpleNode('two')),
+ (1, _SimpleNode('one')),
+ (3, _SimpleNode('three')),
+ ]);
+ expect(buf.toString(), '''
+one
+two
+three
+''');
+ }
+}
+
+class _SimpleNode extends Node<num> {
+ _SimpleNode(String text, [List<(num, Node<num>)> childNodes = const []]) {
+ this.text.add(text);
+ this.childNodes.addAll(childNodes);
+ }
+}
diff --git a/pkgs/api_summary/test/test_utils.dart b/pkgs/api_summary/test/test_utils.dart
new file mode 100644
index 0000000..2376ed1
--- /dev/null
+++ b/pkgs/api_summary/test/test_utils.dart
@@ -0,0 +1,26 @@
+// 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/element/element.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
+// TODO(paulberry): move pub_package_resolution.dart out of src
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:test/test.dart';
+
+class ApiSummaryTest extends PubPackageResolutionTest {
+ Future<LibraryElement> analyzeLibrary(
+ String content, {
+ String pathWithinLib = 'test.dart',
+ }) async {
+ final file = newFile('$testPackageLibPath/$pathWithinLib', content);
+ final resolvedUnitResult = await resolveFile(file.path);
+ expect(
+ resolvedUnitResult.diagnostics.where(
+ (diagnostic) => diagnostic.severity == Severity.error,
+ ),
+ isEmpty,
+ );
+ return resolvedUnitResult.libraryElement;
+ }
+}
diff --git a/pkgs/api_summary/test/unique_namer_test.dart b/pkgs/api_summary/test/unique_namer_test.dart
new file mode 100644
index 0000000..cb95b28
--- /dev/null
+++ b/pkgs/api_summary/test/unique_namer_test.dart
@@ -0,0 +1,82 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:api_summary/src/unique_namer.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(UniqueNamerTest);
+ });
+}
+
+@reflectiveTest
+class UniqueNamerTest extends ApiSummaryTest {
+ Future<void> test_collidingNamesAreDisambiguated() async {
+ final f1 = (await analyzeLibrary(
+ pathWithinLib: 'file1.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final f2 = (await analyzeLibrary(
+ pathWithinLib: 'file2.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final uniqueNamer = UniqueNamer();
+ final f1Name = uniqueNamer.name(f1);
+ final f2Name = uniqueNamer.name(f2);
+ expect(f1Name.toString(), 'f@1');
+ expect(f2Name.toString(), 'f@2');
+ }
+
+ Future<void> test_name_returnsSameNameOnSuccessiveCalls() async {
+ final f = (await analyzeLibrary('f() {}')).getTopLevelFunction('f')!;
+ final uniqueNamer = UniqueNamer();
+ final name1 = uniqueNamer.name(f);
+ final name2 = uniqueNamer.name(f);
+ expect(name1, same(name2));
+ }
+
+ Future<void> test_nonCollidingNamesAreNotDisambiguated() async {
+ final f = (await analyzeLibrary(
+ pathWithinLib: 'file1.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final g = (await analyzeLibrary(
+ pathWithinLib: 'file2.dart',
+ 'g() {}',
+ )).getTopLevelFunction('g')!;
+ final uniqueNamer = UniqueNamer();
+ final fName = uniqueNamer.name(f);
+ final gName = uniqueNamer.name(g);
+ expect(fName.toString(), 'f');
+ expect(gName.toString(), 'g');
+ }
+
+ Future<void> test_three_collisions() async {
+ final f1 = (await analyzeLibrary(
+ pathWithinLib: 'file1.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final f2 = (await analyzeLibrary(
+ pathWithinLib: 'file2.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final f3 = (await analyzeLibrary(
+ pathWithinLib: 'file3.dart',
+ 'f() {}',
+ )).getTopLevelFunction('f')!;
+ final uniqueNamer = UniqueNamer();
+ final f1Name = uniqueNamer.name(f1);
+ final f2Name = uniqueNamer.name(f2);
+ final f3Name = uniqueNamer.name(f3);
+ expect(f1Name.toString(), 'f@1');
+ expect(f2Name.toString(), 'f@2');
+ expect(f3Name.toString(), 'f@3');
+ }
+}
diff --git a/pkgs/api_summary/test/uri_sorting_test.dart b/pkgs/api_summary/test/uri_sorting_test.dart
new file mode 100644
index 0000000..3d7a541
--- /dev/null
+++ b/pkgs/api_summary/test/uri_sorting_test.dart
@@ -0,0 +1,56 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:api_summary/src/uri_sorting.dart';
+import 'package:collection/collection.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(UriTest);
+ });
+}
+
+@reflectiveTest
+class UriTest {
+ void test_sortOrder_inOrOutOfPackageBeforeName() {
+ _checkSorting(
+ uris: [
+ Uri.parse('package:a/a.dart'),
+ Uri.parse('package:b/b.dart'),
+ Uri.parse('package:c/c.dart'),
+ ],
+ expectedOrder: [
+ 'package:b/b.dart',
+ 'package:a/a.dart',
+ 'package:c/c.dart',
+ ],
+ packageName: 'b',
+ );
+ }
+
+ void _checkSorting({
+ required List<Uri> uris,
+ required List<String> expectedOrder,
+ required String packageName,
+ }) {
+ expect(
+ uris
+ .sortedBy((e) => UriSortKey(e, packageName))
+ .map((e) => e.toString())
+ .toList(),
+ expectedOrder,
+ );
+ expect(
+ uris.reversed
+ .sortedBy((e) => UriSortKey(e, packageName))
+ .map((e) => e.toString())
+ .toList(),
+ expectedOrder,
+ );
+ }
+}