| // 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; | |
| } |