Skip to content

Commit c436f99

Browse files
authored
feat(api_summary): Move api_summary package into the tools monorepo (#2412)
1 parent abd6780 commit c436f99

26 files changed

Lines changed: 2725 additions & 0 deletions

.github/workflows/api_summary.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: package:api_summary
2+
permissions: read-all
3+
4+
on:
5+
pull_request:
6+
paths:
7+
- '.github/workflows/api_summary.yaml'
8+
- 'pkgs/api_summary/**'
9+
push:
10+
branches: [ main ]
11+
paths:
12+
- '.github/workflows/api_summary.yaml'
13+
- 'pkgs/api_summary/**'
14+
schedule:
15+
- cron: '0 0 * * 0' # weekly
16+
17+
defaults:
18+
run:
19+
working-directory: pkgs/api_summary
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
sdk: ['3.12', dev]
28+
include:
29+
- sdk: dev
30+
check-formatting: true
31+
steps:
32+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
33+
- uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260
34+
with:
35+
sdk: ${{matrix.sdk}}
36+
37+
- run: dart pub get
38+
39+
- run: dart analyze --fatal-infos
40+
41+
- run: dart format --output=none --set-exit-if-changed .
42+
if: ${{matrix.check-formatting}}
43+
44+
- run: dart test

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ don't naturally belong to other topic monorepos (like
1414

1515
| Package | Description | Issues | Version |
1616
| --- | --- | --- | --- |
17+
| [api_summary](pkgs/api_summary/) | Creates an API summary for a package. | [![issues](https://img.shields.io/badge/issues-4774bc)][api_summary_issues] | [![pub package](https://img.shields.io/pub/v/api_summary.svg)](https://pub.dev/packages/api_summary) |
1718
| [bazel_worker](pkgs/bazel_worker/) | Protocol and utilities to implement or invoke persistent bazel workers. | [![issues](https://img.shields.io/badge/issues-4774bc)][bazel_worker_issues] | [![pub package](https://img.shields.io/pub/v/bazel_worker.svg)](https://pub.dev/packages/bazel_worker) |
1819
| [benchmark_harness](pkgs/benchmark_harness/) | The official Dart project benchmark harness. | [![issues](https://img.shields.io/badge/issues-4774bc)][benchmark_harness_issues] | [![pub package](https://img.shields.io/pub/v/benchmark_harness.svg)](https://pub.dev/packages/benchmark_harness) |
1920
| [boolean_selector](pkgs/boolean_selector/) | A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax. | [![issues](https://img.shields.io/badge/issues-4774bc)][boolean_selector_issues] | [![pub package](https://img.shields.io/pub/v/boolean_selector.svg)](https://pub.dev/packages/boolean_selector) |
@@ -56,6 +57,7 @@ don't naturally belong to other topic monorepos (like
5657
| [yaml](pkgs/yaml/) | A parser for YAML, a human-friendly data serialization standard | [![issues](https://img.shields.io/badge/issues-4774bc)][yaml_issues] | [![pub package](https://img.shields.io/pub/v/yaml.svg)](https://pub.dev/packages/yaml) |
5758
| [yaml_edit](pkgs/yaml_edit/) | A library for YAML manipulation with comment and whitespace preservation. | [![issues](https://img.shields.io/badge/issues-4774bc)][yaml_edit_issues] | [![pub package](https://img.shields.io/pub/v/yaml_edit.svg)](https://pub.dev/packages/yaml_edit) |
5859

60+
[api_summary_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aapi_summary
5961
[bazel_worker_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Abazel_worker
6062
[benchmark_harness_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Abenchmark_harness
6163
[boolean_selector_issues]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aboolean_selector

pkgs/api_summary/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0-wip
2+
3+
- First release.

pkgs/api_summary/LICENSE

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Copyright 2026, the Dart project authors.
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are
5+
met:
6+
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above
10+
copyright notice, this list of conditions and the following
11+
disclaimer in the documentation and/or other materials provided
12+
with the distribution.
13+
* Neither the name of Google LLC nor the names of its
14+
contributors may be used to endorse or promote products derived
15+
from this software without specific prior written permission.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

pkgs/api_summary/README.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
[![Build Status](https://github.com/dart-lang/tools/actions/workflows/api_summary.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/api_summary.yaml)
2+
[![pub package](https://img.shields.io/pub/v/api_summary.svg)](https://pub.dev/packages/api_summary)
3+
[![package publisher](https://img.shields.io/pub/publisher/api_summary.svg)](https://pub.dev/packages/api_summary/publisher)
4+
5+
A library and command-line tool to create a human-readable text summary of the
6+
public API of a Dart package. This is highly suitable for tracking the public
7+
API footprints and ensuring that API modifications are visible during code
8+
reviews (e.g. using `diff` tests).
9+
10+
> [!NOTE]
11+
> For robust breaking change tracking, you should use
12+
> [dart_apitool](https://pub.dev/packages/dart_apitool).
13+
14+
## Command Line Usage
15+
16+
You can use the command-line tool to generate an API summary for any package
17+
containing a `pubspec.yaml` file.
18+
19+
### Installing Globally
20+
21+
Activate the package using `dart pub global`:
22+
23+
```bash
24+
dart pub global activate api_summary
25+
```
26+
27+
Then run the tool:
28+
29+
```bash
30+
api_summary --package-path /path/to/your/package
31+
```
32+
33+
Or, to run against the package in the current working directory, just run:
34+
35+
```bash
36+
api_summary
37+
```
38+
39+
### Running via `dart run`
40+
41+
Alternatively, you can run the executable from within a package directory if
42+
`api_summary` is a dependency:
43+
44+
```bash
45+
dart run api_summary
46+
```
47+
48+
### Options
49+
50+
* `-p, --package-path`: The path to the package directory to summarize
51+
(defaults to the current working directory).
52+
* `-h, --help`: Prints usage instructions.
53+
54+
## Programmatic Usage
55+
56+
You can also use this package programmatically inside your Dart projects, such
57+
as in automated testing or continuous integration scripts.
58+
59+
Add `api_summary` to your `pubspec.yaml`:
60+
61+
```yaml
62+
dependencies:
63+
api_summary: ^0.1.0-wip
64+
```
65+
66+
### Basic Example
67+
68+
Call the `summarizePackage` function to generate a package's public API
69+
representation:
70+
71+
```dart
72+
import 'dart:io';
73+
import 'package:api_summary/api_summary.dart';
74+
75+
void main() async {
76+
final summary = await summarizePackage('/path/to/package', 'my_package');
77+
print(summary);
78+
}
79+
```
80+
81+
An executable programmatic example is also available in the
82+
[example/example.dart](example/example.dart) file.
83+
84+
### Customizing the Summary
85+
86+
Extend the `ApiSummaryCustomizer` class to customize what is displayed in the
87+
API summary. For example, to exclude specific public classes or only display
88+
details of certain elements:
89+
90+
```dart
91+
import 'package:api_summary/api_summary.dart';
92+
import 'package:analyzer/dart/element/element.dart';
93+
94+
base class MyCustomizer extends ApiSummaryCustomizer {
95+
@override
96+
bool shouldShowDetails(Element element) {
97+
// Exclude elements named 'InternalHelper' from details printout
98+
if (element.name == 'InternalHelper') {
99+
return false;
100+
}
101+
return super.shouldShowDetails(element);
102+
}
103+
}
104+
105+
void main() async {
106+
final summary = await summarizePackage(
107+
'/path/to/package',
108+
'my_package',
109+
createCustomizer: () => MyCustomizer(),
110+
);
111+
print(summary);
112+
}
113+
```
114+
115+
## Golden File / Diff Testing
116+
117+
A common best practice with `api_summary` is to verify in a unit test that the
118+
generated summary matches a checked-in golden file (e.g. `api.txt`). If a
119+
developer introduces an accidental breaking change or adds a new public API
120+
element, the test will fail on the `diff`, prompting them to audit and
121+
intentionally update the golden file.
122+
123+
Below is an example of such a test (available in the
124+
[test/app_test.dart](test/app_test.dart) file):
125+
126+
```dart
127+
import 'dart:io';
128+
import 'package:path/path.dart' as p;
129+
import 'package:test/test.dart';
130+
import 'package:api_summary/api_summary.dart';
131+
132+
void main() {
133+
test('public API has not changed unexpectedly', () async {
134+
final packageDir = Directory.current.path;
135+
final goldenFile = File(p.join(packageDir, 'api.txt'));
136+
137+
final actualOutput = await summarizePackage(packageDir, 'my_package');
138+
139+
if (!goldenFile.existsSync()) {
140+
// In a new setup or after updates, generate the golden file first
141+
goldenFile.writeAsStringSync(actualOutput);
142+
fail('Golden file api.txt did not exist and has been generated. Please review and commit it.');
143+
}
144+
145+
final expectedOutput = goldenFile.readAsStringSync();
146+
expect(actualOutput, equals(expectedOutput));
147+
});
148+
}
149+
```
150+
151+
## Status: experimental
152+
153+
**NOTE**: This package is currently experimental and published under the
154+
[tools.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to
155+
solicit feedback.
156+
157+
These packages have a much higher expected rate of API and breaking changes.
158+
159+
Your feedback is valuable and will help us evolve this package. For general
160+
feedback, suggestions, and comments, please file an issue in the
161+
[bug tracker](https://github.com/dart-lang/tools/issues).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
include: package:dart_flutter_team_lints/analysis_options.yaml
2+
3+
analyzer:
4+
language:
5+
strict-raw-types: true
6+
7+
linter:
8+
rules:
9+
- avoid_catches_without_on_clauses
10+
- avoid_unused_constructor_parameters
11+
- cancel_subscriptions
12+
- literal_only_boolean_expressions
13+
- missing_whitespace_between_adjacent_strings
14+
- no_adjacent_strings_in_list
15+
- no_runtimeType_toString
16+
- prefer_const_declarations
17+
- prefer_expression_function_bodies
18+
- prefer_final_in_for_each
19+
- prefer_final_locals
20+
- simple_directive_paths
21+
- unnecessary_await_in_return
22+
- unnecessary_ignore

pkgs/api_summary/api.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package:api_summary/api_summary.dart:
2+
summarizePackage (function: Future<String> Function(String, String, {ApiSummaryCustomizer Function()? createCustomizer}))
3+
ApiSummaryCustomizer (class extends Object, base):
4+
new (constructor: ApiSummaryCustomizer Function())
5+
analysisContext= (setter: AnalysisContext)
6+
packageName= (setter: String)
7+
publicApiLibraries= (setter: Iterable<LibraryElement>)
8+
topLevelPublicElements (getter: Set<Element>)
9+
topLevelPublicElements= (setter: Set<Element>)
10+
initialScanComplete (method: Future<void> Function())
11+
setupComplete (method: Future<void> Function())
12+
shouldShowDetails (method: bool Function(Element))
13+
dart:async:
14+
Future (referenced)
15+
dart:core:
16+
Iterable (referenced)
17+
Object (referenced)
18+
Set (referenced)
19+
String (referenced)
20+
bool (referenced)
21+
package:analyzer/dart/analysis/analysis_context.dart:
22+
AnalysisContext (referenced)
23+
package:analyzer/dart/element/element.dart:
24+
Element (referenced)
25+
LibraryElement (referenced)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:api_summary/api_summary.dart';
8+
import 'package:args/args.dart';
9+
import 'package:path/path.dart' as p;
10+
import 'package:yaml/yaml.dart';
11+
12+
Future<void> main(List<String> arguments) async {
13+
try {
14+
final results = parser.parse(arguments);
15+
16+
if (results.flag('help')) {
17+
print('Usage: api_summary [options]');
18+
print(parser.usage);
19+
return;
20+
}
21+
22+
final packagePath =
23+
results.option('package-path') ?? Directory.current.path;
24+
final absolutePath = p.normalize(p.absolute(packagePath));
25+
final pubspecFile = File(p.join(absolutePath, 'pubspec.yaml'));
26+
27+
if (!pubspecFile.existsSync()) {
28+
stderr.writeln('Error: No pubspec.yaml found at "$absolutePath".');
29+
exitCode = 1;
30+
return;
31+
}
32+
33+
final packageName = _extractPackageName(pubspecFile);
34+
final summary = await summarizePackage(absolutePath, packageName);
35+
stdout.write(summary);
36+
} on FormatException catch (e) {
37+
stderr.writeln('Error: ${e.message}');
38+
stderr.writeln('\nUsage: api_summary [options]');
39+
stderr.writeln(parser.usage);
40+
exitCode = 64;
41+
return;
42+
}
43+
}
44+
45+
final parser = ArgParser()
46+
..addOption(
47+
'package-path',
48+
abbr: 'p',
49+
help:
50+
'The path to the package to summarize. Defaults to the current '
51+
'directory.',
52+
)
53+
..addFlag(
54+
'help',
55+
abbr: 'h',
56+
help: 'Print this usage information.',
57+
negatable: false,
58+
);
59+
60+
String _extractPackageName(File pubspecFile) {
61+
final content = pubspecFile.readAsStringSync();
62+
final yaml = loadYaml(content);
63+
if (yaml is! Map) {
64+
throw ArgumentError(
65+
'Expected pubspec.yaml at ${pubspecFile.path} to be a YAML map.',
66+
);
67+
}
68+
final name = yaml['name'];
69+
if (name == null) {
70+
throw ArgumentError(
71+
'Could not find a "name" field in pubspec.yaml at ${pubspecFile.path}.',
72+
);
73+
}
74+
if (name is! String) {
75+
throw ArgumentError(
76+
'The "name" field in pubspec.yaml at ${pubspecFile.path} must be a '
77+
'String.',
78+
);
79+
}
80+
return name;
81+
}

0 commit comments

Comments
 (0)