Skip to content

Commit f0f0914

Browse files
[interop] Add Support for Enums (#404)
* Added support for basic enums * working example * Implemented enum union types * resolved issues and added more tests * resolved remaining issues * final resolutions
1 parent fb8a149 commit f0f0914

File tree

10 files changed

+767
-81
lines changed

10 files changed

+767
-81
lines changed

web_generator/lib/src/ast/declarations.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,97 @@ class ParameterDeclaration {
138138
..type = type.emit(TypeOptions(nullable: optional)));
139139
}
140140
}
141+
142+
class EnumDeclaration extends NamedDeclaration
143+
implements ExportableDeclaration {
144+
@override
145+
final String name;
146+
147+
@override
148+
final bool exported;
149+
150+
/// The underlying type of the enum (usually a number)
151+
Type baseType;
152+
153+
final List<EnumMember> members;
154+
155+
@override
156+
String? dartName;
157+
158+
EnumDeclaration(
159+
{required this.name,
160+
required this.baseType,
161+
required this.members,
162+
required this.exported,
163+
this.dartName});
164+
165+
@override
166+
Spec emit([DeclarationOptions? options]) {
167+
final baseTypeIsJSType = getJSTypeAlternative(baseType) == baseType;
168+
final externalMember = members.any((m) => m.isExternal);
169+
final shouldUseJSRepType = externalMember || baseTypeIsJSType;
170+
171+
return ExtensionType((e) => e
172+
..annotations.addAll([
173+
if (dartName != null && dartName != name && externalMember)
174+
generateJSAnnotation(name)
175+
])
176+
..constant = !shouldUseJSRepType
177+
..name = dartName ?? name
178+
..primaryConstructorName = '_'
179+
..representationDeclaration = RepresentationDeclaration((r) => r
180+
..declaredRepresentationType = (
181+
// if any member doesn't have a value, we have to use external
182+
// so such type should be the JS rep type
183+
shouldUseJSRepType ? getJSTypeAlternative(baseType) : baseType)
184+
.emit(options?.toTypeOptions())
185+
..name = '_')
186+
..fields
187+
.addAll(members.map((member) => member.emit(shouldUseJSRepType))));
188+
}
189+
190+
@override
191+
ID get id => ID(type: 'enum', name: name);
192+
}
193+
194+
class EnumMember {
195+
final String name;
196+
197+
final Type? type;
198+
199+
final Object? value;
200+
201+
final String parent;
202+
203+
bool get isExternal => value == null;
204+
205+
EnumMember(this.name, this.value,
206+
{this.type, required this.parent, this.dartName});
207+
208+
Field emit([bool? shouldUseJSRepType]) {
209+
final jsRep = shouldUseJSRepType ?? (value == null);
210+
return Field((f) {
211+
// TODO(nikeokoronkwo): This does not render correctly on `code_builder`.
212+
// Until the update is made, we will omit examples concerning this
213+
// Luckily, not many real-world instances of enums use this anyways, https://github.com/dart-lang/tools/issues/2118
214+
if (!isExternal) {
215+
f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$);
216+
}
217+
if (dartName != null && name != dartName && isExternal) {
218+
f.annotations.add(generateJSAnnotation(name));
219+
}
220+
f
221+
..name = dartName ?? name
222+
..type = refer(parent)
223+
..external = value == null
224+
..static = true
225+
..assignment = value == null
226+
? null
227+
: refer(parent).property('_').call([
228+
jsRep ? literal(value).property('toJS') : literal(value)
229+
]).code;
230+
});
231+
}
232+
233+
String? dartName;
234+
}

web_generator/lib/src/ast/types.dart

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import 'package:code_builder/code_builder.dart';
66
import '../interop_gen/namer.dart';
77
import 'base.dart';
8+
import 'builtin.dart';
9+
import 'declarations.dart';
810

911
class ReferredType<T extends Declaration> extends Type {
1012
@override
@@ -24,27 +26,76 @@ class ReferredType<T extends Declaration> extends Type {
2426

2527
@override
2628
Reference emit([TypeOptions? options]) {
27-
// TODO: implement emit
28-
throw UnimplementedError();
29+
// TODO: Support referred types imported from URL
30+
return TypeReference((t) => t
31+
..symbol = declaration.name
32+
..types.addAll(typeParams.map((t) => t.emit(options)))
33+
..isNullable = options?.nullable);
2934
}
3035
}
3136

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

3641
UnionType({required this.types});
3742

3843
@override
39-
ID get id => ID(type: 'type', name: types.map((t) => t.id).join('|'));
44+
ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('|'));
45+
46+
@override
47+
String? get name => null;
4048

4149
@override
4250
Reference emit([TypeOptions? options]) {
4351
throw UnimplementedError('TODO: Implement UnionType.emit');
4452
}
53+
}
54+
55+
// TODO: Handle naming anonymous declarations
56+
// TODO: Extract having a declaration associated with a type to its own type
57+
// (e.g DeclarationAssociatedType)
58+
class HomogenousEnumType<T extends LiteralType, D extends Declaration>
59+
extends UnionType {
60+
final List<T> _types;
4561

4662
@override
47-
String? get name => null;
63+
List<T> get types => _types;
64+
65+
final Type baseType;
66+
67+
final bool isNullable;
68+
69+
String declarationName;
70+
71+
HomogenousEnumType(
72+
{required List<T> types, this.isNullable = false, required String name})
73+
: declarationName = name,
74+
_types = types,
75+
baseType = types.first.baseType,
76+
super(types: types);
77+
78+
EnumDeclaration get declaration => EnumDeclaration(
79+
name: declarationName,
80+
dartName: UniqueNamer.makeNonConflicting(declarationName),
81+
baseType: baseType,
82+
members: types.map((t) {
83+
final name = t.value.toString();
84+
return EnumMember(
85+
name,
86+
t.value,
87+
dartName: UniqueNamer.makeNonConflicting(name),
88+
parent: UniqueNamer.makeNonConflicting(declarationName),
89+
);
90+
}).toList(),
91+
exported: true);
92+
93+
@override
94+
Reference emit([TypeOptions? options]) {
95+
return TypeReference((t) => t
96+
..symbol = declarationName
97+
..isNullable = options?.nullable ?? isNullable);
98+
}
4899
}
49100

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

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

112+
@override
113+
ID get id =>
114+
ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');
115+
61116
@override
62117
Reference emit([TypeOptions? options]) => TypeReference((t) => t
63118
..symbol = name
64119
..bound = constraint?.emit()
65120
..isNullable = options?.nullable);
121+
}
122+
123+
/// A type representing a bare literal, such as `null`, a string or number
124+
class LiteralType extends Type {
125+
final LiteralKind kind;
126+
127+
final Object? value;
66128

67129
@override
68-
ID get id =>
69-
ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');
130+
String get name => switch (kind) {
131+
LiteralKind.$null => 'null',
132+
LiteralKind.int || LiteralKind.double => 'number',
133+
LiteralKind.string => 'string',
134+
LiteralKind.$true => 'true',
135+
LiteralKind.$false => 'false'
136+
};
137+
138+
BuiltinType get baseType {
139+
final primitive = kind.primitive;
140+
141+
return BuiltinType.primitiveType(primitive);
142+
}
143+
144+
LiteralType({required this.kind, required this.value});
145+
146+
@override
147+
Reference emit([TypeOptions? options]) {
148+
return baseType.emit(options);
149+
}
150+
151+
@override
152+
ID get id => ID(type: 'type', name: name);
153+
}
154+
155+
enum LiteralKind {
156+
$null,
157+
string,
158+
double,
159+
$true,
160+
$false,
161+
int;
162+
163+
PrimitiveType get primitive => switch (this) {
164+
LiteralKind.$null => PrimitiveType.undefined,
165+
LiteralKind.string => PrimitiveType.string,
166+
LiteralKind.int => PrimitiveType.num,
167+
LiteralKind.double => PrimitiveType.double,
168+
LiteralKind.$true || LiteralKind.$false => PrimitiveType.boolean
169+
};
70170
}

web_generator/lib/src/interop_gen/namer.dart

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ class UniqueNamer {
2323
UniqueNamer([Iterable<String> used = const <String>[]])
2424
: _usedNames = used.toSet();
2525

26+
/// Makes a name that does not conflict with dart keywords
27+
static String makeNonConflicting(String name) {
28+
if (int.tryParse(name) != null) {
29+
return '\$$name';
30+
} else if (double.tryParse(name) != null) {
31+
return '\$${name.splitMapJoin(
32+
'.',
33+
onMatch: (p0) => 'dot',
34+
)}';
35+
} else if (keywords.contains(name)) {
36+
return '$name\$';
37+
} else {
38+
return name;
39+
}
40+
}
41+
2642
/// Creates a unique name and ID for a given declaration to prevent
2743
/// name collisions in Dart applications
2844
///
@@ -33,10 +49,7 @@ class UniqueNamer {
3349
name = 'unnamed';
3450
}
3551

36-
var newName = name;
37-
if (keywords.contains(newName)) {
38-
newName = '$newName\$';
39-
}
52+
var newName = UniqueNamer.makeNonConflicting(name);
4053

4154
var i = 0;
4255
while (_usedNames.contains(newName)) {

web_generator/lib/src/interop_gen/transform.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ class TransformResult {
3232
final Type _ => null,
3333
};
3434
}).whereType<Spec>();
35-
final lib = Library((l) => l..body.addAll(specs));
36-
return MapEntry(file, formatter.format('${lib.accept(emitter)}'));
35+
final lib = Library((l) => l
36+
..ignoreForFile.addAll(
37+
['constant_identifier_names', 'non_constant_identifier_names'])
38+
..body.addAll(specs));
39+
return MapEntry(
40+
file,
41+
formatter.format('${lib.accept(emitter)}'
42+
.replaceAll('static external', 'external static')));
3743
});
3844
}
3945
}

0 commit comments

Comments
 (0)