blob: a1498be443441540a38c8f47d1be44d6b1dd5b0c [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:convert';
import 'dart:core';
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(ApiBuilderTest);
});
}
@reflectiveTest
class ApiBuilderTest 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_fromJson_roundTrip() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
class C<T extends Object> {
int x = 0;
void m(String requiredParam, {int? optionalParam}) {}
}
enum E { a, b }
extension Ext on int {}
''',
});
final decodedMap = jsonDecode(summary) as Map<String, dynamic>;
final rehydrated = ApiSummary.fromJson(decodedMap);
expect(rehydrated.name, 'test');
expect(rehydrated.libraries, hasLength(1));
final library = rehydrated.libraries.single;
expect(library.uri, 'package:test/file.dart');
expect(library.classes, hasLength(1));
expect(library.enums, hasLength(1));
expect(library.extensions, hasLength(1));
final cls = library.classes.single;
expect(cls.name, 'C');
expect(cls.typeParameters, contains('T'));
expect(
cls.typeParameters['T'],
isA<ApiInterfaceType>().having((t) => t.name, 'name', 'Object'),
);
expect(cls.methods.map((m) => m.name), containsAll(['m', 'x', 'x=']));
final methodM = cls.methods.firstWhere((m) => m.name == 'm');
expect(
methodM,
isA<ApiExecutable>()
.having((m) => m.returnType, 'returnType', isA<ApiVoidType>())
.having((m) => m.parameters, 'parameters', hasLength(2)),
);
expect(
methodM.parameters[0],
isA<ApiParameter>()
.having((p) => p.name, 'name', 'requiredParam')
.having(
(p) => p.type,
'type',
isA<ApiInterfaceType>().having((t) => t.name, 'name', 'String'),
)
.having((p) => p.isRequired, 'isRequired', isTrue),
);
expect(
methodM.parameters[1],
isA<ApiParameter>()
.having((p) => p.name, 'name', 'optionalParam')
.having(
(p) => p.type,
'type',
isA<ApiInterfaceType>().having((t) => t.name, 'name', 'int'),
)
.having((p) => p.isNamed, 'isNamed', isTrue),
);
// Verify re-encoding produces the same output
final reEncodedSummary = jsonEncode(rehydrated.toJson());
expect(reEncodedSummary, summary);
}
Future<void> test_fromJson_roundTrip_comprehensive() async {
final summary = await _build({
'$testPackageLibPath/comprehensive.dart': '''
import 'package:meta/meta.dart';
@experimental
@deprecated
int topLevelVar = 42;
typedef IntAlias = int;
typedef BoundedAlias<T extends num> = Map<String, T>;
void takeRecord((int, {String name}) rec) {}
void takeFunc(int Function(String) fn) {}
void takeGenericFunc(void Function<T extends num>(T) fn) {}
void genericMethod<T extends num>(T t) {}
extension type Id(int value) {}
extension type IdBounded<T extends num>(int value) implements int {}
extension Ext<T extends num> on List<T> {}
mixin M on Object {}
''',
});
final decodedMap = jsonDecode(summary) as Map<String, dynamic>;
final rehydrated = ApiSummary.fromJson(decodedMap);
final reEncodedSummary = jsonEncode(rehydrated.toJson());
expect(reEncodedSummary, summary);
final renderedText = rehydrated.toString();
expect(
renderedText,
equals('''
package:test/comprehensive.dart:
topLevelVar (static getter: int, deprecated, experimental)
topLevelVar= (static setter: int, deprecated, experimental)
genericMethod (function: void Function<T extends num>(T))
takeFunc (function: void Function(int Function(String)))
takeGenericFunc (function: void Function(void Function<T extends num>(T)))
takeRecord (function: void Function((int, {String name})))
Id (extension type):
new (constructor: Id Function(int))
value (getter: int)
IdBounded (extension type<T extends num> implements int):
new (constructor: IdBounded<T> Function(int))
value (getter: int)
M (mixin on Object)
Ext (extension on List<T>)
BoundedAlias (type alias<T extends num> for Map<String, T>)
IntAlias (type alias for int)
dart:core:
List (referenced)
Map (referenced)
Object (referenced)
String (referenced)
int (referenced)
num (referenced)
'''),
);
}
Future<void> test_jsonOutput() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
class C {
int x = 0;
void m() {}
}
''',
});
final decoded = jsonDecode(summary) as Map<String, dynamic>;
expect(
decoded,
equals({
'name': 'test',
'libraries': [
{
'uri': 'package:test/file.dart',
'isPublicEntryPoint': true,
'classes': [
{
'name': 'C',
'locationUri': 'package:test/file.dart',
'supertype': {
'kind': 'interface',
'name': 'Object',
'libraryUri': 'dart:core',
},
'constructors': [
{
'name': 'new',
'locationUri': 'package:test/file.dart',
'kind': 'constructor',
'returnType': {
'kind': 'interface',
'name': 'C',
'libraryUri': 'package:test/file.dart',
},
},
],
'methods': [
{
'name': 'm',
'locationUri': 'package:test/file.dart',
'kind': 'method',
'returnType': {'kind': 'void'},
},
{
'name': 'x',
'locationUri': 'package:test/file.dart',
'kind': 'getter',
'returnType': {
'kind': 'interface',
'name': 'int',
'libraryUri': 'dart:core',
},
},
{
'name': 'x=',
'locationUri': 'package:test/file.dart',
'kind': 'setter',
'returnType': {'kind': 'void'},
'parameters': [
{
'name': 'value',
'type': {
'kind': 'interface',
'name': 'int',
'libraryUri': 'dart:core',
},
'isRequired': true,
},
],
},
],
},
],
},
],
}),
);
}
Future<void> test_includeReferencedTypes() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
import 'package:foo/foo.dart';
import 'src/private.dart';
class MyClass extends Foo implements PrivateInterface {}
''',
'$testPackageLibPath/src/private.dart': '''
class PrivateInterface {}
''',
}, customizer: _IncludeReferencedTypesCustomizer());
final decodedMap = jsonDecode(summary) as Map<String, dynamic>;
final rehydrated = ApiSummary.fromJson(decodedMap);
// Verify package:foo/foo.dart is in the summary libraries
final fooLib = rehydrated.libraries.firstWhere(
(l) => l.uri == 'package:foo/foo.dart',
);
expect(fooLib.isPublicEntryPoint, isFalse);
expect(fooLib.classes, hasLength(1));
final fooClass = fooLib.classes.single;
expect(fooClass.name, 'Foo');
expect(fooClass.status, ApiDeclarationStatus.referenced);
expect(fooClass.constructors, isEmpty);
expect(fooClass.methods, isEmpty);
// Verify private.dart is in the summary libraries
final privateLib = rehydrated.libraries.firstWhere(
(l) => l.uri == 'package:test/src/private.dart',
);
expect(privateLib.isPublicEntryPoint, isFalse);
expect(privateLib.classes, hasLength(1));
final privateClass = privateLib.classes.single;
expect(privateClass.name, 'PrivateInterface');
expect(privateClass.status, ApiDeclarationStatus.nonPublic);
expect(privateClass.constructors, isEmpty);
expect(privateClass.methods, isEmpty);
// Verify dart:core is also in the summary libraries for Object
final coreLib = rehydrated.libraries.firstWhere(
(l) => l.uri == 'dart:core',
);
expect(coreLib.isPublicEntryPoint, isFalse);
expect(coreLib.classes.map((c) => c.name), contains('Object'));
final objectClass = coreLib.classes.firstWhere((c) => c.name == 'Object');
expect(objectClass.status, ApiDeclarationStatus.referenced);
expect(objectClass.constructors, isEmpty);
}
Future<void> test_constMembers() async {
final summary = await _build({
'$testPackageLibPath/file.dart': '''
class MyClass {
static const int myConstField = 42;
final int myFinalField;
const MyClass(this.myFinalField);
MyClass.nonConst() : myFinalField = 0;
}
const int myTopLevelConst = 100;
enum MyEnum {
v1, v2;
static const int customConst = 3;
}
''',
});
final decodedMap = jsonDecode(summary) as Map<String, dynamic>;
final rehydrated = ApiSummary.fromJson(decodedMap);
final lib = rehydrated.libraries.single;
// Verify top-level const
final topLevelConst = lib.functions.firstWhere(
(e) => e.name == 'myTopLevelConst',
);
expect(topLevelConst.kind, ApiExecutableKind.getter);
expect(topLevelConst.isConst, isTrue);
expect(topLevelConst.isEnumConstant, isFalse);
final myClass = lib.classes.single;
// Verify static const field (getter)
final constField = myClass.methods.firstWhere(
(e) => e.name == 'myConstField',
);
expect(constField.kind, ApiExecutableKind.getter);
expect(constField.isConst, isTrue);
expect(constField.isEnumConstant, isFalse);
// Verify final field (getter)
final finalField = myClass.methods.firstWhere(
(e) => e.name == 'myFinalField',
);
expect(finalField.kind, ApiExecutableKind.getter);
expect(finalField.isConst, isFalse);
expect(finalField.isEnumConstant, isFalse);
// Verify const constructor
final constConstructor = myClass.constructors.firstWhere(
(e) => e.name == 'new',
);
expect(constConstructor.kind, ApiExecutableKind.constructor);
expect(constConstructor.isConst, isTrue);
// Verify non-const constructor
final nonConstConstructor = myClass.constructors.firstWhere(
(e) => e.name == 'nonConst',
);
expect(nonConstConstructor.kind, ApiExecutableKind.constructor);
expect(nonConstConstructor.isConst, isFalse);
// Verify enum constants
final myEnum = lib.enums.single;
final enumVal1 = myEnum.methods.firstWhere((e) => e.name == 'v1');
expect(enumVal1.isConst, isTrue);
expect(enumVal1.isEnumConstant, isTrue);
final enumCustomConst = myEnum.methods.firstWhere(
(e) => e.name == 'customConst',
);
expect(enumCustomConst.isConst, isTrue);
expect(enumCustomConst.isEnumConstant, isFalse);
final enumValues = myEnum.methods.firstWhere((e) => e.name == 'values');
expect(enumValues.isConst, isTrue);
expect(enumValues.isEnumConstant, isFalse);
}
Future<String> _build(
Map<String, String> files, {
ApiSummaryCustomizer? 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 ?? const ApiSummaryCustomizer();
final package = await buildApiPackage('test', context, resolvedCustomizer);
return jsonEncode(package.toJson());
}
}
base class _IncludeReferencedTypesCustomizer extends ApiSummaryCustomizer {
@override
bool get includeReferencedTypes => true;
}