blob: 3618ee42a54a80c015d12a7f83d5f6fad2243423 [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 '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 {};
}