blob: ede11c4a04723fe3c4e2472c7455116186ff2e2d [file] [log] [blame]
// Copyright (c) 2020, 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.
import 'package:collection/collection.dart';
import '../config_provider.dart';
import '../context.dart';
import '../visitor/ast.dart';
import 'binding_string.dart';
import 'imports.dart';
import 'scope.dart';
import 'type.dart';
import 'utils.dart';
import 'writer.dart';
/// A binding for enums in C.
///
/// For a C enum -
/// ```c
/// enum Fruits {apple, banana = 10, yellow_fruit = 10};
/// ```
/// The generated dart code is
///
/// ```dart
/// enum Fruits {
/// apple(0),
/// banana(10);
///
/// static const yellow_fruit = banana;
///
/// final int value;
/// const Fruit(this.value);
///
/// @override
/// String toString() {
/// if (this == banana) return "Fruits.banana, Fruits.yellow_fruit";
/// return super.toString();
/// }
/// }
/// ```
class EnumClass extends BindingType with HasLocalScope {
/// Backing integer type for this enum.
Type nativeType;
/// A list of all the members of the native enum.
final List<EnumConstant> enumConstants;
Context context;
/// Whether this enum should be generated as a collection of integers.
EnumStyle style;
EnumClass({
super.usr,
super.originalName,
required super.name,
super.dartDoc,
Type? nativeType,
List<EnumConstant>? enumConstants,
required this.context,
this.style = EnumStyle.dartEnum,
}) : nativeType = nativeType ?? intType,
enumConstants = enumConstants ?? [];
/// Returns a string to declare the enum member and any documentation it may
/// have had.
String _formatValue(EnumConstant ec, {bool asInt = false}) {
final buffer = StringBuffer();
if (ec.dartDoc != null) {
buffer.write(' /// ');
buffer.writeAll(ec.dartDoc!.split('\n'), '\n /// ');
buffer.write('\n');
}
if (asInt) {
buffer.write(' static const ${ec.name} = ${ec.value};');
} else {
buffer.write(' ${ec.name}(${ec.value})');
}
return buffer.toString();
}
void _writeIntegerConstants(StringBuffer s) {
s.writeAll(enumConstants.map((c) => _formatValue(c, asInt: true)), '\n');
}
/// Writes the enum declarations for all unique members.
///
/// Eg, C: `apple = 1`, Dart: `apple(1)`
void _writeUniqueMembers(StringBuffer s, Set<EnumConstant> uniqueMembers) {
s.writeAll(uniqueMembers.map(_formatValue), ',\n');
if (uniqueMembers.isNotEmpty) s.write(';\n');
}
/// Writes alias declarations for all members with duplicate values.
///
/// Eg, C: `banana = 10, yellow_fruit = 10`.
/// Dart: `static const yellow_fruit = banana`.
void _writeDuplicateMembers(
StringBuffer s,
Map<EnumConstant, EnumConstant> duplicateToOriginal,
) {
if (duplicateToOriginal.isEmpty) return;
for (final entry in duplicateToOriginal.entries) {
final duplicate = entry.key;
final original = entry.value;
if (duplicate.dartDoc != null) {
s.write(' /// ');
s.writeAll(duplicate.dartDoc!.split('\n'), '\n /// ');
s.write('\n');
}
s.write(' static const ${duplicate.name} = ${original.name};\n');
}
}
/// Writes the constructor for the enum.
///
/// Always accepts an integer value to match the native value.
void _writeConstructor(StringBuffer s) {
s.write(' final int value;\n');
s.write(' const $name(this.value);\n');
}
/// Overrides [Enum.toString] so all aliases are included, if any.
///
/// If a native enum has two members with the same value, they are
/// functionally identical, and should be represented as such. This method
/// overrides [toString] to include all duplicate members in the same message.
void _writeToStringOverride(
StringBuffer s,
Map<EnumConstant, List<EnumConstant>> uniqueToDuplicates,
Map<EnumConstant, EnumConstant> duplicateToOriginal,
) {
if (duplicateToOriginal.isEmpty) return;
s.write(' @override\n');
s.write(' String toString() {\n');
for (final entry in uniqueToDuplicates.entries) {
// [!] All enum values were given a name when their declarations were
// generated.
final unique = entry.key;
final duplicates = entry.value;
if (duplicates.isEmpty) continue;
final allDuplicates = [
for (final duplicate in [unique] + duplicates)
'$name.${duplicate.name}',
].join(', ');
s.write(
' '
'if (this == ${unique.name}) return "$allDuplicates";\n',
);
}
s.write(' return super.toString();\n');
s.write(' }');
}
/// Writes the DartDoc string for this enum.
void _writeDartDoc(StringBuffer s) {
s.write(makeDartDoc(dartDoc));
}
/// Writes a sealed class when no members exist, because Dart enums cannot be
/// empty.
void _writeEmptyEnum(StringBuffer s) {
s.write('sealed class $name { }\n');
}
/// Writes a static function that maps integers to enum values.
void _writeFromValue(StringBuffer s, Set<EnumConstant> uniqueMembers) {
s.write(' static $name fromValue(int value) => switch (value) {\n');
for (final member in uniqueMembers) {
s.write(' ${member.value} => ${member.name},\n');
}
s.write(
' _ => throw ArgumentError('
"'Unknown value for ${Namer.stringLiteral(name)}: \$value'),\n",
);
s.write(' };\n');
}
@override
bool get isObjCImport =>
context.objCBuiltInFunctions.isBuiltInEnum(originalName);
@override
BindingString toBindingString(Writer w) {
final s = StringBuffer();
final uniqueToDuplicates = <EnumConstant, List<EnumConstant>>{};
final duplicateToOriginal = <EnumConstant, EnumConstant>{};
final uniqueMembers = <EnumConstant>{};
for (final ec in enumConstants) {
final original = uniqueMembers.firstWhereOrNull(
(other) => other.value == ec.value,
);
if (original == null) {
// This is a unique entry
uniqueMembers.add(ec);
uniqueToDuplicates[ec] = [];
} else {
// This is a duplicate of a previous entry
duplicateToOriginal[ec] = original;
uniqueToDuplicates[original]!.add(ec);
}
}
_writeDartDoc(s);
if (enumConstants.isEmpty) {
_writeEmptyEnum(s);
} else if (style == EnumStyle.intConstants) {
s.write('sealed class $name {\n');
_writeIntegerConstants(s);
s.write('}\n\n');
} else {
s.write('enum $name {\n');
_writeUniqueMembers(s, uniqueMembers);
s.write('\n');
_writeDuplicateMembers(s, duplicateToOriginal);
s.write('\n');
_writeConstructor(s);
s.write('\n');
_writeFromValue(s, uniqueMembers);
s.write('\n');
_writeToStringOverride(s, uniqueToDuplicates, duplicateToOriginal);
s.write('}\n\n');
}
return BindingString(type: BindingStringType.enum_, string: s.toString());
}
@override
String getCType(Context context) => nativeType.getCType(context);
@override
String getFfiDartType(Context context) => nativeType.getFfiDartType(context);
@override
String getDartType(Context context) {
if (style == EnumStyle.intConstants) {
return nativeType.getDartType(context);
} else if (isObjCImport) {
return '${context.libs.prefix(objcPkgImport)}.$name';
} else {
return name;
}
}
@override
String getNativeType({String varName = ''}) => '$originalName $varName';
@override
bool get sameFfiDartAndCType => nativeType.sameFfiDartAndCType;
@override
bool get sameDartAndFfiDartType => style == EnumStyle.intConstants;
@override
String? getDefaultValue(Context context) => '0';
@override
String convertDartTypeToFfiDartType(
Context context,
String value, {
required bool objCRetain,
required bool objCAutorelease,
}) => sameDartAndFfiDartType ? value : '$value.value';
@override
String convertFfiDartTypeToDartType(
Context context,
String value, {
required bool objCRetain,
String? objCEnclosingClass,
}) => sameDartAndFfiDartType
? value
: '${getDartType(context)}.fromValue($value)';
@override
void visitChildren(Visitor visitor) {
super.visitChildren(visitor);
visitor.visit(nativeType);
visitor.visitAll(enumConstants);
if (isObjCImport) visitor.visit(objcPkgImport);
}
@override
void visit(Visitation visitation) => visitation.visitEnumClass(this);
}
/// Represents a single value in an enum.
class EnumConstant extends AstNode {
final String? originalName;
final String? dartDoc;
final int value;
final Symbol _symbol;
String get name => _symbol.name;
EnumConstant({
String? originalName,
required String name,
required this.value,
this.dartDoc,
}) : originalName = originalName ?? name,
_symbol = Symbol(name, SymbolKind.field);
@override
void visitChildren(Visitor visitor) {
super.visitChildren(visitor);
visitor.visit(_symbol);
}
}