Skip to content

[interop] Add Support for Enums #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions web_generator/lib/src/ast/declarations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,97 @@ class ParameterDeclaration {
..type = type.emit(TypeOptions(nullable: optional)));
}
}

class EnumDeclaration extends NamedDeclaration
implements ExportableDeclaration {
@override
final String name;

@override
final bool exported;

/// The underlying type of the enum (usually a number)
Type baseType;

final List<EnumMember> members;

@override
String? dartName;

EnumDeclaration(
{required this.name,
required this.baseType,
required this.members,
required this.exported,
this.dartName});

@override
Spec emit([DeclarationOptions? options]) {
final baseTypeIsJSType = getJSTypeAlternative(baseType) == baseType;
final externalMember = members.any((m) => m.isExternal);
final shouldUseJSRepType = externalMember || baseTypeIsJSType;

return ExtensionType((e) => e
..annotations.addAll([
if (dartName != null && dartName != name && externalMember)
generateJSAnnotation(name)
])
..constant = !shouldUseJSRepType
..name = dartName ?? name
..primaryConstructorName = '_'
..representationDeclaration = RepresentationDeclaration((r) => r
..declaredRepresentationType = (
// if any member doesn't have a value, we have to use external
// so such type should be the JS rep type
shouldUseJSRepType ? getJSTypeAlternative(baseType) : baseType)
.emit(options?.toTypeOptions())
..name = '_')
..fields
.addAll(members.map((member) => member.emit(shouldUseJSRepType))));
}

@override
ID get id => ID(type: 'enum', name: name);
}

class EnumMember {
final String name;

final Type? type;

final Object? value;

final String parent;

bool get isExternal => value == null;

EnumMember(this.name, this.value,
{this.type, required this.parent, this.dartName});

Field emit([bool? shouldUseJSRepType]) {
final jsRep = shouldUseJSRepType ?? (value == null);
return Field((f) {
// TODO(nikeokoronkwo): This does not render correctly on `code_builder`.
// Until the update is made, we will omit examples concerning this
// Luckily, not many real-world instances of enums use this anyways, https://github.com/dart-lang/tools/issues/2118
if (!isExternal) {
f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$);
}
if (dartName != null && name != dartName && isExternal) {
f.annotations.add(generateJSAnnotation(name));
}
f
..name = dartName ?? name
..type = refer(parent)
..external = value == null
..static = true
..assignment = value == null
? null
: refer(parent).property('_').call([
jsRep ? literal(value).property('toJS') : literal(value)
]).code;
});
}

String? dartName;
}
114 changes: 107 additions & 7 deletions web_generator/lib/src/ast/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import 'package:code_builder/code_builder.dart';
import '../interop_gen/namer.dart';
import 'base.dart';
import 'builtin.dart';
import 'declarations.dart';

class ReferredType<T extends Declaration> extends Type {
@override
Expand All @@ -24,27 +26,76 @@ class ReferredType<T extends Declaration> extends Type {

@override
Reference emit([TypeOptions? options]) {
// TODO: implement emit
throw UnimplementedError();
// TODO: Support referred types imported from URL
return TypeReference((t) => t
..symbol = declaration.name
..types.addAll(typeParams.map((t) => t.emit(options)))
..isNullable = options?.nullable);
}
}

// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`)
class UnionType extends Type {
List<Type> types;
final List<Type> types;

UnionType({required this.types});

@override
ID get id => ID(type: 'type', name: types.map((t) => t.id).join('|'));
ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('|'));

@override
String? get name => null;

@override
Reference emit([TypeOptions? options]) {
throw UnimplementedError('TODO: Implement UnionType.emit');
}
}

// TODO: Handle naming anonymous declarations
// TODO: Extract having a declaration associated with a type to its own type
// (e.g DeclarationAssociatedType)
class HomogenousEnumType<T extends LiteralType, D extends Declaration>
extends UnionType {
final List<T> _types;

@override
String? get name => null;
List<T> get types => _types;

final Type baseType;

final bool isNullable;

String declarationName;

HomogenousEnumType(
{required List<T> types, this.isNullable = false, required String name})
: declarationName = name,
_types = types,
baseType = types.first.baseType,
super(types: types);

EnumDeclaration get declaration => EnumDeclaration(
name: declarationName,
dartName: UniqueNamer.makeNonConflicting(declarationName),
baseType: baseType,
members: types.map((t) {
final name = t.value.toString();
return EnumMember(
name,
t.value,
dartName: UniqueNamer.makeNonConflicting(name),
parent: UniqueNamer.makeNonConflicting(declarationName),
);
}).toList(),
exported: true);

@override
Reference emit([TypeOptions? options]) {
return TypeReference((t) => t
..symbol = declarationName
..isNullable = options?.nullable ?? isNullable);
}
}

/// The base class for a type generic (like 'T')
Expand All @@ -58,13 +109,62 @@ class GenericType extends Type {

GenericType({required this.name, this.constraint, this.parent});

@override
ID get id =>
ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');

@override
Reference emit([TypeOptions? options]) => TypeReference((t) => t
..symbol = name
..bound = constraint?.emit()
..isNullable = options?.nullable);
}

/// A type representing a bare literal, such as `null`, a string or number
class LiteralType extends Type {
final LiteralKind kind;

final Object? value;

@override
ID get id =>
ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');
String get name => switch (kind) {
LiteralKind.$null => 'null',
LiteralKind.int || LiteralKind.double => 'number',
LiteralKind.string => 'string',
LiteralKind.$true => 'true',
LiteralKind.$false => 'false'
};

BuiltinType get baseType {
final primitive = kind.primitive;

return BuiltinType.primitiveType(primitive);
}

LiteralType({required this.kind, required this.value});

@override
Reference emit([TypeOptions? options]) {
return baseType.emit(options);
}

@override
ID get id => ID(type: 'type', name: name);
}

enum LiteralKind {
$null,
string,
double,
$true,
$false,
int;

PrimitiveType get primitive => switch (this) {
LiteralKind.$null => PrimitiveType.undefined,
LiteralKind.string => PrimitiveType.string,
LiteralKind.int => PrimitiveType.num,
LiteralKind.double => PrimitiveType.double,
LiteralKind.$true || LiteralKind.$false => PrimitiveType.boolean
};
}
21 changes: 17 additions & 4 deletions web_generator/lib/src/interop_gen/namer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ class UniqueNamer {
UniqueNamer([Iterable<String> used = const <String>[]])
: _usedNames = used.toSet();

/// Makes a name that does not conflict with dart keywords
static String makeNonConflicting(String name) {
if (int.tryParse(name) != null) {
return '\$$name';
} else if (double.tryParse(name) != null) {
return '\$${name.splitMapJoin(
'.',
onMatch: (p0) => 'dot',
)}';
} else if (keywords.contains(name)) {
return '$name\$';
} else {
return name;
}
}

/// Creates a unique name and ID for a given declaration to prevent
/// name collisions in Dart applications
///
Expand All @@ -33,10 +49,7 @@ class UniqueNamer {
name = 'unnamed';
}

var newName = name;
if (keywords.contains(newName)) {
newName = '$newName\$';
}
var newName = UniqueNamer.makeNonConflicting(name);

var i = 0;
while (_usedNames.contains(newName)) {
Expand Down
10 changes: 8 additions & 2 deletions web_generator/lib/src/interop_gen/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ class TransformResult {
final Type _ => null,
};
}).whereType<Spec>();
final lib = Library((l) => l..body.addAll(specs));
return MapEntry(file, formatter.format('${lib.accept(emitter)}'));
final lib = Library((l) => l
..ignoreForFile.addAll(
['constant_identifier_names', 'non_constant_identifier_names'])
..body.addAll(specs));
return MapEntry(
file,
formatter.format('${lib.accept(emitter)}'
.replaceAll('static external', 'external static')));
});
}
}
Expand Down
Loading