| // 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/element/element.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(MetaAnnotationsTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class MetaAnnotationsTest extends ApiSummaryTest { |
| @override |
| bool get addMetaPackageDep => true; |
| |
| Future<String> _build( |
| Map<String, String> files, { |
| ApiSummaryCustomizer? customizer, |
| }) async { |
| files.forEach(newFile); |
| for (final file in files.keys) { |
| if (file.endsWith('.dart')) await assertNoDiagnosticsInFile(file); |
| } |
| final context = contextCollection.contextFor( |
| convertPath(testPackageLibPath), |
| ); |
| final package = await buildApiPackage( |
| 'test', |
| context, |
| customizer ?? const ApiSummaryCustomizer(), |
| ); |
| return package.toString(); |
| } |
| |
| Future<ApiSummary> _buildModel(Map<String, String> files) async { |
| files.forEach(newFile); |
| for (final file in files.keys) { |
| if (file.endsWith('.dart')) await assertNoDiagnosticsInFile(file); |
| } |
| final context = contextCollection.contextFor( |
| convertPath(testPackageLibPath), |
| ); |
| return buildApiPackage('test', context, const ApiSummaryCustomizer()); |
| } |
| |
| Future<void> test_class_immutable() async { |
| final summary = await _build({ |
| '$testPackageLibPath/file.dart': ''' |
| import 'package:meta/meta.dart'; |
| |
| @immutable |
| class ImmutableClass { |
| final int x; |
| const ImmutableClass(this.x); |
| } |
| ''', |
| }); |
| |
| expect(summary, ''' |
| package:test/file.dart: |
| ImmutableClass (class extends Object, immutable): |
| new (const constructor: ImmutableClass Function(int)) |
| x (getter: int) |
| dart:core: |
| Object (referenced) |
| int (referenced) |
| '''); |
| } |
| |
| Future<void> test_internal_annotations() async { |
| final summary = await _build({ |
| '$testPackageLibPath/file.dart': ''' |
| import 'src/internal.dart'; |
| |
| InternalClass getInternal() => InternalClass(); |
| ''', |
| '$testPackageLibPath/src/internal.dart': ''' |
| import 'package:meta/meta.dart'; |
| |
| @internal |
| class InternalClass { |
| @internal |
| void internalMethod() {} |
| } |
| ''', |
| }, customizer: _ShowInternalDetailsCustomizer()); |
| |
| expect(summary, ''' |
| package:test/file.dart: |
| getInternal (function: InternalClass Function()) |
| package:test/src/internal.dart: |
| InternalClass (class extends Object, non-public, internal): |
| new (constructor: InternalClass Function(), internal) |
| internalMethod (method: void Function(), internal) |
| dart:core: |
| Object (referenced) |
| '''); |
| } |
| |
| Future<void> test_member_contractual_annotations() async { |
| final summary = await _build({ |
| '$testPackageLibPath/file.dart': ''' |
| import 'package:meta/meta.dart'; |
| |
| class Base { |
| @protected |
| void protectedMethod() {} |
| |
| @mustCallSuper |
| void mustCallSuperMethod() {} |
| |
| @visibleForOverriding |
| void visibleForOverridingMethod() {} |
| |
| @nonVirtual |
| void nonVirtualMethod() {} |
| |
| @mustBeOverridden |
| void mustBeOverriddenMethod() {} |
| |
| @useResult |
| int useResultMethod() => 42; |
| |
| @protected |
| int protectedField = 0; |
| } |
| |
| @useResult |
| int topLevelUseResultFunction() => 1; |
| ''', |
| }); |
| |
| expect(summary, ''' |
| package:test/file.dart: |
| topLevelUseResultFunction (function: int Function(), use result) |
| Base (class extends Object): |
| new (constructor: Base Function()) |
| protectedField (getter: int, protected) |
| protectedField= (setter: int, protected) |
| mustBeOverriddenMethod (method: void Function(), must be overridden) |
| mustCallSuperMethod (method: void Function(), must call super) |
| nonVirtualMethod (method: void Function(), non-virtual) |
| protectedMethod (method: void Function(), protected) |
| useResultMethod (method: int Function(), use result) |
| visibleForOverridingMethod (method: void Function(), visible for overriding) |
| dart:core: |
| Object (referenced) |
| int (referenced) |
| '''); |
| } |
| |
| Future<void> test_json_roundtrip_with_meta_annotations() async { |
| final model = await _buildModel({ |
| '$testPackageLibPath/file.dart': ''' |
| import 'package:meta/meta.dart'; |
| |
| @immutable |
| class Target { |
| @protected |
| @mustCallSuper |
| @nonVirtual |
| @useResult |
| int calculate() => 0; |
| } |
| ''', |
| }); |
| |
| final jsonMap = model.toJson(); |
| final rehydrated = ApiSummary.fromJson(jsonMap); |
| |
| final cls = rehydrated.libraries.single.classes.single; |
| expect(cls.name, 'Target'); |
| expect(cls.contracts, contains(MetaContract.immutable)); |
| expect(cls.contracts, isNot(contains(MetaContract.internal))); |
| |
| final method = cls.methods.singleWhere((m) => m.name == 'calculate'); |
| expect( |
| method.contracts, |
| equals({ |
| MetaContract.mustCallSuper, |
| MetaContract.nonVirtual, |
| MetaContract.protected, |
| MetaContract.useResult, |
| }), |
| ); |
| |
| // Verify facets in JSON output |
| final libJson = (jsonMap['libraries'] as List) |
| .cast<Map<String, dynamic>>()[0]; |
| final classJson = (libJson['classes'] as List) |
| .cast<Map<String, dynamic>>()[0]; |
| final methodJson = (classJson['methods'] as List) |
| .cast<Map<String, dynamic>>()[0]; |
| |
| expect( |
| classJson, |
| containsPair('facets', [ |
| { |
| 'namespace': 'meta', |
| 'contracts': ['immutable'], |
| }, |
| ]), |
| ); |
| expect( |
| methodJson, |
| containsPair('facets', [ |
| { |
| 'namespace': 'meta', |
| 'contracts': [ |
| 'mustCallSuper', |
| 'nonVirtual', |
| 'protected', |
| 'useResult', |
| ], |
| }, |
| ]), |
| ); |
| } |
| |
| void test_facets_deduplication_sorting_and_immutability() { |
| // 1. Verify MetaContractFacet deduplicates contracts and sorts them |
| // deterministically. |
| final metaFacet1 = MetaContractFacet([ |
| MetaContract.useResult, |
| MetaContract.protected, |
| MetaContract.useResult, |
| MetaContract.immutable, |
| MetaContract.protected, |
| MetaContract.useResult, |
| ]); |
| |
| expect( |
| metaFacet1.contracts.toList(), |
| equals([ |
| MetaContract.immutable, |
| MetaContract.protected, |
| MetaContract.useResult, |
| ]), |
| ); |
| |
| // Verify immutability of contracts set |
| expect( |
| () => (metaFacet1.contracts as dynamic).add(MetaContract.internal), |
| throwsUnsupportedError, |
| ); |
| |
| // Verify equality with differently ordered construction |
| final metaFacet2 = MetaContractFacet([ |
| MetaContract.protected, |
| MetaContract.immutable, |
| MetaContract.useResult, |
| ]); |
| expect(metaFacet1, equals(metaFacet2)); |
| expect(metaFacet1.hashCode, equals(metaFacet2.hashCode)); |
| |
| // Verify fromJson gracefully ignores unknown contracts |
| final fromJsonFacet = MetaContractFacet.fromJson({ |
| 'namespace': 'meta', |
| 'contracts': ['protected', 'unknownFutureContract', 'useResult'], |
| }); |
| expect( |
| fromJsonFacet.contracts.toList(), |
| equals([MetaContract.protected, MetaContract.useResult]), |
| ); |
| |
| // 2. Verify ApiDeclaration deduplicates facets and sorts them |
| // deterministically. |
| final dummyExecutable = ApiExecutable( |
| name: 'foo', |
| kind: ApiExecutableKind.function, |
| typeParameters: const {}, |
| returnType: const ApiVoidType(), |
| parameters: const [], |
| isStatic: true, |
| facets: [metaFacet1, metaFacet2, metaFacet1], |
| ); |
| |
| expect(dummyExecutable.facets.length, equals(1)); |
| expect(dummyExecutable.facets.single, equals(metaFacet1)); |
| expect( |
| () => (dummyExecutable.facets as dynamic).add(metaFacet1), |
| throwsUnsupportedError, |
| ); |
| } |
| } |
| |
| final class _ShowInternalDetailsCustomizer extends ApiSummaryCustomizer { |
| @override |
| bool shouldShowDetails(Element element, ApiSummaryContext context) => |
| element.name?.contains('Internal') ?? false; |
| } |
| |
| extension on ApiDeclaration { |
| Set<MetaContract> get contracts => |
| facets.whereType<MetaContractFacet>().firstOrNull?.contracts ?? const {}; |
| } |