From f874117740ab54fcdf89e56bbd5d95e3f286e9a5 Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Tue, 1 Jul 2025 14:57:04 -0400 Subject: [PATCH 1/6] Added support for basic enums --- web_generator/lib/src/ast/declarations.dart | 76 ++++++++++++++++ web_generator/lib/src/ast/types.dart | 7 +- .../lib/src/interop_gen/transform.dart | 5 +- .../interop_gen/transform/transformer.dart | 85 ++++++++++++++++- .../lib/src/js/typescript.types.dart | 42 +++++++++ .../interop_gen/enum_expected.dart | 91 +++++++++++++++++++ .../integration/interop_gen/enum_input.d.ts | 66 ++++++++++++++ .../interop_gen/functions_expected.dart | 2 + .../interop_gen/variables_expected.dart | 2 + 9 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 web_generator/test/integration/interop_gen/enum_expected.dart create mode 100644 web_generator/test/integration/interop_gen/enum_input.d.ts diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 1b02774b..a626222a 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -138,3 +138,79 @@ 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 members; + + EnumDeclaration( + {required this.name, + required this.baseType, + required this.members, + required this.exported}); + + @override + String? get dartName => null; + + @override + Spec emit([DeclarationOptions? options]) { + final baseTypeIsJSType = getJSTypeAlternative(baseType) == baseType; + final shouldUseJSRepType = + members.any((m) => m.value == null) || baseTypeIsJSType; + + return ExtensionType((e) => e + ..constant = !shouldUseJSRepType + ..name = 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}); + + Field emit([bool? shouldUseJSRepType]) { + final jsRep = shouldUseJSRepType ?? (value == null); + return Field((f) { + if (value != null) { + f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$); + } + f + ..name = 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; + }); + } +} diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index e3d789b6..7b592038 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -24,8 +24,11 @@ class ReferredType 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); } } diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 663786b6..e2c3dcfe 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -32,7 +32,10 @@ class TransformResult { final Type _ => null, }; }).whereType(); - 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)); return MapEntry(file, formatter.format('${lib.accept(emitter)}')); }); } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 404c2992..1d53bdea 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -44,9 +44,11 @@ class Transformer { final decs = _transformVariable(node as TSVariableStatement); nodeMap.addAll({for (final d in decs) d.id.toString(): d}); default: - final Declaration decl = switch (node.kind) { + final decl = switch (node.kind) { TSSyntaxKind.FunctionDeclaration => _transformFunction(node as TSFunctionDeclaration), + TSSyntaxKind.EnumDeclaration => + _transformEnum(node as TSEnumDeclaration), _ => throw Exception('Unsupported Declaration Kind: ${node.kind}') }; // ignore: dead_code This line will not be dead in future decl additions @@ -56,7 +58,82 @@ class Transformer { nodes.add(node); } - List _transformVariable(TSVariableStatement variable) { + EnumDeclaration _transformEnum(TSEnumDeclaration enumeration) { + final modifiers = enumeration.modifiers?.toDart; + final isExported = modifiers?.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }) ?? + false; + + // get the name + final name = enumeration.name.text; + + // get the members and the rep type + final enumMembers = enumeration.members.toDart; + + final members = []; + PrimitiveType? enumRepType; + + for (final mem in enumMembers) { + final memName = mem.name.text; + final memInitializer = mem.initializer; + + // check the type of the initializer + if (memInitializer != null) { + switch (memInitializer.kind) { + case TSSyntaxKind.NumericLiteral: + // parse numeric literal + final value = + _parseNumericLiteral(memInitializer as TSNumericLiteral); + const primitiveType = PrimitiveType.num; + members.add(EnumMember(memName, value, + type: BuiltinType.primitiveType(primitiveType), parent: name)); + if (enumRepType == null) { + enumRepType = primitiveType; + } else if (enumRepType != primitiveType) { + enumRepType = PrimitiveType.any; + } + break; + case TSSyntaxKind.StringLiteral: + // parse string literal + final value = + _parseStringLiteral(memInitializer as TSStringLiteral); + const primitiveType = PrimitiveType.string; + members.add(EnumMember(memName, value, + type: BuiltinType.primitiveType(primitiveType), parent: name)); + if (enumRepType == null) { + enumRepType = primitiveType; + } else if (enumRepType != primitiveType) { + enumRepType = PrimitiveType.any; + } + break; + default: + // unsupported + + break; + } + } else { + // get the type + members.add(EnumMember(memName, null, parent: name)); + } + } + + return EnumDeclaration( + name: name, + baseType: BuiltinType.primitiveType(enumRepType ?? PrimitiveType.num), + members: members, + exported: isExported); + } + + num _parseNumericLiteral(TSNumericLiteral numericLiteral) { + return num.parse(numericLiteral.text); + } + + String _parseStringLiteral(TSStringLiteral stringLiteral) { + return stringLiteral.text; + } + + List _transformVariable(TSVariableStatement variable) { // get the modifier of the declaration final modifiers = variable.modifiers.toDart; final isExported = modifiers.any((m) { @@ -258,6 +335,8 @@ class Transformer { } }); + if (filteredDeclarations.isEmpty) return filteredDeclarations; + // then filter for dependencies final otherDecls = filteredDeclarations.entries .map((e) => _getDependenciesOfDecl(e.value)) @@ -290,6 +369,8 @@ class Transformer { node.id.toString(): node }); break; + case final EnumDeclaration _: + break; case final UnionType u: filteredDeclarations.addAll({ for (final t in u.types.where((t) => t is! BuiltinType)) diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index ed8855df..ab3c7bed 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -25,6 +25,11 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262); static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278); static const TSSyntaxKind Parameter = TSSyntaxKind._(169); + static const TSSyntaxKind EnumDeclaration = TSSyntaxKind._(266); + + /// expressions + static const TSSyntaxKind NumericLiteral = TSSyntaxKind._(9); + static const TSSyntaxKind StringLiteral = TSSyntaxKind._(11); /// keywords static const TSSyntaxKind ExportKeyword = TSSyntaxKind._(95); @@ -98,9 +103,32 @@ extension type TSTypeReferenceNode._(JSObject _) implements TSTypeNode { external TSNodeArray? get typeArguments; } +@JS('Expression') +extension type TSExpression._(JSObject _) implements TSNode {} + +@JS('LiteralExpression') +extension type TSLiteralExpression._(JSObject _) implements TSExpression { + external String text; + external bool? isUnterminated; +} + @JS('Declaration') extension type TSDeclaration._(JSObject _) implements TSNode {} +@JS('NumericLiteral') +extension type TSNumericLiteral._(JSObject _) + implements TSLiteralExpression, TSDeclaration { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.NumericLiteral; +} + +@JS('StringLiteral') +extension type TSStringLiteral._(JSObject _) + implements TSLiteralExpression, TSDeclaration { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.StringLiteral; +} + @JS('Statement') extension type TSStatement._(JSObject _) implements TSNode {} @@ -152,6 +180,20 @@ extension type TSTypeParameterDeclaration._(JSObject _) external TSTypeNode? get constraint; } +@JS('EnumDeclaration') +extension type TSEnumDeclaration._(JSObject _) + implements TSDeclaration, TSStatement { + external TSIdentifier get name; + external TSNodeArray? get modifiers; + external TSNodeArray get members; +} + +@JS('EnumMember') +extension type TSEnumMember._(JSObject _) implements TSDeclaration { + external TSIdentifier get name; + external TSExpression? get initializer; +} + @JS('NodeArray') extension type TSNodeArray._(JSArray _) implements JSArray {} diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart new file mode 100644 index 00000000..aee8b828 --- /dev/null +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -0,0 +1,91 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +extension type const Direction._(num _) { + static const Direction Up = Direction._(0); + + static const Direction Down = Direction._(1); + + static const Direction Left = Direction._(2); + + static const Direction Right = Direction._(3); +} +extension type const ResponseCode._(num _) { + static const ResponseCode Success = ResponseCode._(200); + + static const ResponseCode NotFound = ResponseCode._(404); + + static const ResponseCode ServerError = ResponseCode._(500); +} +extension type const LogLevel._(String _) { + static const LogLevel Info = LogLevel._('INFO'); + + static const LogLevel Warn = LogLevel._('WARN'); + + static const LogLevel Error = LogLevel._('ERROR'); + + static const LogLevel Debug = LogLevel._('DEBUG'); +} +extension type const HttpMethod._(String _) { + static const HttpMethod GET = HttpMethod._('GET'); + + static const HttpMethod POST = HttpMethod._('POST'); + + static const HttpMethod DELETE = HttpMethod._('DELETE'); +} +extension type BooleanLike._(_i1.JSAny? _) { + static final BooleanLike No = BooleanLike._(0.toJS); + + static final BooleanLike Yes = BooleanLike._('YES'.toJS); +} +extension type MathConstants._(_i1.JSNumber _) { + static final MathConstants PI = MathConstants._(3.14.toJS); + + static final MathConstants TwoPI = MathConstants._(6.28.toJS); + + external static MathConstants Random; + + external static MathConstants Length; +} +extension type const Status._(num _) { + static const Status Active = Status._(1); + + static const Status Inactive = Status._(0); + + static const Status Pending = Status._(2); +} +@_i1.JS() +external Status get statusFromName; +@_i1.JS() +external void logStatus(Status status); +@_i1.JS() +external String handleDirection(Direction dir); +extension type const HttpStatus._(num _) { + static const HttpStatus OK = HttpStatus._(200); + + static const HttpStatus BadRequest = HttpStatus._(400); + + static const HttpStatus Unauthorized = HttpStatus._(401); + + static const HttpStatus Forbidden = HttpStatus._(403); +} +@_i1.JS() +external HttpStatus get statusCode; +extension type const Permissions._(num _) { + static const Permissions Read = Permissions._(1); + + static const Permissions Write = Permissions._(2); + + static const Permissions Execute = Permissions._(4); + + static const Permissions All = Permissions._(7); +} +@_i1.JS() +external bool hasPermission( + Permissions perm, + Permissions flag, +); +@_i1.JS() +external Permissions get userPermissions; diff --git a/web_generator/test/integration/interop_gen/enum_input.d.ts b/web_generator/test/integration/interop_gen/enum_input.d.ts new file mode 100644 index 00000000..b54c25cc --- /dev/null +++ b/web_generator/test/integration/interop_gen/enum_input.d.ts @@ -0,0 +1,66 @@ +export declare enum Direction { + Up = 0, + Down = 1, + Left = 2, + Right = 3 +} +export declare enum ResponseCode { + Success = 200, + NotFound = 404, + ServerError = 500 +} +export declare enum LogLevel { + Info = "INFO", + Warn = "WARN", + Error = "ERROR", + Debug = "DEBUG" +} +export declare enum HttpMethod { + GET = "GET", + POST = "POST", + DELETE = "DELETE" +} +export declare enum BooleanLike { + No = 0, + Yes = "YES" +} +export declare enum MathConstants { + PI = 3.14,// constant + TwoPI = 6.28,// computed + Random,// computed at compile time + Length +} +export declare enum Status { + Active = 1, + Inactive = 0, + Pending = 2 +} +declare const nameOfStatus: string; +export declare const statusFromName: Status; +export declare function logStatus(status: Status): void; +export declare function handleDirection(dir: Direction): string; +export declare const enum HttpStatus { + OK = 200, + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403 +} +export declare const statusCode: HttpStatus; +declare enum ExternalLibResult { + OK = 0, + FAIL = 1 +} +declare enum DuplicateValues { + A = 1, + B = 2, + C = 1 +} +declare const statusKeys: string[]; +export declare enum Permissions { + Read = 1,// 0001 + Write = 2,// 0010 + Execute = 4,// 0100 + All = 7 +} +export declare function hasPermission(perm: Permissions, flag: Permissions): boolean; +export declare const userPermissions: Permissions; diff --git a/web_generator/test/integration/interop_gen/functions_expected.dart b/web_generator/test/integration/interop_gen/functions_expected.dart index 4c8c2f8e..ce36fa8b 100644 --- a/web_generator/test/integration/interop_gen/functions_expected.dart +++ b/web_generator/test/integration/interop_gen/functions_expected.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; diff --git a/web_generator/test/integration/interop_gen/variables_expected.dart b/web_generator/test/integration/interop_gen/variables_expected.dart index 1df172a0..4bf7c0f6 100644 --- a/web_generator/test/integration/interop_gen/variables_expected.dart +++ b/web_generator/test/integration/interop_gen/variables_expected.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; From 93761aab5b182b06c4ecfb71661ed431ac55800e Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Tue, 1 Jul 2025 17:27:37 -0400 Subject: [PATCH 2/6] working example --- web_generator/lib/src/ast/declarations.dart | 3 +++ .../lib/src/interop_gen/transform/transformer.dart | 10 ++++------ .../integration/interop_gen/enum_expected.dart | 14 +------------- .../test/integration/interop_gen/enum_input.d.ts | 6 ------ 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index a626222a..a9e0460b 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -198,6 +198,9 @@ class EnumMember { 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$); } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 1d53bdea..eff682bf 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -323,15 +323,11 @@ class Transformer { filteredDeclarations.add(e); } break; - case final BuiltinType _: - // primitive types are generated by default - break; - case Type(): - // TODO: Handle this case. - throw UnimplementedError(); case Declaration(): // TODO: Handle this case. throw UnimplementedError(); + default: + break; } }); @@ -380,6 +376,8 @@ class Transformer { case final BuiltinType _: // primitive types are generated by default break; + case final ReferredType r: + filteredDeclarations.add(r.declaration); default: print('WARN: The given node type ${decl.runtimeType.toString()} ' 'is not supported for filtering. Skipping...'); diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart index aee8b828..e4a31ff6 100644 --- a/web_generator/test/integration/interop_gen/enum_expected.dart +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -40,15 +40,6 @@ extension type BooleanLike._(_i1.JSAny? _) { static final BooleanLike Yes = BooleanLike._('YES'.toJS); } -extension type MathConstants._(_i1.JSNumber _) { - static final MathConstants PI = MathConstants._(3.14.toJS); - - static final MathConstants TwoPI = MathConstants._(6.28.toJS); - - external static MathConstants Random; - - external static MathConstants Length; -} extension type const Status._(num _) { static const Status Active = Status._(1); @@ -83,9 +74,6 @@ extension type const Permissions._(num _) { static const Permissions All = Permissions._(7); } @_i1.JS() -external bool hasPermission( - Permissions perm, - Permissions flag, -); +external bool hasPermission(Permissions perm, Permissions flag); @_i1.JS() external Permissions get userPermissions; diff --git a/web_generator/test/integration/interop_gen/enum_input.d.ts b/web_generator/test/integration/interop_gen/enum_input.d.ts index b54c25cc..d8ab309f 100644 --- a/web_generator/test/integration/interop_gen/enum_input.d.ts +++ b/web_generator/test/integration/interop_gen/enum_input.d.ts @@ -24,12 +24,6 @@ export declare enum BooleanLike { No = 0, Yes = "YES" } -export declare enum MathConstants { - PI = 3.14,// constant - TwoPI = 6.28,// computed - Random,// computed at compile time - Length -} export declare enum Status { Active = 1, Inactive = 0, From 87918b836f461731df4430ed0862b5bb1e0a2588 Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Wed, 2 Jul 2025 00:09:17 -0400 Subject: [PATCH 3/6] Implemented enum union types --- web_generator/lib/src/ast/declarations.dart | 20 +- web_generator/lib/src/ast/types.dart | 99 ++++++++- web_generator/lib/src/interop_gen/namer.dart | 21 +- .../lib/src/interop_gen/transform.dart | 1 + .../interop_gen/transform/transformer.dart | 197 ++++++++++++------ .../lib/src/js/typescript.types.dart | 22 +- .../interop_gen/enum_expected.dart | 36 ++++ .../integration/interop_gen/enum_input.d.ts | 3 + 8 files changed, 320 insertions(+), 79 deletions(-) diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index a9e0460b..15dbc656 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -155,10 +155,11 @@ class EnumDeclaration extends NamedDeclaration {required this.name, required this.baseType, required this.members, - required this.exported}); + required this.exported, + this.dartName}); @override - String? get dartName => null; + String? dartName; @override Spec emit([DeclarationOptions? options]) { @@ -167,8 +168,11 @@ class EnumDeclaration extends NamedDeclaration members.any((m) => m.value == null) || baseTypeIsJSType; return ExtensionType((e) => e + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name) + ]) ..constant = !shouldUseJSRepType - ..name = name + ..name = dartName ?? name ..primaryConstructorName = '_' ..representationDeclaration = RepresentationDeclaration((r) => r ..declaredRepresentationType = ( @@ -193,7 +197,8 @@ class EnumMember { final String parent; - EnumMember(this.name, this.value, {this.type, required this.parent}); + EnumMember(this.name, this.value, + {this.type, required this.parent, this.dartName}); Field emit([bool? shouldUseJSRepType]) { final jsRep = shouldUseJSRepType ?? (value == null); @@ -204,8 +209,11 @@ class EnumMember { if (value != null) { f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$); } + if (dartName != null && name != dartName) { + f.annotations.add(generateJSAnnotation(name)); + } f - ..name = name + ..name = dartName ?? name ..type = refer(parent) ..external = value == null ..static = true @@ -216,4 +224,6 @@ class EnumMember { ]).code; }); } + + String? dartName; } diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 7b592038..5c2ceefa 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -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 extends Type { @override @@ -34,12 +42,12 @@ class ReferredType extends Type { // TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`) class UnionType extends Type { - List types; + final List 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]) { @@ -50,6 +58,53 @@ class UnionType extends Type { String? get name => null; } +class HomogenousUnionType + extends UnionType implements DeclarationAssociatedType { + final List _types; + + @override + List get types => _types; + + Type get baseType { + return types.first.baseType; + } + + final bool isNullable; + + HomogenousUnionType( + {required List 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 @@ -71,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 } diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart index dd07115d..085fb820 100644 --- a/web_generator/lib/src/interop_gen/namer.dart +++ b/web_generator/lib/src/interop_gen/namer.dart @@ -23,6 +23,22 @@ class UniqueNamer { UniqueNamer([Iterable used = const []]) : _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 /// @@ -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)) { diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index e2c3dcfe..2e879da8 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -36,6 +36,7 @@ class TransformResult { ..ignoreForFile.addAll( ['constant_identifier_names', 'non_constant_identifier_names']) ..body.addAll(specs)); + print('${lib.accept(emitter)}'); return MapEntry(file, formatter.format('${lib.accept(emitter)}')); }); } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index eff682bf..bf95d555 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -76,6 +76,7 @@ class Transformer { for (final mem in enumMembers) { final memName = mem.name.text; + final dartMemName = UniqueNamer.makeNonConflicting(memName); final memInitializer = mem.initializer; // check the type of the initializer @@ -87,7 +88,9 @@ class Transformer { _parseNumericLiteral(memInitializer as TSNumericLiteral); const primitiveType = PrimitiveType.num; members.add(EnumMember(memName, value, - type: BuiltinType.primitiveType(primitiveType), parent: name)); + type: BuiltinType.primitiveType(primitiveType), + parent: name, + dartName: dartMemName)); if (enumRepType == null) { enumRepType = primitiveType; } else if (enumRepType != primitiveType) { @@ -100,7 +103,9 @@ class Transformer { _parseStringLiteral(memInitializer as TSStringLiteral); const primitiveType = PrimitiveType.string; members.add(EnumMember(memName, value, - type: BuiltinType.primitiveType(primitiveType), parent: name)); + type: BuiltinType.primitiveType(primitiveType), + parent: name, + dartName: dartMemName)); if (enumRepType == null) { enumRepType = primitiveType; } else if (enumRepType != primitiveType) { @@ -114,7 +119,8 @@ class Transformer { } } else { // get the type - members.add(EnumMember(memName, null, parent: name)); + members.add( + EnumMember(memName, null, parent: name, dartName: dartMemName)); } } @@ -231,80 +237,140 @@ class Transformer { /// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`) /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types Type _transformType(TSTypeNode type, {bool parameter = false}) { - if (type.kind == TSSyntaxKind.UnionType) { - final unionType = type as TSUnionTypeNode; - return UnionType( - types: unionType.types.toDart.map(_transformType).toList()); - } - - if (type.kind == TSSyntaxKind.TypeReference) { - final refType = type as TSTypeReferenceNode; + switch (type.kind) { + case TSSyntaxKind.TypeReference: + final refType = type as TSTypeReferenceNode; + + final name = refType.typeName.text; + final typeArguments = refType.typeArguments?.toDart; + + var declarationsMatching = nodeMap.findByName(name); + + if (declarationsMatching.isEmpty) { + // check if builtin + // TODO(https://github.com/dart-lang/web/issues/380): A better name + // for this, and adding support for "supported declarations" + // (also a better name for that) + final supportedType = getSupportedType( + name, (typeArguments ?? []).map(_transformType).toList()); + if (supportedType != null) { + return supportedType; + } - final name = refType.typeName.text; - final typeArguments = refType.typeArguments?.toDart; + // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? + final declaration = _getDeclarationByName(refType.typeName); - var declarationsMatching = nodeMap.findByName(name); + if (declaration == null) { + throw Exception('Found no declaration matching $name'); + } - if (declarationsMatching.isEmpty) { - // check if builtin - // TODO(https://github.com/dart-lang/web/issues/380): A better name - // for this, and adding support for "supported declarations" - // (also a better name for that) - final supportedType = getSupportedType( - name, (typeArguments ?? []).map(_transformType).toList()); - if (supportedType != null) { - return supportedType; - } + if (declaration.kind == TSSyntaxKind.TypeParameter) { + return GenericType(name: name); + } - // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? - final declaration = _getDeclarationByName(refType.typeName); + transform(declaration); - if (declaration == null) { - throw Exception('Found no declaration matching $name'); + declarationsMatching = nodeMap.findByName(name); } - if (declaration.kind == TSSyntaxKind.TypeParameter) { - return GenericType(name: name); - } + // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? + final firstNode = + declarationsMatching.whereType().first; + + return firstNode.asReferredType( + (typeArguments ?? []).map(_transformType).toList(), + ); + // TODO: Union types are also anonymous by design + // Unless we are making typedefs for them, we should + // try to handle not making multiple of them for a given use-case + case TSSyntaxKind.UnionType: + final unionType = type as TSUnionTypeNode; + // TODO: Unions + final types = unionType.types.toDart.map(_transformType).toList(); + + // check if it is a union of literals + if (types.every((t) => t is LiteralType) && + types.every((t) => + (t as LiteralType).kind == (types.first as LiteralType).kind || + t.kind == LiteralKind.$null)) { + // get the literal types other than null + final literalTypes = types.whereType(); + final nonNullLiteralTypes = + literalTypes.where((t) => t.kind != LiteralKind.$null).toList(); + + final isNullable = nonNullLiteralTypes.length == literalTypes.length; + + if (nonNullLiteralTypes.map((t) => t.kind) + case [ + LiteralKind.$true || LiteralKind.$false, + LiteralKind.$true || LiteralKind.$false + ]) { + return BuiltinType.primitiveType(PrimitiveType.boolean, + isNullable: isNullable); + } - transform(declaration); + final (id: _, name: name) = + namer.makeUnique('AnonymousUnion', 'type'); - declarationsMatching = nodeMap.findByName(name); - } + // TODO: Handle similar types here... + final type = HomogenousUnionType( + types: nonNullLiteralTypes, isNullable: isNullable, name: name); - // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? - final firstNode = - declarationsMatching.whereType().first; + return type; + } - return firstNode.asReferredType( - (typeArguments ?? []).map(_transformType).toList(), - ); - } + return UnionType(types: types); + case TSSyntaxKind.LiteralType: + final literalType = type as TSLiteralTypeNode; + final literal = literalType.literal; + + return LiteralType( + kind: switch (literal.kind) { + // TODO: Will we support Regex? + TSSyntaxKind.NumericLiteral => num.parse(literal.text) is int + ? LiteralKind.int + : LiteralKind.double, + TSSyntaxKind.StringLiteral => LiteralKind.string, + TSSyntaxKind.TrueKeyword => LiteralKind.$true, + TSSyntaxKind.FalseKeyword => LiteralKind.$false, + TSSyntaxKind.NullKeyword => LiteralKind.$null, + _ => throw UnimplementedError( + 'Unsupported Literal Kind ${literal.kind}') + }, + value: switch (literal.kind) { + // TODO: Will we support Regex? + TSSyntaxKind.NumericLiteral => num.parse(literal.text), + TSSyntaxKind.StringLiteral => literal.text, + TSSyntaxKind.TrueKeyword => true, + TSSyntaxKind.FalseKeyword => false, + TSSyntaxKind.NullKeyword => null, + _ => throw UnimplementedError( + 'Unsupported Literal Kind ${literal.kind}') + }); + case TSSyntaxKind.ArrayType: + return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [ + getJSTypeAlternative( + _transformType((type as TSArrayTypeNode).elementType)) + ]); + default: + // check for primitive type via its kind + final primitiveType = switch (type.kind) { + TSSyntaxKind.ArrayType => PrimitiveType.array, + TSSyntaxKind.StringKeyword => PrimitiveType.string, + TSSyntaxKind.AnyKeyword => PrimitiveType.any, + TSSyntaxKind.ObjectKeyword => PrimitiveType.object, + TSSyntaxKind.NumberKeyword => + (parameter ? PrimitiveType.num : PrimitiveType.double), + TSSyntaxKind.UndefinedKeyword => PrimitiveType.undefined, + TSSyntaxKind.UnknownKeyword => PrimitiveType.unknown, + TSSyntaxKind.BooleanKeyword => PrimitiveType.boolean, + TSSyntaxKind.VoidKeyword => PrimitiveType.$void, + _ => throw UnsupportedError( + 'The given type with kind ${type.kind} is not supported yet') + }; - if (type.kind == TSSyntaxKind.ArrayType) { - return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [ - getJSTypeAlternative( - _transformType((type as TSArrayTypeNode).elementType)) - ]); + return BuiltinType.primitiveType(primitiveType); } - - // check for primitive type via its kind - final primitiveType = switch (type.kind) { - TSSyntaxKind.ArrayType => PrimitiveType.array, - TSSyntaxKind.StringKeyword => PrimitiveType.string, - TSSyntaxKind.AnyKeyword => PrimitiveType.any, - TSSyntaxKind.ObjectKeyword => PrimitiveType.object, - TSSyntaxKind.NumberKeyword => - (parameter ? PrimitiveType.num : PrimitiveType.double), - TSSyntaxKind.UndefinedKeyword => PrimitiveType.undefined, - TSSyntaxKind.UnknownKeyword => PrimitiveType.unknown, - TSSyntaxKind.BooleanKeyword => PrimitiveType.boolean, - TSSyntaxKind.VoidKeyword => PrimitiveType.$void, - _ => throw UnsupportedError( - 'The given type with kind ${type.kind} is not supported yet') - }; - - return BuiltinType.primitiveType(primitiveType); } NodeMap filter() { @@ -367,6 +433,9 @@ class Transformer { break; case final EnumDeclaration _: break; + case final HomogenousUnionType hu: + filteredDeclarations.add(hu.declaration); + break; case final UnionType u: filteredDeclarations.addAll({ for (final t in u.types.where((t) => t is! BuiltinType)) diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index ab3c7bed..25bda5ce 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -30,6 +30,9 @@ extension type const TSSyntaxKind._(num _) { /// expressions static const TSSyntaxKind NumericLiteral = TSSyntaxKind._(9); static const TSSyntaxKind StringLiteral = TSSyntaxKind._(11); + static const TSSyntaxKind NullKeyword = TSSyntaxKind._(106); + static const TSSyntaxKind TrueKeyword = TSSyntaxKind._(112); + static const TSSyntaxKind FalseKeyword = TSSyntaxKind._(97); /// keywords static const TSSyntaxKind ExportKeyword = TSSyntaxKind._(95); @@ -52,6 +55,7 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind UnionType = TSSyntaxKind._(192); static const TSSyntaxKind TypeReference = TSSyntaxKind._(183); static const TSSyntaxKind ArrayType = TSSyntaxKind._(188); + static const TSSyntaxKind LiteralType = TSSyntaxKind._(201); /// Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); @@ -103,6 +107,14 @@ extension type TSTypeReferenceNode._(JSObject _) implements TSTypeNode { external TSNodeArray? get typeArguments; } +@JS('LiteralTypeNode') +extension type TSLiteralTypeNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.LiteralType; + + external TSLiteral get literal; +} + @JS('Expression') extension type TSExpression._(JSObject _) implements TSNode {} @@ -115,16 +127,18 @@ extension type TSLiteralExpression._(JSObject _) implements TSExpression { @JS('Declaration') extension type TSDeclaration._(JSObject _) implements TSNode {} +@JS() +extension type TSLiteral._(JSObject _) + implements TSLiteralExpression, TSDeclaration {} + @JS('NumericLiteral') -extension type TSNumericLiteral._(JSObject _) - implements TSLiteralExpression, TSDeclaration { +extension type TSNumericLiteral._(JSObject _) implements TSLiteral { @redeclare TSSyntaxKind get kind => TSSyntaxKind.NumericLiteral; } @JS('StringLiteral') -extension type TSStringLiteral._(JSObject _) - implements TSLiteralExpression, TSDeclaration { +extension type TSStringLiteral._(JSObject _) implements TSLiteral { @redeclare TSSyntaxKind get kind => TSSyntaxKind.StringLiteral; } diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart index e4a31ff6..c5fb7112 100644 --- a/web_generator/test/integration/interop_gen/enum_expected.dart +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -77,3 +77,39 @@ extension type const Permissions._(num _) { external bool hasPermission(Permissions perm, Permissions flag); @_i1.JS() external Permissions get userPermissions; +@_i1.JS() +external AnonymousUnion currentTheme; +@_i1.JS() +external AnonymousUnion$1 buttonState; +@_i1.JS() +external AnonymousUnion$2 retriesLeft; +extension type const AnonymousUnion._(String _) { + static const AnonymousUnion light = AnonymousUnion._('light'); + + static const AnonymousUnion dark = AnonymousUnion._('dark'); + + static const AnonymousUnion system = AnonymousUnion._('system'); +} +extension type const AnonymousUnion$1._(String _) { + @_i1.JS('default') + static const AnonymousUnion$1 default$ = AnonymousUnion$1._('default'); + + static const AnonymousUnion$1 hovered = AnonymousUnion$1._('hovered'); + + static const AnonymousUnion$1 pressed = AnonymousUnion$1._('pressed'); + + static const AnonymousUnion$1 disabled = AnonymousUnion$1._('disabled'); +} +extension type const AnonymousUnion$2._(num _) { + @_i1.JS('0') + static const AnonymousUnion$2 $0 = AnonymousUnion$2._(0); + + @_i1.JS('1') + static const AnonymousUnion$2 $1 = AnonymousUnion$2._(1); + + @_i1.JS('2') + static const AnonymousUnion$2 $2 = AnonymousUnion$2._(2); + + @_i1.JS('3') + static const AnonymousUnion$2 $3 = AnonymousUnion$2._(3); +} diff --git a/web_generator/test/integration/interop_gen/enum_input.d.ts b/web_generator/test/integration/interop_gen/enum_input.d.ts index d8ab309f..2449496f 100644 --- a/web_generator/test/integration/interop_gen/enum_input.d.ts +++ b/web_generator/test/integration/interop_gen/enum_input.d.ts @@ -58,3 +58,6 @@ export declare enum Permissions { } export declare function hasPermission(perm: Permissions, flag: Permissions): boolean; export declare const userPermissions: Permissions; +export declare let currentTheme: "light" | "dark" | "system"; +export declare let buttonState: "default" | "hovered" | "pressed" | "disabled"; +export declare let retriesLeft: 0 | 1 | 2 | 3; From fe38de6a480361326dd89d050e5f97bc423ad86b Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Thu, 3 Jul 2025 21:28:30 -0400 Subject: [PATCH 4/6] resolved issues and added more tests --- web_generator/lib/src/ast/declarations.dart | 23 +++--- web_generator/lib/src/ast/types.dart | 54 ++++++++------ .../lib/src/interop_gen/transform.dart | 6 +- .../interop_gen/transform/transformer.dart | 55 +++++++++----- .../interop_gen/enum_expected.dart | 71 ++++++++++++++++--- .../integration/interop_gen/enum_input.d.ts | 22 ++++++ 6 files changed, 169 insertions(+), 62 deletions(-) diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 15dbc656..1c6bc012 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -147,10 +147,14 @@ class EnumDeclaration extends NamedDeclaration @override final bool exported; + /// The underlying type of the enum (usually a number) Type baseType; final List members; + @override + String? dartName; + EnumDeclaration( {required this.name, required this.baseType, @@ -158,18 +162,16 @@ class EnumDeclaration extends NamedDeclaration 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; + final externalMember = members.any((m) => m.isExternal); + final shouldUseJSRepType = externalMember || baseTypeIsJSType; return ExtensionType((e) => e ..annotations.addAll([ - if (dartName != null && dartName != name) generateJSAnnotation(name) + if (dartName != null && dartName != name && externalMember) + generateJSAnnotation(name) ]) ..constant = !shouldUseJSRepType ..name = dartName ?? name @@ -181,7 +183,8 @@ class EnumDeclaration extends NamedDeclaration shouldUseJSRepType ? getJSTypeAlternative(baseType) : baseType) .emit(options?.toTypeOptions()) ..name = '_') - ..fields.addAll(members.map((mem) => mem.emit(shouldUseJSRepType)))); + ..fields + .addAll(members.map((member) => member.emit(shouldUseJSRepType)))); } @override @@ -197,6 +200,8 @@ class EnumMember { final String parent; + bool get isExternal => value == null; + EnumMember(this.name, this.value, {this.type, required this.parent, this.dartName}); @@ -206,10 +211,10 @@ class EnumMember { // 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) { + if (!isExternal) { f.modifier = (!jsRep ? FieldModifier.constant : FieldModifier.final$); } - if (dartName != null && name != dartName) { + if (dartName != null && name != dartName && isExternal) { f.annotations.add(generateJSAnnotation(name)); } f diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 5c2ceefa..64cb5067 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -49,15 +49,16 @@ class UnionType extends Type { @override 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'); } - - @override - String? get name => null; } +// TODO: Handle naming anonymous declarations class HomogenousUnionType extends UnionType implements DeclarationAssociatedType { final List _types; @@ -65,22 +66,20 @@ class HomogenousUnionType @override List get types => _types; - Type get baseType { - return types.first.baseType; - } + final Type baseType; final bool isNullable; + @override + String declarationName; + HomogenousUnionType( {required List types, this.isNullable = false, required String name}) : declarationName = name, _types = types, + baseType = types.first.baseType, super(types: types); - // TODO: We need a better way of naming declarations - @override - String declarationName; - @override EnumDeclaration get declaration => EnumDeclaration( name: declarationName, @@ -101,7 +100,7 @@ class HomogenousUnionType Reference emit([TypeOptions? options]) { return TypeReference((t) => t ..symbol = declarationName - ..isNullable = options?.nullable); + ..isNullable = options?.nullable ?? isNullable); } } @@ -116,15 +115,15 @@ 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); - - @override - 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 @@ -143,13 +142,7 @@ class LiteralType extends Type { }; 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 - }; + final primitive = kind.primitive; return BuiltinType.primitiveType(primitive); } @@ -165,4 +158,19 @@ class LiteralType extends Type { ID get id => ID(type: 'type', name: name); } -enum LiteralKind { $null, string, double, $true, $false, int } +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 + }; +} diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 2e879da8..08009ced 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -36,8 +36,10 @@ class TransformResult { ..ignoreForFile.addAll( ['constant_identifier_names', 'non_constant_identifier_names']) ..body.addAll(specs)); - print('${lib.accept(emitter)}'); - return MapEntry(file, formatter.format('${lib.accept(emitter)}')); + return MapEntry( + file, + formatter.format('${lib.accept(emitter)}' + .replaceAll('static external', 'external static'))); }); } } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index bf95d555..1753c62f 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -74,10 +74,10 @@ class Transformer { final members = []; PrimitiveType? enumRepType; - for (final mem in enumMembers) { - final memName = mem.name.text; + for (final member in enumMembers) { + final memName = member.name.text; final dartMemName = UniqueNamer.makeNonConflicting(memName); - final memInitializer = mem.initializer; + final memInitializer = member.initializer; // check the type of the initializer if (memInitializer != null) { @@ -86,12 +86,15 @@ class Transformer { // parse numeric literal final value = _parseNumericLiteral(memInitializer as TSNumericLiteral); - const primitiveType = PrimitiveType.num; + final primitiveType = + value is int ? PrimitiveType.int : PrimitiveType.double; members.add(EnumMember(memName, value, type: BuiltinType.primitiveType(primitiveType), parent: name, dartName: dartMemName)); - if (enumRepType == null) { + if (enumRepType == null && + !(primitiveType == PrimitiveType.int && + enumRepType == PrimitiveType.double)) { enumRepType = primitiveType; } else if (enumRepType != primitiveType) { enumRepType = PrimitiveType.any; @@ -288,23 +291,39 @@ class Transformer { // TODO: Unions final types = unionType.types.toDart.map(_transformType).toList(); + var isHomogenous = true; + + for (final type in types) { + if (type is LiteralType) { + if (type.kind == LiteralKind.$null) continue; + if (type.kind.primitive != + (types.first as LiteralType).kind.primitive) { + isHomogenous = false; + } + } else { + isHomogenous = false; + } + } + // check if it is a union of literals - if (types.every((t) => t is LiteralType) && - types.every((t) => - (t as LiteralType).kind == (types.first as LiteralType).kind || - t.kind == LiteralKind.$null)) { + if (isHomogenous) { // get the literal types other than null - final literalTypes = types.whereType(); - final nonNullLiteralTypes = - literalTypes.where((t) => t.kind != LiteralKind.$null).toList(); + final literalTypes = []; + final nonNullLiteralTypes = []; + var isBooleanType = false; + + for (final type in types) { + literalTypes.add(type as LiteralType); + if (type.kind != LiteralKind.$null) { + nonNullLiteralTypes.add(type); + isBooleanType = (type.kind == LiteralKind.$true) || + (type.kind == LiteralKind.$false); + } + } - final isNullable = nonNullLiteralTypes.length == literalTypes.length; + final isNullable = nonNullLiteralTypes.length != literalTypes.length; - if (nonNullLiteralTypes.map((t) => t.kind) - case [ - LiteralKind.$true || LiteralKind.$false, - LiteralKind.$true || LiteralKind.$false - ]) { + if (isBooleanType) { return BuiltinType.primitiveType(PrimitiveType.boolean, isNullable: isNullable); } diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart index c5fb7112..57f30934 100644 --- a/web_generator/test/integration/interop_gen/enum_expected.dart +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -3,7 +3,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; -extension type const Direction._(num _) { +extension type const Direction._(int _) { static const Direction Up = Direction._(0); static const Direction Down = Direction._(1); @@ -12,13 +12,24 @@ extension type const Direction._(num _) { static const Direction Right = Direction._(3); } -extension type const ResponseCode._(num _) { +extension type const ResponseCode._(int _) { static const ResponseCode Success = ResponseCode._(200); static const ResponseCode NotFound = ResponseCode._(404); static const ResponseCode ServerError = ResponseCode._(500); } +extension type const Fractions._(double _) { + static const Fractions Half = Fractions._(0.5); + + static const Fractions Quarter = Fractions._(0.25); + + static const Fractions Fifth = Fractions._(0.2); + + static const Fractions Tenth = Fractions._(0.1); + + static const Fractions Third = Fractions._(0.3333333333333333); +} extension type const LogLevel._(String _) { static const LogLevel Info = LogLevel._('INFO'); @@ -40,7 +51,7 @@ extension type BooleanLike._(_i1.JSAny? _) { static final BooleanLike Yes = BooleanLike._('YES'.toJS); } -extension type const Status._(num _) { +extension type const Status._(int _) { static const Status Active = Status._(1); static const Status Inactive = Status._(0); @@ -53,7 +64,7 @@ external Status get statusFromName; external void logStatus(Status status); @_i1.JS() external String handleDirection(Direction dir); -extension type const HttpStatus._(num _) { +extension type const HttpStatus._(int _) { static const HttpStatus OK = HttpStatus._(200); static const HttpStatus BadRequest = HttpStatus._(400); @@ -64,7 +75,26 @@ extension type const HttpStatus._(num _) { } @_i1.JS() external HttpStatus get statusCode; -extension type const Permissions._(num _) { +extension type MathConstants._(_i1.JSNumber _) { + static final MathConstants PI = MathConstants._(3.14.toJS); + + static final MathConstants TwoPI = MathConstants._(6.28.toJS); + + external static MathConstants Random; + + external static MathConstants Length; +} +extension type SomeRandomEnumValues._(_i1.JSAny? _) { + static final SomeRandomEnumValues moment = SomeRandomEnumValues._(2.toJS); + + static final SomeRandomEnumValues true$ = SomeRandomEnumValues._(6.28.toJS); + + @_i1.JS('default') + external static SomeRandomEnumValues default$; + + external static SomeRandomEnumValues unknown; +} +extension type const Permissions._(int _) { static const Permissions Read = Permissions._(1); static const Permissions Write = Permissions._(2); @@ -83,6 +113,12 @@ external AnonymousUnion currentTheme; external AnonymousUnion$1 buttonState; @_i1.JS() external AnonymousUnion$2 retriesLeft; +@_i1.JS() +external AnonymousUnion$3? get direction; +@_i1.JS() +external AnonymousUnion$4 get someUnionEnum; +@_i1.JS() +external bool get myBooleanEnum; extension type const AnonymousUnion._(String _) { static const AnonymousUnion light = AnonymousUnion._('light'); @@ -91,7 +127,6 @@ extension type const AnonymousUnion._(String _) { static const AnonymousUnion system = AnonymousUnion._('system'); } extension type const AnonymousUnion$1._(String _) { - @_i1.JS('default') static const AnonymousUnion$1 default$ = AnonymousUnion$1._('default'); static const AnonymousUnion$1 hovered = AnonymousUnion$1._('hovered'); @@ -101,15 +136,31 @@ extension type const AnonymousUnion$1._(String _) { static const AnonymousUnion$1 disabled = AnonymousUnion$1._('disabled'); } extension type const AnonymousUnion$2._(num _) { - @_i1.JS('0') static const AnonymousUnion$2 $0 = AnonymousUnion$2._(0); - @_i1.JS('1') static const AnonymousUnion$2 $1 = AnonymousUnion$2._(1); - @_i1.JS('2') static const AnonymousUnion$2 $2 = AnonymousUnion$2._(2); - @_i1.JS('3') static const AnonymousUnion$2 $3 = AnonymousUnion$2._(3); } +extension type const AnonymousUnion$3._(String _) { + static const AnonymousUnion$3 N = AnonymousUnion$3._('N'); + + static const AnonymousUnion$3 S = AnonymousUnion$3._('S'); + + static const AnonymousUnion$3 E = AnonymousUnion$3._('E'); + + static const AnonymousUnion$3 W = AnonymousUnion$3._('W'); +} +extension type const AnonymousUnion$4._(num _) { + static const AnonymousUnion$4 $2 = AnonymousUnion$4._(2); + + static const AnonymousUnion$4 $4 = AnonymousUnion$4._(4); + + static const AnonymousUnion$4 $6 = AnonymousUnion$4._(6); + + static const AnonymousUnion$4 $8 = AnonymousUnion$4._(8); + + static const AnonymousUnion$4 $10 = AnonymousUnion$4._(10); +} diff --git a/web_generator/test/integration/interop_gen/enum_input.d.ts b/web_generator/test/integration/interop_gen/enum_input.d.ts index 2449496f..b19e8109 100644 --- a/web_generator/test/integration/interop_gen/enum_input.d.ts +++ b/web_generator/test/integration/interop_gen/enum_input.d.ts @@ -9,6 +9,13 @@ export declare enum ResponseCode { NotFound = 404, ServerError = 500 } +export declare enum Fractions { + Half = 0.5, + Quarter = 0.25, + Fifth = 0.2, + Tenth = 0.1, + Third = 0.3333333333333333 +} export declare enum LogLevel { Info = "INFO", Warn = "WARN", @@ -49,6 +56,18 @@ declare enum DuplicateValues { B = 2, C = 1 } +export declare enum MathConstants { + PI = 3.14, // constant + TwoPI = 6.28, + Random, // computed at compile time + Length, +} +export declare enum SomeRandomEnumValues { + moment = 2, + true = 6.28, + default, + unknown, +} declare const statusKeys: string[]; export declare enum Permissions { Read = 1,// 0001 @@ -61,3 +80,6 @@ export declare const userPermissions: Permissions; export declare let currentTheme: "light" | "dark" | "system"; export declare let buttonState: "default" | "hovered" | "pressed" | "disabled"; export declare let retriesLeft: 0 | 1 | 2 | 3; +export declare const direction: "N" | "S" | "E" | "W" | null; +export declare const someUnionEnum: 2 | 4 | 6 | 8 | 10; +export declare const myBooleanEnum: true | false; \ No newline at end of file From 72d9bed8f8b08a7a26ce4dd105ef5031983d088d Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Mon, 7 Jul 2025 21:27:36 -0400 Subject: [PATCH 5/6] resolved remaining issues --- web_generator/lib/src/ast/types.dart | 16 ++----- .../interop_gen/transform/transformer.dart | 47 +++++++++---------- .../integration/interop_gen/enum_input.d.ts | 2 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 64cb5067..7ba0e7fe 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -8,12 +8,6 @@ import 'base.dart'; import 'builtin.dart'; import 'declarations.dart'; -abstract interface class DeclarationAssociatedType { - String get declarationName; - - Declaration get declaration; -} - class ReferredType extends Type { @override String name; @@ -59,8 +53,10 @@ class UnionType extends Type { } // TODO: Handle naming anonymous declarations -class HomogenousUnionType - extends UnionType implements DeclarationAssociatedType { +// TODO: Extract having a declaration associated with a type to its own type +// (e.g DeclarationAssociatedType) +class HomogenousEnumType + extends UnionType { final List _types; @override @@ -70,17 +66,15 @@ class HomogenousUnionType final bool isNullable; - @override String declarationName; - HomogenousUnionType( + HomogenousEnumType( {required List 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), diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 1753c62f..55165aaf 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -159,11 +159,12 @@ class Transformer { return variable.declarationList.declarations.toDart.map((d) { namer.markUsed(d.name.text); - return VariableDeclaration( + final variableDeclaration = VariableDeclaration( name: d.name.text, type: d.type == null ? BuiltinType.anyType : _transformType(d.type!), modifier: modifier, exported: isExported); + return variableDeclaration; }).toList(); } @@ -292,14 +293,29 @@ class Transformer { final types = unionType.types.toDart.map(_transformType).toList(); var isHomogenous = true; + final nonNullLiteralTypes = []; + bool? isBooleanType; + var isNullable = false; + LiteralType? firstNonNullablePrimitiveType; for (final type in types) { if (type is LiteralType) { - if (type.kind == LiteralKind.$null) continue; + if (type.kind == LiteralKind.$null) { + isNullable = true; + continue; + } + firstNonNullablePrimitiveType ??= type; + isBooleanType ??= (type.kind == LiteralKind.$true) || + (type.kind == LiteralKind.$false); if (type.kind.primitive != - (types.first as LiteralType).kind.primitive) { + firstNonNullablePrimitiveType.kind.primitive) { isHomogenous = false; } + if (isBooleanType) { + isBooleanType = (type.kind == LiteralKind.$true) || + (type.kind == LiteralKind.$false); + } + nonNullLiteralTypes.add(type); } else { isHomogenous = false; } @@ -307,23 +323,7 @@ class Transformer { // check if it is a union of literals if (isHomogenous) { - // get the literal types other than null - final literalTypes = []; - final nonNullLiteralTypes = []; - var isBooleanType = false; - - for (final type in types) { - literalTypes.add(type as LiteralType); - if (type.kind != LiteralKind.$null) { - nonNullLiteralTypes.add(type); - isBooleanType = (type.kind == LiteralKind.$true) || - (type.kind == LiteralKind.$false); - } - } - - final isNullable = nonNullLiteralTypes.length != literalTypes.length; - - if (isBooleanType) { + if (isBooleanType ?? false) { return BuiltinType.primitiveType(PrimitiveType.boolean, isNullable: isNullable); } @@ -332,10 +332,8 @@ class Transformer { namer.makeUnique('AnonymousUnion', 'type'); // TODO: Handle similar types here... - final type = HomogenousUnionType( + return HomogenousEnumType( types: nonNullLiteralTypes, isNullable: isNullable, name: name); - - return type; } return UnionType(types: types); @@ -452,7 +450,8 @@ class Transformer { break; case final EnumDeclaration _: break; - case final HomogenousUnionType hu: + // TODO: We can make (DeclarationAssociatedType) and use that rather than individual type names + case final HomogenousEnumType hu: filteredDeclarations.add(hu.declaration); break; case final UnionType u: diff --git a/web_generator/test/integration/interop_gen/enum_input.d.ts b/web_generator/test/integration/interop_gen/enum_input.d.ts index b19e8109..b4b720ec 100644 --- a/web_generator/test/integration/interop_gen/enum_input.d.ts +++ b/web_generator/test/integration/interop_gen/enum_input.d.ts @@ -82,4 +82,4 @@ export declare let buttonState: "default" | "hovered" | "pressed" | "disabled"; export declare let retriesLeft: 0 | 1 | 2 | 3; export declare const direction: "N" | "S" | "E" | "W" | null; export declare const someUnionEnum: 2 | 4 | 6 | 8 | 10; -export declare const myBooleanEnum: true | false; \ No newline at end of file +export declare const myBooleanEnum: true | false; From 53c2bf1ace58475cfa4119d63460f85aa238b3ff Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Tue, 8 Jul 2025 19:27:47 -0400 Subject: [PATCH 6/6] final resolutions --- .../src/interop_gen/transform/transformer.dart | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 55165aaf..cd121358 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -159,12 +159,11 @@ class Transformer { return variable.declarationList.declarations.toDart.map((d) { namer.markUsed(d.name.text); - final variableDeclaration = VariableDeclaration( + return VariableDeclaration( name: d.name.text, type: d.type == null ? BuiltinType.anyType : _transformType(d.type!), modifier: modifier, exported: isExported); - return variableDeclaration; }).toList(); } @@ -294,7 +293,7 @@ class Transformer { var isHomogenous = true; final nonNullLiteralTypes = []; - bool? isBooleanType; + var onlyContainsBooleanTypes = true; var isNullable = false; LiteralType? firstNonNullablePrimitiveType; @@ -305,16 +304,12 @@ class Transformer { continue; } firstNonNullablePrimitiveType ??= type; - isBooleanType ??= (type.kind == LiteralKind.$true) || + onlyContainsBooleanTypes &= (type.kind == LiteralKind.$true) || (type.kind == LiteralKind.$false); if (type.kind.primitive != firstNonNullablePrimitiveType.kind.primitive) { isHomogenous = false; } - if (isBooleanType) { - isBooleanType = (type.kind == LiteralKind.$true) || - (type.kind == LiteralKind.$false); - } nonNullLiteralTypes.add(type); } else { isHomogenous = false; @@ -323,7 +318,7 @@ class Transformer { // check if it is a union of literals if (isHomogenous) { - if (isBooleanType ?? false) { + if (nonNullLiteralTypes.isNotEmpty && onlyContainsBooleanTypes) { return BuiltinType.primitiveType(PrimitiveType.boolean, isNullable: isNullable); } @@ -450,7 +445,8 @@ class Transformer { break; case final EnumDeclaration _: break; - // TODO: We can make (DeclarationAssociatedType) and use that rather than individual type names + // TODO: We can make (DeclarationAssociatedType) and use that + // rather than individual type names case final HomogenousEnumType hu: filteredDeclarations.add(hu.declaration); break;