blob: 944320be577cec38ba84ce5d61e75f9704efb8f6 [file] [log] [blame]
// 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:analyzer_utilities/src/api_summary/src/api_description.dart';
import 'package:analyzer_utilities/src/api_summary/src/api_summary_customizer.dart';
import 'package:analyzer_utilities/src/api_summary/src/node.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utilities.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 {
var 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 {
var 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.
var 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.
var 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 {
var 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.
var 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 {
var 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 {
var 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 {
var 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 {
var 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 {
var 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 {
var 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_sealedClass() async {
var 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 {
var 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)".
var 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.
var 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 {
var 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 {
var 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.
var 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 {
var 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 {
var summary = await _build({'$testPackageLibPath/file.dar': 'f() {}'});
expect(summary, '');
}
Future<void> test_topLevel_otherPackagePublicApi() async {
var 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.
var 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 {
var 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.
var 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 {
var 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 {
var 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 {
var 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 (var file in files.keys) {
if (file.endsWith('.dart')) await assertNoDiagnosticsInFile(file);
}
// Generate the API description.
var context = contextCollection.contextFor(convertPath(testPackageLibPath));
var customizer = createCustomizer?.call() ?? _ValidatingCustomizer();
var apiDescription = ApiDescription('test', customizer);
var stringBuffer = StringBuffer();
var 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);
}
}