blob: 29a30039b1de0ae1763f10b919ff24db95b69947 [file] [edit]
// 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 'dart:typed_data';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:api_summary/api_summary.dart';
import 'package:api_summary/src/api_builder.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'test_utils.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(ApiDescriptionTest);
defineReflectiveTests(ApiDescriptionDeletedFileTest);
});
}
@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_mixins() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
mixin M1 {}
mixin M2 {}
class C1 extends Object with M1 {}
class C2 extends Object with M2 implements M1 {}
''',
});
expect(summary, '''
package:test/file.dart:
C1 (class extends Object with M1):
new (constructor: C1 Function())
C2 (class extends Object with M2 implements M1):
new (constructor: C2 Function())
M1 (mixin on Object)
M2 (mixin on Object)
dart:core:
Object (referenced)
''');
}
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 (mixin class extends Object, abstract, base, mixin):
new (constructor: ABM Function())
AF (class extends Object, abstract, final)
AI (class extends Object, abstract, interface)
AM (mixin class extends Object, abstract, mixin):
new (constructor: AM Function())
B (class extends Object, base):
new (constructor: B Function())
BM (mixin 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 (mixin 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 {}
''',
},
customizer: _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:
shown1 (function: void Function(Shown2, Hidden2))
package:test/src/private.dart:
Hidden2 (non-public)
Shown2 (class extends Object, non-public):
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 (enum constant: E)
values (static const 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 (enum constant: E1)
values (static const 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_exportShow() async {
final summary = await _build({
'$testPackageLibPath/public.dart':
'export "src/private.dart" show Shown;',
'$testPackageLibPath/src/private.dart': '''
class Shown {}
class Hidden {}
''',
});
expect(summary, '''
package:test/public.dart:
Shown (class extends Object):
new (constructor: Shown Function())
dart:core:
Object (referenced)
''');
}
Future<void> test_topLevel_exportHide() async {
final summary = await _build({
'$testPackageLibPath/public.dart':
'export "src/private.dart" hide Hidden;',
'$testPackageLibPath/src/private.dart': '''
class Shown {}
class Hidden {}
''',
});
expect(summary, '''
package:test/public.dart:
Shown (class extends Object):
new (constructor: Shown Function())
dart:core:
Object (referenced)
''');
}
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 (enum constant: E)
values (static const 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<void> test_type_never() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
Never get n => throw '';
''',
});
expect(summary, '''
package:test/file.dart:
n (static getter: Never)
dart:core:
Never (referenced)
''');
}
Future<void> test_unnamed_extensions_excluded() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
class C {}
extension on C {
void f1() {}
}
''',
});
expect(summary, '''
package:test/file.dart:
C (class extends Object):
new (constructor: C Function())
dart:core:
Object (referenced)
''');
}
Future<void> test_multiple_named_extensions() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
class C {}
extension E1 on C {
void f1() {}
}
extension E2 on C {
void f2() {}
}
''',
});
expect(summary, '''
package:test/file.dart:
C (class extends Object):
new (constructor: C Function())
E1 (extension on C):
f1 (method: void Function())
E2 (extension on C):
f2 (method: void Function())
dart:core:
Object (referenced)
''');
}
Future<void> test_visibleForTesting() async {
final description = await _build({
'$testPackageLibPath/temp.dart': '''
import 'package:meta/meta.dart';
@visibleForTesting
class C {}
@visibleForTesting
void f() {}
''',
});
expect(description, '''
package:test/temp.dart:
f (function: void Function(), visible for testing)
C (class extends Object, visible for testing):
new (constructor: C Function(), visible for testing)
dart:core:
Object (referenced)
''');
}
Future<String> _build(
Map<String, String> files, {
_ValidatingCustomizer? customizer,
}) 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 resolvedCustomizer = customizer ?? _ValidatingCustomizer();
final package = await buildApiPackage('test', context, resolvedCustomizer);
expect(resolvedCustomizer.initialScanCompleteCalled, isTrue);
return package.toString();
}
}
final class _ShouldShowDetailsCustomizer extends _ValidatingCustomizer {
final bool Function(Element) _shouldShowDetails;
_ShouldShowDetailsCustomizer(this._shouldShowDetails);
@override
bool shouldShowDetails(Element element, ApiSummaryContext context) {
expect(initialScanCompleteCalled, isTrue);
return _shouldShowDetails(element);
}
}
base class _ValidatingCustomizer extends ApiSummaryCustomizer {
bool initialScanCompleteCalled = false;
bool setupCompleteCalled = false;
@override
Future<void> initialScanComplete(ApiSummaryContext context) async {
expect(setupCompleteCalled, isTrue);
expect(context.topLevelPublicElements, isNotNull);
expect(context.publicApiLibraries, isNotNull);
initialScanCompleteCalled = true;
await super.initialScanComplete(context);
}
@override
Future<void> setupComplete(ApiSummaryContext context) async {
expect(context.packageName, 'test');
expect(context.analysisContext, isNotNull);
setupCompleteCalled = true;
await super.setupComplete(context);
}
@override
bool shouldShowDetails(Element element, ApiSummaryContext context) {
expect(initialScanCompleteCalled, isTrue);
return super.shouldShowDetails(element, context);
}
}
@reflectiveTest
class ApiDescriptionDeletedFileTest extends ApiSummaryTest {
@override
bool get addMetaPackageDep => true;
late final _myProvider = _MyResourceProvider();
@override
MemoryResourceProvider get resourceProvider => _myProvider;
Future<void> test_deletedFile_graceful() async {
final filePath1 = convertPath('$testPackageLibPath/temp1.dart');
final filePath2 = convertPath('$testPackageLibPath/temp2.dart');
newFile(filePath1, 'class Temp1 {}');
newFile(filePath2, 'class Temp2 {}');
// Make temp2.dart unreadable.
_myProvider.unreadables.add(filePath2);
final context = contextCollection.contextFor(
convertPath(testPackageLibPath),
);
// Verify it doesn't crash when summarizing.
final package = await buildApiPackage(
'test',
context,
_ValidatingCustomizer(),
);
// temp1.dart should be fully summarized.
expect(
package.libraries.map((l) => l.uri),
contains('package:test/temp1.dart'),
);
// temp2.dart was unreadable, so it should resolve as an empty library
// without crashing.
final temp2Lib = package.libraries.firstWhere(
(l) => l.uri == 'package:test/temp2.dart',
);
expect(temp2Lib.classes, isEmpty);
expect(temp2Lib.enums, isEmpty);
expect(temp2Lib.mixins, isEmpty);
expect(temp2Lib.extensions, isEmpty);
expect(temp2Lib.extensionTypes, isEmpty);
expect(temp2Lib.functions, isEmpty);
expect(temp2Lib.typeAliases, isEmpty);
}
}
final class _MyResourceProvider extends MemoryResourceProvider {
final Set<String> unreadables = {};
_MyResourceProvider() : super();
@override
File getFile(String path) {
final file = super.getFile(path);
if (unreadables.contains(path)) {
return _UnreadableFile(file);
}
return file;
}
}
final class _UnreadableFile implements File {
final File _delegate;
_UnreadableFile(this._delegate);
@override
String get path => _delegate.path;
@override
bool get exists => _delegate.exists;
@override
Folder get parent => _delegate.parent;
@override
String readAsStringSync() => throw FileSystemException(path, 'Cannot read');
@override
Uint8List readAsBytesSync() => throw FileSystemException(path, 'Cannot read');
@override
dynamic noSuchMethod(Invocation invocation) {
throw UnimplementedError('noSuchMethod: ${invocation.memberName}');
}
}