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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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;
}
120 changes: 113 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,14 @@
import 'package:code_builder/code_builder.dart';
import '../interop_gen/namer.dart';
import 'base.dart';
import 'builtin.dart';
import 'declarations.dart';

abstract interface class DeclarationAssociatedType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wait to add this type until we need it (and at that point decide on naming). It looks like only HomogenousUnionType uses it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, the type does not handle naming: naming is done externally when transforming

String get declarationName;

Declaration get declaration;
}

class ReferredType<T extends Declaration> extends Type {
@override
Expand All @@ -24,27 +32,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
class HomogenousUnionType<T extends LiteralType, D extends Declaration>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at declaration, if this is always going to be an Enum, can we call it HomogenousEnumType?

Copy link
Contributor Author

@nikeokoronkwo nikeokoronkwo Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe UnionEnumType? I feel maybe calling it HomogenousEnumType would confuse it as being directly related to an EnumDeclaration when it only is in reference. (I might be totally wrong on this however, but what do you think?)

extends UnionType implements DeclarationAssociatedType {
final List<T> _types;

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

final Type baseType;

final bool isNullable;

@override
String declarationName;

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

@override
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 +115,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