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 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
89 changes: 89 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,92 @@ class ParameterDeclaration {
..type = type.emit(TypeOptions(nullable: optional)));
}
}

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

@override
final bool exported;

Type baseType;

final List<EnumMember> members;

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

@override
String? dartName;

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

return ExtensionType((e) => e
..annotations.addAll([
if (dartName != null && dartName != name) 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((mem) => mem.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;

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 (value != null) {
f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$);
}
if (dartName != null && name != dartName) {
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;
}
106 changes: 102 additions & 4 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 {
String get declarationName;

Declaration get declaration;
}

class ReferredType<T extends Declaration> extends Type {
@override
Expand All @@ -24,19 +32,22 @@ 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
Reference emit([TypeOptions? options]) {
Expand All @@ -47,6 +58,53 @@ class UnionType extends Type {
String? get name => null;
}

class HomogenousUnionType<T extends LiteralType, D extends Declaration>
extends UnionType implements DeclarationAssociatedType {
final List<T> _types;

@override
List<T> get types => _types;

Type get baseType {
return types.first.baseType;
}

final bool isNullable;

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

// TODO: We need a better way of naming declarations
@override
String declarationName;

@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);
}
}

/// The base class for a type generic (like 'T')
class GenericType extends Type {
@override
Expand All @@ -68,3 +126,43 @@ class GenericType extends Type {
ID get id =>
ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');
}

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

final Object? value;

@override
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 = switch (kind) {
LiteralKind.$null => PrimitiveType.undefined,
LiteralKind.string => PrimitiveType.string,
LiteralKind.int => PrimitiveType.num,
LiteralKind.double => PrimitiveType.double,
LiteralKind.$true || LiteralKind.$false => PrimitiveType.boolean
};

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 }
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
6 changes: 5 additions & 1 deletion web_generator/lib/src/interop_gen/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class TransformResult {
final Type _ => null,
};
}).whereType<Spec>();
final lib = Library((l) => l..body.addAll(specs));
final lib = Library((l) => l
..ignoreForFile.addAll(
['constant_identifier_names', 'non_constant_identifier_names'])
..body.addAll(specs));
print('${lib.accept(emitter)}');
return MapEntry(file, formatter.format('${lib.accept(emitter)}'));
});
}
Expand Down
Loading