diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart index c158715c..fa728762 100644 --- a/web_generator/lib/src/ast/base.dart +++ b/web_generator/lib/src/ast/base.dart @@ -44,7 +44,6 @@ class ASTOptions { } sealed class Node { - String? get name; abstract final ID id; String? get dartName; @@ -54,7 +53,6 @@ sealed class Node { } abstract class Declaration extends Node { - @override String get name; @override @@ -65,9 +63,14 @@ abstract class NamedDeclaration extends Declaration { @override abstract String name; - ReferredType asReferredType([List? typeArgs, String? url]) => + ReferredType asReferredType( + [List? typeArgs, bool isNullable = false, String? url]) => ReferredType( - name: name, declaration: this, typeParams: typeArgs ?? [], url: url); + name: name, + declaration: this, + typeParams: typeArgs ?? [], + url: url, + isNullable: isNullable); } abstract interface class ExportableDeclaration extends Declaration { @@ -85,8 +88,20 @@ abstract class Type extends Node { @override String? dartName; + /// Whether the given type is nullable or not + /// (unioned with `undefined` or `null`) + abstract bool isNullable; + @override Reference emit([covariant TypeOptions? options]); + + @override + bool operator ==(Object other) { + return other is Type && id == other.id; + } + + @override + int get hashCode => Object.hashAll([id]); } abstract class FieldDeclaration extends NamedDeclaration { @@ -103,6 +118,12 @@ abstract class CallableDeclaration extends NamedDeclaration { enum DeclScope { private, protected, public } +abstract class DeclarationType extends Type { + T get declaration; + + String get declarationName; +} + class ParameterDeclaration { final String name; @@ -124,3 +145,7 @@ class ParameterDeclaration { ..type = type.emit(TypeOptions(nullable: optional))); } } + +abstract class NamedType extends Type { + String get name; +} diff --git a/web_generator/lib/src/ast/builtin.dart b/web_generator/lib/src/ast/builtin.dart index 24d83584..a46d7e30 100644 --- a/web_generator/lib/src/ast/builtin.dart +++ b/web_generator/lib/src/ast/builtin.dart @@ -12,7 +12,7 @@ import 'base.dart'; /// A built in type supported by `dart:js_interop` or by this library /// (with generated declarations) -class BuiltinType extends Type { +class BuiltinType extends NamedType { @override final String name; @@ -21,15 +21,15 @@ class BuiltinType extends Type { /// Whether the given type is present in "dart:js_interop" final bool fromDartJSInterop; - // TODO(nikeokoronkwo): Types in general should have an `isNullable` - // property on them to indicate nullability for Dart generated code. - final bool? isNullable; + @override + bool isNullable; BuiltinType( {required this.name, this.typeParams = const [], this.fromDartJSInterop = false, - this.isNullable}); + bool? isNullable}) + : isNullable = isNullable ?? false; @override ID get id => ID(type: 'type', name: name); @@ -39,16 +39,14 @@ class BuiltinType extends Type { @override Reference emit([TypeOptions? options]) { - options ??= TypeOptions(); - return TypeReference((t) => t ..symbol = name ..types.addAll(typeParams // if there is only one type param, and it is void, ignore .where((p) => typeParams.length != 1 || p != $voidType) - .map((p) => p.emit(TypeOptions()))) + .map((p) => p.emit(options))) ..url = fromDartJSInterop ? 'dart:js_interop' : null - ..isNullable = isNullable ?? options!.nullable); + ..isNullable = isNullable || (options?.nullable ?? false)); } static final BuiltinType $voidType = BuiltinType(name: 'void'); @@ -81,7 +79,10 @@ class BuiltinType extends Type { name: 'JSString', fromDartJSInterop: true, isNullable: isNullable) : BuiltinType(name: 'String', isNullable: isNullable), PrimitiveType.$void || PrimitiveType.undefined => $voidType, - PrimitiveType.any || PrimitiveType.unknown => anyType, + PrimitiveType.any => (isNullable ?? false) + ? anyType + : BuiltinType(name: 'JSAny', fromDartJSInterop: true), + PrimitiveType.unknown => anyType, PrimitiveType.object => BuiltinType( name: 'JSObject', fromDartJSInterop: true, isNullable: isNullable), PrimitiveType.symbol => BuiltinType( @@ -128,13 +129,14 @@ class BuiltinType extends Type { } } -class PackageWebType extends Type { +class PackageWebType extends NamedType { @override final String name; final List typeParams; - final bool? isNullable; + @override + bool isNullable; @override ID get id => ID(type: 'type', name: name); @@ -143,12 +145,12 @@ class PackageWebType extends Type { String? get dartName => null; PackageWebType._( - {required this.name, this.typeParams = const [], this.isNullable}); + {required this.name, + this.typeParams = const [], + this.isNullable = false}); @override Reference emit([TypeOptions? options]) { - options ??= TypeOptions(); - // TODO: We can make this a shared function as it is called a lot // between types return TypeReference((t) => t @@ -156,16 +158,16 @@ class PackageWebType extends Type { ..types.addAll(typeParams // if there is only one type param, and it is void, ignore .where((p) => typeParams.length != 1 || p != BuiltinType.$voidType) - .map((p) => p.emit(TypeOptions()))) + .map((p) => p.emit(options))) ..url = 'package:web/web.dart' - ..isNullable = isNullable ?? options!.nullable); + ..isNullable = isNullable || (options?.nullable ?? false)); } static PackageWebType parse(String name, {bool? isNullable, List typeParams = const []}) { return PackageWebType._( name: renameMap.containsKey(name) ? renameMap[name]! : name, - isNullable: isNullable, + isNullable: isNullable ?? false, typeParams: typeParams); } } diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 2c7367b5..69b0f194 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -63,11 +63,15 @@ sealed class TypeDeclaration extends NestableDeclaration this.constructors = const [], this.parent}); - ExtensionType _emit( - [covariant DeclarationOptions? options, - bool abstract = false, + /// [useFirstExtendeeAsRepType] is used to assert that the extension type + /// generated has a representation type of the first member of [extendees] + /// if any. + ExtensionType _emit(covariant DeclarationOptions? options, + {bool abstract = false, List extendees = const [], - List implementees = const []]) { + List implementees = const [], + bool useFirstExtendeeAsRepType = false, + bool objectLiteralConstructor = false}) { options ??= DeclarationOptions(); final hierarchy = getMemberHierarchy(this); @@ -93,8 +97,8 @@ sealed class TypeDeclaration extends NestableDeclaration methodDecs.addAll(operators.where((p) => p.scope == DeclScope.public).map( (m) => m.emit(options!..override = isOverride(m.dartName ?? m.name)))); - final repType = this is ClassDeclaration - ? getClassRepresentationType(this as ClassDeclaration) + final repType = useFirstExtendeeAsRepType || this is ClassDeclaration + ? getRepresentationType(this) : BuiltinType.primitiveType(PrimitiveType.object, isNullable: false); return ExtensionType((e) => e @@ -120,6 +124,15 @@ sealed class TypeDeclaration extends NestableDeclaration ..types .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) ..constructors.addAll([ + if (objectLiteralConstructor) + Constructor((c) => c + ..external = true + ..optionalParameters.addAll(properties + .where((p) => p.scope == DeclScope.public) + .map((p) => Parameter((param) => param + ..named = true + ..name = p.name + ..type = p.type.emit(options?.toTypeOptions()))))), if (!abstract) if (constructors.isEmpty && this is ClassDeclaration) ConstructorDeclaration.defaultFor(this).emit(options) @@ -135,6 +148,8 @@ abstract class MemberDeclaration { late final TypeDeclaration parent; abstract final DeclScope scope; + + String? get name; } class VariableDeclaration extends FieldDeclaration @@ -186,9 +201,9 @@ class VariableDeclaration extends FieldDeclaration @override ReferredType asReferredType( - [List? typeArgs, String? url]) { + [List? typeArgs, bool? isNullable, String? url]) { return ReferredType.fromType(type, this, - typeParams: typeArgs ?? [], url: url); + typeParams: typeArgs ?? [], url: url, isNullable: isNullable ?? false); } } @@ -248,11 +263,11 @@ class FunctionDeclaration extends CallableDeclaration @override ReferredType asReferredType( - [List? typeArgs, String? url]) { + [List? typeArgs, bool? isNullable, String? url]) { // TODO: We could do better here and make the function type typed return ReferredType.fromType( BuiltinType.referred('Function', typeParams: typeArgs ?? [])!, this, - typeParams: typeArgs ?? [], url: url); + typeParams: typeArgs ?? [], url: url, isNullable: isNullable ?? false); } } @@ -435,8 +450,7 @@ class NamespaceDeclaration extends NestableDeclaration @override ExtensionType emit([covariant DeclarationOptions? options]) { - options ??= DeclarationOptions(); - options.static = true; + options?.static = true; // static props and vars final methods = []; final fields = []; @@ -444,12 +458,14 @@ class NamespaceDeclaration extends NestableDeclaration for (final decl in topLevelDeclarations) { if (decl case final VariableDeclaration variable) { if (variable.modifier == VariableModifier.$const) { - methods.add(variable.emit(options) as Method); + methods.add(variable.emit(options ?? DeclarationOptions(static: true)) + as Method); } else { - fields.add(variable.emit(options) as Field); + fields.add(variable.emit(options ?? DeclarationOptions(static: true)) + as Field); } } else if (decl case final FunctionDeclaration fn) { - methods.add(fn.emit(options)); + methods.add(fn.emit(options ?? DeclarationOptions(static: true))); } } @@ -569,8 +585,10 @@ class ClassDeclaration extends TypeDeclaration { @override ExtensionType emit([covariant DeclarationOptions? options]) { - return super._emit(options, abstract, - [if (extendedType case final extendee?) extendee], implementedTypes); + return super._emit(options, + abstract: abstract, + extendees: [if (extendedType case final extendee?) extendee], + implementees: implementedTypes); } @override @@ -592,6 +610,15 @@ class InterfaceDeclaration extends TypeDeclaration { final List extendedTypes; + /// This asserts that the extension type generated produces a rep type + /// other than its default, which is denoted by the first member of + /// [extendedTypes] if any. + final bool assertRepType; + + /// This asserts generating a constructor for creating the given interface + /// as an object literal via an object literal constructor + final bool objectLiteralConstructor; + InterfaceDeclaration( {required super.name, required super.exported, @@ -602,16 +629,17 @@ class InterfaceDeclaration extends TypeDeclaration { super.methods, super.properties, super.operators, - super.constructors}) + super.constructors, + this.assertRepType = false, + this.objectLiteralConstructor = false}) : _id = id; @override ExtensionType emit([covariant DeclarationOptions? options]) { - return super._emit( - options, - false, - extendedTypes, - ); + return super._emit(options, + extendees: extendedTypes, + useFirstExtendeeAsRepType: assertRepType, + objectLiteralConstructor: objectLiteralConstructor); } } @@ -798,6 +826,7 @@ class ConstructorDeclaration implements MemberDeclaration { final List parameters; + @override final String? name; final ID id; diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index acd3c824..affea337 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -4,6 +4,7 @@ import 'package:code_builder/code_builder.dart'; +import '../interop_gen/namer.dart'; import 'base.dart'; import 'builtin.dart'; import 'declarations.dart'; @@ -96,15 +97,23 @@ Set getMemberHierarchy(TypeDeclaration type, return members; } -Type getClassRepresentationType(ClassDeclaration cl) { - if (cl.extendedType case final extendee?) { +Type getRepresentationType(TypeDeclaration td) { + if (td case ClassDeclaration(extendedType: final extendee?)) { return switch (extendee) { - final ClassDeclaration classExtendee => - getClassRepresentationType(classExtendee), + ReferredType(declaration: final d) when d is TypeDeclaration => + getRepresentationType(d), + final BuiltinType b => b, + _ => BuiltinType.primitiveType(PrimitiveType.object, isNullable: false) + }; + } else if (td case InterfaceDeclaration(extendedTypes: [final extendee])) { + return switch (extendee) { + ReferredType(declaration: final d) when d is TypeDeclaration => + getRepresentationType(d), + final BuiltinType b => b, _ => BuiltinType.primitiveType(PrimitiveType.object, isNullable: false) }; } else { - final primitiveType = switch (cl.name) { + final primitiveType = switch (td.name) { 'Array' => PrimitiveType.array, _ => PrimitiveType.object }; @@ -133,3 +142,190 @@ Type getClassRepresentationType(ClassDeclaration cl) { return (requiredParams, optionalParams); } + +/// Recursively get the generic types specified in a given type [t] +List getGenericTypes(Type t) { + final types = <(String, Type?)>[]; + switch (t) { + case GenericType(): + types.add((t.name, t.constraint)); + break; + case ReferredType(typeParams: final referredTypeParams): + case UnionType(types: final referredTypeParams): + for (final referredTypeParam in referredTypeParams) { + types.addAll(getGenericTypes(referredTypeParam) + .map((t) => (t.name, t.constraint))); + } + break; + case ObjectLiteralType( + properties: final objectProps, + methods: final objectMethods, + constructors: final objectConstructors, + operators: final objectOperators + ): + for (final PropertyDeclaration(type: propType) in objectProps) { + types.addAll( + getGenericTypes(propType).map((t) => (t.name, t.constraint))); + } + + for (final MethodDeclaration( + typeParameters: alreadyEstablishedTypeParams, + returnType: methodType, + parameters: methodParams + ) in objectMethods) { + final typeParams = [methodType, ...methodParams.map((p) => p.type)]; + + for (final type in typeParams) { + final genericTypes = getGenericTypes(type); + for (final genericType in genericTypes) { + if (!alreadyEstablishedTypeParams + .any((al) => al.name == genericType.name)) { + types.add((genericType.name, genericType.constraint)); + } + } + } + } + + for (final ConstructorDeclaration(parameters: methodParams) + in objectConstructors) { + for (final ParameterDeclaration(type: methodParamType) + in methodParams) { + types.addAll(getGenericTypes(methodParamType) + .map((t) => (t.name, t.constraint))); + } + } + + for (final OperatorDeclaration( + typeParameters: alreadyEstablishedTypeParams, + returnType: methodType, + parameters: methodParams + ) in objectOperators) { + final typeParams = [methodType, ...methodParams.map((p) => p.type)]; + + for (final type in typeParams) { + final genericTypes = getGenericTypes(type); + for (final genericType in genericTypes) { + if (!alreadyEstablishedTypeParams + .any((al) => al.name == genericType.name)) { + types.add((genericType.name, genericType.constraint)); + } + } + } + } + break; + case ClosureType( + typeParameters: final alreadyEstablishedTypeParams, + returnType: final closureType, + parameters: final closureParams + ): + for (final type in [closureType, ...closureParams.map((p) => p.type)]) { + final genericTypes = getGenericTypes(type); + for (final genericType in genericTypes) { + if (!alreadyEstablishedTypeParams + .any((al) => al.name == genericType.name)) { + types.add((genericType.name, genericType.constraint)); + } + } + } + break; + default: + break; + } + + // Types are cloned so that modifications to constraints can happen without + // affecting initial references + return types.map((t) => GenericType(name: t.$1, constraint: t.$2)).toList(); +} + +Type desugarTypeAliases(Type t) { + if (t case final ReferredType ref + when ref.declaration is TypeAliasDeclaration) { + return desugarTypeAliases((ref.declaration as TypeAliasDeclaration).type); + } + return t; +} + +class TupleDeclaration extends NamedDeclaration + implements ExportableDeclaration { + @override + bool get exported => true; + + @override + ID get id => ID(type: 'tuple', name: name); + + final int count; + + final bool readonly; + + TupleDeclaration({required this.count, this.readonly = false}); + + @override + String? dartName; + + @override + String get name => readonly ? 'JSReadonlyTuple$count' : 'JSTuple$count'; + + @override + set name(String name) { + throw Exception('Forbidden: Cannot set name on tuple declaration'); + } + + /// Creates a tuple from types. + /// + /// The type args represent the tuple types for the tuple declaration + @override + TupleType asReferredType( + [List? typeArgs, bool isNullable = false, String? url]) { + assert(typeArgs?.length == count, + 'Type arguments must equal the number of tuples supported'); + return TupleType(types: typeArgs ?? [], tupleDeclUrl: url); + } + + @override + Spec emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + final repType = BuiltinType.primitiveType(PrimitiveType.array, + shouldEmitJsType: true, typeParams: [BuiltinType.anyType]); + + return ExtensionType((e) => e + ..name = name + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((r) => r + ..name = '_' + ..declaredRepresentationType = repType.emit()) + ..implements.addAll([if (repType != BuiltinType.anyType) repType.emit()]) + ..types.addAll(List.generate( + count, + (index) => TypeReference((t) => t + ..symbol = String.fromCharCode(65 + index) + ..bound = BuiltinType.anyType.emit()))) + ..methods.addAll([ + ...List.generate(count, (index) { + final returnType = String.fromCharCode(65 + index); + return Method((m) => m + ..name = '\$${index + 1}' + ..returns = refer(returnType) + ..type = MethodType.getter + ..body = refer('_') + .index(literalNum(index)) + .asA(refer(returnType)) + .code); + }), + if (!readonly) + ...List.generate(count, (index) { + final returnType = String.fromCharCode(65 + index); + return Method((m) => m + ..name = '\$${index + 1}' + ..type = MethodType.setter + ..requiredParameters.add(Parameter((p) => p + ..name = 'newValue' + ..type = refer(returnType))) + ..body = refer('_') + .index(literalNum(index)) + .assign(refer('newValue')) + .code); + }) + ])); + } +} diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 8c8a7c10..5e5552a9 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -4,18 +4,24 @@ import 'package:code_builder/code_builder.dart'; import '../interop_gen/namer.dart'; +import '../interop_gen/sub_type.dart'; +import '../utils/case.dart'; import 'base.dart'; import 'builtin.dart'; import 'declarations.dart'; +import 'helpers.dart'; /// A type referring to a type in the TypeScript AST -class ReferredType extends Type { +class ReferredType extends NamedType { @override String name; @override ID get id => ID(type: 'type', name: name); + @override + bool isNullable; + T declaration; List typeParams; @@ -26,10 +32,13 @@ class ReferredType extends Type { {required this.name, required this.declaration, this.typeParams = const [], - this.url}); + this.url, + this.isNullable = false}); factory ReferredType.fromType(Type type, T declaration, - {List typeParams, String? url}) = ReferredDeclarationType; + {List typeParams, + String? url, + bool isNullable}) = ReferredDeclarationType; @override Reference emit([TypeOptions? options]) { @@ -38,7 +47,7 @@ class ReferredType extends Type { ? (declaration as NestableDeclaration).completedDartName : declaration.dartName ?? declaration.name ..types.addAll(typeParams.map((t) => t.emit(options))) - ..isNullable = options?.nullable + ..isNullable = (options?.nullable ?? false) || isNullable ..url = options?.url ?? url); } } @@ -47,44 +56,86 @@ class ReferredDeclarationType extends ReferredType { Type type; @override - String get name => type.name ?? declaration.name; + String get name => + type is NamedType ? (type as NamedType).name : declaration.name; ReferredDeclarationType(this.type, T declaration, - {super.typeParams, super.url}) + {super.typeParams, super.url, super.isNullable}) : super(name: declaration.name, declaration: declaration); @override Reference emit([covariant TypeOptions? options]) { options ??= TypeOptions(); options.url = super.url; + options.nullable = super.isNullable; return type.emit(options); } } -// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`) -class UnionType extends Type { +class TupleType extends ReferredType { + final List types; + + @override + List get typeParams => types; + + TupleType( + {required this.types, super.isNullable, required String? tupleDeclUrl}) + : super( + declaration: TupleDeclaration(count: types.length), + name: 'JSTuple${types.length}', + url: tupleDeclUrl); + + @override + ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join(',')); + + @override + int get hashCode => Object.hashAllUnordered(types); + + @override + bool operator ==(Object other) { + return other is TupleType && other.types.every(types.contains); + } +} + +class UnionType extends DeclarationType { final List types; - UnionType({required this.types}); + @override + bool isNullable; + + @override + String declarationName; + + UnionType( + {required this.types, required String name, this.isNullable = false}) + : declarationName = name; @override ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('|')); @override - String? get name => null; + Declaration get declaration => _UnionDeclaration( + name: declarationName, types: types, isNullable: isNullable); @override Reference emit([TypeOptions? options]) { - throw UnimplementedError('TODO: Implement UnionType.emit'); + return TypeReference((t) => t + ..symbol = declarationName + ..isNullable = (options?.nullable ?? false) || isNullable); + } + + @override + int get hashCode => Object.hashAllUnordered(types); + + @override + bool operator ==(Object other) { + return other is TupleType && other.types.every(types.contains); } } -// TODO: Handle naming anonymous declarations -// TODO: Extract having a declaration associated with a type to its own type -// (e.g DeclarationAssociatedType) class HomogenousEnumType - extends UnionType { + extends UnionType implements DeclarationType { final List _types; @override @@ -92,17 +143,12 @@ class HomogenousEnumType final Type baseType; - final bool isNullable; - - String declarationName; - HomogenousEnumType( - {required List types, this.isNullable = false, required String name}) - : declarationName = name, - _types = types, - baseType = types.first.baseType, - super(types: types); + {required List super.types, super.isNullable, required super.name}) + : _types = types, + baseType = types.first.baseType; + @override EnumDeclaration get declaration => EnumDeclaration( name: declarationName, dartName: UniqueNamer.makeNonConflicting(declarationName), @@ -117,25 +163,23 @@ class HomogenousEnumType ); }).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') -class GenericType extends Type { +class GenericType extends NamedType { @override final String name; - final Type? constraint; + Type? constraint; final Declaration? parent; - GenericType({required this.name, this.constraint, this.parent}); + @override + bool isNullable = false; + + GenericType( + {required this.name, this.constraint, this.parent, bool? isNullable}) + : isNullable = isNullable ?? false; @override ID get id => @@ -145,7 +189,17 @@ class GenericType extends Type { Reference emit([TypeOptions? options]) => TypeReference((t) => t ..symbol = name ..bound = constraint?.emit() - ..isNullable = options?.nullable); + ..isNullable = (options?.nullable ?? false) || isNullable); + + @override + bool operator ==(Object other) { + return other is GenericType && + other.name == name && + other.constraint == constraint; + } + + @override + int get hashCode => Object.hash(name, constraint); } /// A type representing a bare literal, such as `null`, a string or number @@ -155,6 +209,8 @@ class LiteralType extends Type { final Object? value; @override + bool isNullable; + String get name => switch (kind) { LiteralKind.$null => 'null', LiteralKind.int || LiteralKind.double => 'number', @@ -169,15 +225,27 @@ class LiteralType extends Type { return BuiltinType.primitiveType(primitive); } - LiteralType({required this.kind, required this.value}); + LiteralType( + {required this.kind, required this.value, this.isNullable = false}); @override Reference emit([TypeOptions? options]) { + options ??= TypeOptions(); + options.nullable = isNullable; + return baseType.emit(options); } @override - ID get id => ID(type: 'type', name: name); + ID get id => ID(type: 'type', name: '$name.$value'); + + @override + bool operator ==(Object other) { + return other is LiteralType && other.name == name && other.value == value; + } + + @override + int get hashCode => Object.hash(name, value); } enum LiteralKind { @@ -196,3 +264,331 @@ enum LiteralKind { LiteralKind.$true || LiteralKind.$false => PrimitiveType.boolean }; } + +class ObjectLiteralType extends DeclarationType { + final List properties; + + final List methods; + + final List constructors; + + final List operators; + + @override + bool isNullable; + + @override + final String declarationName; + + @override + final ID id; + + ObjectLiteralType( + {required String name, + required this.id, + this.properties = const [], + this.methods = const [], + this.constructors = const [], + this.operators = const [], + this.isNullable = false}) + : declarationName = name; + + @override + TypeDeclaration get declaration => InterfaceDeclaration( + name: declarationName, + exported: true, + id: ID(type: 'interface', name: id.name), + objectLiteralConstructor: true, + properties: properties, + methods: methods, + operators: operators, + constructors: constructors, + typeParameters: getGenericTypes(this).map((g) { + g.constraint ??= BuiltinType.anyType; + return g; + }).toList()); + + @override + Reference emit([TypeOptions? options]) { + return TypeReference((t) => t + ..symbol = declarationName + ..isNullable = options?.nullable ?? isNullable + ..types.addAll(getGenericTypes(this).map((t) => t.emit(options)))); + } +} + +sealed class ClosureType extends DeclarationType { + final List parameters; + final Type returnType; + final List typeParameters; + @override + bool isNullable; + + @override + final String declarationName; + + @override + final ID id; + + ClosureType({ + required String name, + required this.id, + required this.returnType, + this.typeParameters = const [], + this.parameters = const [], + this.isNullable = false, + }) : declarationName = name; + + @override + Reference emit([TypeOptions? options]) { + return TypeReference((t) => t + ..symbol = declarationName + ..isNullable = options?.nullable ?? isNullable); + } +} + +class ConstructorType extends ClosureType { + ConstructorType( + {required super.name, + required super.id, + required super.returnType, + super.typeParameters, + super.parameters, + super.isNullable}); + + @override + CallableDeclaration get declaration => _ConstructorDeclaration( + name: declarationName, + returnType: returnType, + parameters: parameters, + typeParameters: typeParameters); +} + +class FunctionType extends ClosureType { + FunctionType( + {required super.name, + required super.id, + required super.returnType, + super.typeParameters, + super.parameters, + super.isNullable}); + + @override + InterfaceDeclaration get declaration => InterfaceDeclaration( + name: declarationName, + exported: true, + id: ID(type: 'interface', name: declarationName), + typeParameters: typeParameters, + assertRepType: true, + extendedTypes: [ + BuiltinType.referred('Function')! + ], + methods: [ + MethodDeclaration( + name: 'call', + id: const ID(type: 'fun', name: 'call'), + returnType: returnType, + parameters: parameters, + typeParameters: typeParameters) + ]); +} + +class _ConstructorDeclaration extends CallableDeclaration + implements ExportableDeclaration { + @override + bool get exported => true; + + @override + ID get id => ID(type: 'closure', name: name); + + @override + String? dartName; + + @override + String name; + + @override + List parameters; + + @override + Type returnType; + + @override + List typeParameters; + + _ConstructorDeclaration( + {required this.name, + this.parameters = const [], + this.typeParameters = const [], + required this.returnType}); + + @override + Spec emit([covariant DeclarationOptions? options]) { + final (requiredParams, optionalParams) = + emitParameters(parameters, options); + + final repType = BuiltinType.referred('Function')!; + + final isNamedParams = desugarTypeAliases(returnType) is ObjectLiteralType && + (desugarTypeAliases(returnType) as ObjectLiteralType) + .constructors + .isEmpty; + + return ExtensionType((eType) => eType + ..name = name + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((r) => r + ..declaredRepresentationType = repType.emit(options?.toTypeOptions()) + ..name = '_') + ..implements.add(repType.emit(options?.toTypeOptions())) + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..methods.add(Method((m) => m + ..name = 'call' + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..returns = returnType.emit(options?.toTypeOptions()) + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams) + ..lambda = true + ..body = returnType + .emit(options?.toTypeOptions()) + .call( + isNamedParams + ? [] + : [ + ...requiredParams.map((p) => refer(p.name)), + if (optionalParams.isNotEmpty) + ...optionalParams.map((p) => refer(p.name)) + ], + isNamedParams + ? [ + ...requiredParams.map((p) => (p.name, p.type)), + if (optionalParams.isNotEmpty) + ...optionalParams.map((p) => (p.name, p.type)) + ].asMap().map((_, v) { + final (name, type) = v; + final isNumType = type?.symbol == 'num'; + return MapEntry( + name, + isNumType + ? refer(name).property('toDouble').call([]) + : refer(name)); + }) + : {}, + typeParameters + .map((t) => t.emit(options?.toTypeOptions())) + .toList()) + .code))); + } +} + +// TODO: Merge properties/methods of related types +class _UnionDeclaration extends NamedDeclaration + implements ExportableDeclaration { + @override + bool get exported => true; + + @override + ID get id => ID(type: 'union', name: name); + + bool isNullable; + + List types; + + List typeParameters; + + _UnionDeclaration( + {required this.name, + this.types = const [], + this.isNullable = false, + List? typeParams}) + : typeParameters = typeParams ?? [] { + if (typeParams == null) { + for (final type in types) { + typeParameters.addAll(getGenericTypes(type).map((t) { + t.constraint ??= BuiltinType.anyType; + return t; + })); + } + } + } + + @override + String? dartName; + + @override + String name; + + @override + Spec emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + final repType = + getLowestCommonAncestorOfTypes(types, isNullable: isNullable); + + return ExtensionType((e) => e + ..name = name + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((r) => r + ..name = '_' + ..declaredRepresentationType = repType.emit(options?.toTypeOptions())) + ..implements.addAll([repType.emit(options?.toTypeOptions())]) + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..methods.addAll(types.map((t) { + final type = t.emit(options?.toTypeOptions()); + final jsTypeAlt = getJSTypeAlternative(t); + return Method((m) { + final word = switch (t) { + DeclarationType(declarationName: final declName) => declName, + NamedType(name: final typeName, dartName: final dartTypeName) => + dartTypeName ?? typeName, + _ => t.dartName ?? t.id.name + }; + m + ..type = MethodType.getter + ..name = 'as${uppercaseFirstLetter(word)}' + ..returns = type + ..body = jsTypeAlt.id == t.id + ? refer('_').asA(type).code + : switch (t) { + BuiltinType(name: final n) when n == 'int' => refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDartInt') + .code, + BuiltinType(name: final n) + when n == 'double' || n == 'num' => + refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDartDouble') + .code, + BuiltinType() => refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDart') + .code, + ReferredType( + declaration: final decl, + name: final n, + url: final url + ) + when decl is EnumDeclaration => + refer(n, url).property('_').call([ + refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property(decl.baseType is NamedType + ? switch ((decl.baseType as NamedType).name) { + 'int' => 'toDartInt', + 'num' || 'double' => 'toDartDouble', + _ => 'toDart' + } + : 'toDart') + ]).code, + _ => refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .code + }; + }); + }))); + } +} diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart index 26f34d56..2291f18a 100644 --- a/web_generator/lib/src/dart_main.dart +++ b/web_generator/lib/src/dart_main.dart @@ -83,14 +83,19 @@ Future generateJSInteropBindings(Config config) async { if (generatedCodeMap.isEmpty) return; // write code to file - if (generatedCodeMap.length == 1) { - final entry = generatedCodeMap.entries.first; - fs.writeFileSync(configOutput.toJS, entry.value.toJS); - } else { + if (dartDeclarations.multiFileOutput) { for (final entry in generatedCodeMap.entries) { fs.writeFileSync( p.join(configOutput, p.basename(entry.key)).toJS, entry.value.toJS); } + } else { + final entry = generatedCodeMap.entries.first; + fs.writeFileSync(configOutput.toJS, entry.value.toJS); + for (final entry in generatedCodeMap.entries.skip(1)) { + fs.writeFileSync( + p.join(p.dirname(configOutput), p.basename(entry.key)).toJS, + entry.value.toJS); + } } } diff --git a/web_generator/lib/src/interop_gen/hasher.dart b/web_generator/lib/src/interop_gen/hasher.dart new file mode 100644 index 00000000..117bfa09 --- /dev/null +++ b/web_generator/lib/src/interop_gen/hasher.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; + +/// A hasher is used to give a unique hash to a given anonymous declaration +class AnonymousHasher { + static String hashUnion(List parts) { + final cloneParts = parts; + cloneParts.sort((a, b) => a.compareTo(b)); + + return _hashValues(cloneParts).toString().substring(0, 7); + } + + static String hashTuple(List parts) { + return _hashValues(parts).toString().substring(0, 7); + } + + static String hashObject(List<(String, String)> parts) { + final cloneParts = parts; + cloneParts.sort((a, b) => a.$1.compareTo(b.$1)); + + final hashes = cloneParts.map((v) { + return _hashValues([v.$1, v.$2]).toString(); + }); + + return _hashValues(hashes).toString().substring(0, 7); + } + + static String hashFun(List<(String, String)> params, String returnType, + [bool constructor = false]) { + final hashes = params.map((v) { + return _hashValues([v.$1, v.$2]).toString(); + }); + final paramHash = _hashValues(hashes); + return _hashValues( + [constructor.toString(), paramHash.toString(), returnType]) + .toString() + .substring(0, 7); + } +} + +// TODO: A better way for hashing values +int _hashValues(Iterable values) { + final output = AccumulatorSink(); + final input = sha512.startChunkedConversion(output); + + for (final v in values) { + final encoded = jsonEncode(v); + input.add(utf8.encode(encoded)); + } + + input.close(); + final digest = output.events.single.bytes; + + return BigInt.parse( + digest + .sublist(0, 8) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(), + radix: 16) + .toInt(); +} diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart index 519e1445..1e64351c 100644 --- a/web_generator/lib/src/interop_gen/namer.dart +++ b/web_generator/lib/src/interop_gen/namer.dart @@ -26,6 +26,17 @@ class ID { @override String toString() => '$type#$name${index != null ? '#$index' : ''}'; + + @override + bool operator ==(Object other) { + return other is ID && + other.name == name && + other.type == type && + other.index == index; + } + + @override + int get hashCode => Object.hash(type, name, index); } class UniqueNamer { diff --git a/web_generator/lib/src/interop_gen/sub_type.dart b/web_generator/lib/src/interop_gen/sub_type.dart new file mode 100644 index 00000000..1380d38e --- /dev/null +++ b/web_generator/lib/src/interop_gen/sub_type.dart @@ -0,0 +1,431 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import '../ast/base.dart'; +import '../ast/builtin.dart'; +import '../ast/declarations.dart'; +import '../ast/helpers.dart'; +import '../ast/types.dart'; +import '../js_type_supertypes.dart'; +import 'hasher.dart'; +import 'transform.dart'; + +/// A directed acyclic graph representation of an inverted type hierarchy, +/// where a node (type) is connected to other nodes such that the given type +/// is directed to a supertype of the given type. +class TypeHierarchy { + List nodes = []; + + String value; + + TypeHierarchy(this.value); + + Set? _cachedTypeHierarchy; + + TypeHierarchy? getMapWithValue(String value) { + if (this.value == value) return this; + if (nodes.isEmpty) return null; + return nodes.where((v) => v.getMapWithValue(value) != null).firstOrNull; + } + + @visibleForTesting + TypeHierarchy getMapWithLookup(Iterable path) { + if (path.isEmpty) return this; + return nodes[path.first].getMapWithLookup(path.skip(1)); + } + + @visibleForTesting + String getValueWithLookup(Iterable path) { + if (path.isEmpty) return value; + return nodes[path.first].getValueWithLookup(path.skip(1)); + } + + @visibleForTesting + ({int level, List path})? lookup(String value) { + return _lookup(value, 0, []); + } + + ({int level, List path})? _lookup( + String value, int level, List indexPath) { + if (this.value == value) return (level: level, path: indexPath); + if (nodes.isEmpty) { + return null; + } else { + // find value + return nodes.mapIndexed((index, node) { + final lookupVal = node._lookup(value, level + 1, [...indexPath, index]); + return lookupVal; + }).firstWhereOrNull((v) => v != null); + } + } + + void addChainedValues(Iterable list) { + if (list.isEmpty) { + return; + } else if (list.length == 1) { + nodes.add(TypeHierarchy(list.single)); + return; + } + nodes.add(TypeHierarchy(list.first)..addChainedValues(list.skip(1))); + } + + Set expand() { + if (_cachedTypeHierarchy != null) return _cachedTypeHierarchy!; + final set = {value}; + for (final node in nodes) { + set.add(node.value); + set.addAll(node.expand()); + } + + _cachedTypeHierarchy ??= set; + + return set; + } + + @override + bool operator ==(Object other) { + return other is TypeHierarchy && other.value == value; + } + + @override + int get hashCode => Object.hashAll([value]); + + @override + String toString() => value.toString(); +} + +Map _cachedTrees = {}; + +/// Given a set of type hierarchies, form a map indexing the number of edges +/// directed at a given node (the center for a specific [TypeHierarchy]) to the +/// node. +Map generateMapFromNodes(List nodes) { + final graph = {}; + for (final node in nodes) { + graph[node] = 0; + _mapFromNodes(node.nodes, graph); + } + return graph; +} + +void _mapFromNodes(List nodes, Map map) { + for (final node in nodes) { + map.update(node, (v) => v++, ifAbsent: () => 1); + _mapFromNodes(node.nodes, map); + } +} + +/// Given a set of type hierarchies, form a topologically sorted list using +/// (a modified version of) Kahn's algorithm on the type hierarchies, +/// sorting each level as a [Set] of distinct types. +/// +/// Returns a list of sets of strings forming the levels in the topological +/// ordering +List> topologicalList(List nodes) { + final graph = generateMapFromNodes(nodes); + final outputList = >[]; + + final nodesWithoutEdges = {}; + for (final MapEntry(key: node, value: noOfEdges) in graph.entries) { + if (noOfEdges == 0) nodesWithoutEdges.add(node); + } + + while (nodesWithoutEdges.isNotEmpty) { + final listToAdd = {}; + final nodesToAddNext = {}; + for (final node in nodesWithoutEdges) { + listToAdd.add(node.value); + for (final n in node.nodes) { + graph[n] = graph[n]! - 1; + if (graph[n] == 0) { + nodesToAddNext.add(n); + } + } + } + + nodesWithoutEdges.clear(); + nodesWithoutEdges.addAll(nodesToAddNext); + outputList.add(listToAdd); + } + + return outputList; +} + +/// Given a [Type], get its ancestoral graph (a DAG) and form a [TypeHierarchy] +/// from it. +/// +/// The function recursively goes through the types [type] inherits from, +/// depending on what kind of type it is, and follows this ancestral tree up +/// until it gets to the final ancestor: `JSAny` (all types inherit `JSAny`) +TypeHierarchy getTypeHierarchy(Type type) { + if (type case final ReferredType ref + when ref.declaration is TypeAliasDeclaration) { + return getTypeHierarchy((ref.declaration as TypeAliasDeclaration).type); + } + + final name = type is NamedType ? type.name : type.id.name; + return _cachedTrees.putIfAbsent(name, () { + final hierarchy = TypeHierarchy(name); + + switch (type) { + case HomogenousEnumType(types: final homogenousTypes): + hierarchy.nodes.add(getTypeHierarchy(homogenousTypes.first.baseType)); + break; + case UnionType(types: final types): + // subtype is union + hierarchy.nodes + .add(getTypeHierarchy(getLowestCommonAncestorOfTypes(types))); + break; + case TupleType(types: final types): + // subtype is JSArray + hierarchy.nodes.add(getTypeHierarchy(BuiltinType.primitiveType( + PrimitiveType.array, + typeParams: [getLowestCommonAncestorOfTypes(types)]))); + break; + case GenericType(constraint: final constraintedType): + hierarchy.nodes + .add(getTypeHierarchy(constraintedType ?? BuiltinType.anyType)); + break; + case ReferredDeclarationType(type: final referredType): + return getTypeHierarchy(referredType); + case ReferredType(declaration: final decl) when decl is ClassDeclaration: + final types = [ + if (decl.extendedType != null) decl.extendedType!, + ...decl.implementedTypes, + ]; + if (types.isEmpty) { + hierarchy.nodes.add(getTypeHierarchy( + BuiltinType.primitiveType(PrimitiveType.object))); + } else { + for (final t in types) { + hierarchy.nodes.add(getTypeHierarchy(t)); + } + } + break; + case ReferredType(declaration: final decl) + when decl is InterfaceDeclaration: + if (decl.extendedTypes.isEmpty) { + hierarchy.nodes.add(getTypeHierarchy( + BuiltinType.primitiveType(PrimitiveType.object))); + } else { + for (final t in decl.extendedTypes) { + hierarchy.nodes.add(getTypeHierarchy(t)); + } + } + break; + case ReferredType(declaration: final decl) + when decl is NamespaceDeclaration: + case ObjectLiteralType(): + // subtype is JSObject + hierarchy.nodes.add( + getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.object))); + break; + case ReferredType(declaration: final decl) + when decl is FunctionDeclaration: + case ClosureType(): + // subtype is JSFunction + hierarchy.nodes + .add(getTypeHierarchy(BuiltinType.referred('Function')!)); + break; + case LiteralType(baseType: final baseType): + hierarchy.nodes.add(getTypeHierarchy(baseType)); + break; + + case BuiltinType(): + // we can only use JS types + final BuiltinType(name: jsName) = + getJSTypeAlternative(type) as BuiltinType; + + var value = jsTypeSupertypes[jsName]; + final list = []; + while (value != null) { + list.add(value); + value = jsTypeSupertypes[value]; + } + hierarchy.addChainedValues(list); + break; + default: + print('WARN: Could not get type hierarchy for type of kind ' + '${type.runtimeType}. Skipping...'); + break; + } + return hierarchy; + }); +} + +TypeMap createTypeMap(List types, {TypeMap? map}) { + final outputMap = map ?? + TypeMap({ + 'JSBoolean': BuiltinType.primitiveType(PrimitiveType.boolean, + shouldEmitJsType: true), + 'JSNumber': BuiltinType.primitiveType(PrimitiveType.num, + shouldEmitJsType: true), + 'JSObject': BuiltinType.primitiveType(PrimitiveType.object, + shouldEmitJsType: true), + 'JSString': BuiltinType.primitiveType(PrimitiveType.string, + shouldEmitJsType: true), + 'JSVoid': BuiltinType.primitiveType(PrimitiveType.$void, + shouldEmitJsType: true), + 'JSSymbol': BuiltinType.primitiveType(PrimitiveType.symbol, + shouldEmitJsType: true), + 'JSBigInt': BuiltinType.primitiveType(PrimitiveType.bigint, + shouldEmitJsType: true), + 'JSAny': BuiltinType.primitiveType(PrimitiveType.any), + 'JSTypedArray': + BuiltinType(name: 'JSTypedArray', fromDartJSInterop: true), + }); + + void addToMap(Type type) { + outputMap[type is NamedType ? type.name : type.id.name] = type; + } + + for (final type in types) { + final name = type is NamedType ? type.name : type.id.name; + + if (outputMap.containsKey(name)) continue; + + addToMap(type); + switch (type) { + case ReferredDeclarationType(type: final referredType): + outputMap.addAll(createTypeMap([referredType], map: outputMap)); + break; + case ReferredType(declaration: final decl) when decl is ClassDeclaration: + outputMap.addAll(createTypeMap([ + if (decl.extendedType != null) decl.extendedType!, + ...decl.implementedTypes + ], map: outputMap)); + case ReferredType(declaration: final decl) + when decl is FunctionDeclaration: + addToMap(BuiltinType.referred('Function')!); + break; + case ReferredType(declaration: final decl) + when decl is InterfaceDeclaration: + outputMap + .addAll(createTypeMap([...decl.extendedTypes], map: outputMap)); + case HomogenousEnumType(types: final homogenousTypes): + outputMap.addAll( + createTypeMap([homogenousTypes.first.baseType], map: outputMap)); + break; + case TupleType(types: final types): + case UnionType(types: final types): + outputMap.addAll(createTypeMap(types, map: outputMap)); + break; + case GenericType(constraint: final constrainedType?): + outputMap.addAll(createTypeMap([constrainedType], map: outputMap)); + break; + default: + break; + } + } + + return outputMap; +} + +/// Given a list of types, usually from a union, get the subtype shared by the +/// types by getting the lowest common ancestor between the given types. +/// +/// If a [typeMap] is not provided, it generates the smallest necessary typemap +/// for the types. If only one type is provided, that type is returned. +/// +/// Types may have more than one type in common. In such case, a union of those +/// common types is returned by the given function. +Type getLowestCommonAncestorOfTypes(List types, + {bool isNullable = false, TypeMap? typeMap}) { + typeMap ??= createTypeMap(types); + + if (types.isEmpty) throw Exception('You must pass types'); + if (types.singleOrNull case final singleType?) { + return singleType..isNullable = isNullable; + } + + if (_getSharedPrimitiveTypeIfAny(types, isNullable: isNullable) + case final t?) { + return t; + } + + // Calculate the intersection of all type hierarchies + final typeMaps = types.map(getTypeHierarchy); + final parentHierarchy = typeMaps.map((map) => map.expand()); + final commonTypes = + parentHierarchy.reduce((val, element) => val.intersection(element)); + + final topoList = topologicalList(typeMaps.toList()); + for (final level in topoList) { + final typesAtLevel = commonTypes.intersection(level); + // look for level where common types are present + // the LCA are on the same topological level. + if (typesAtLevel.isNotEmpty) { + if (typesAtLevel.singleOrNull case final finalType?) { + return deduceType(finalType, typeMap); + } else { + return UnionType( + types: typesAtLevel.map((c) => deduceType(c, typeMap!)).toList(), + name: 'AnonymousUnion_' + '${AnonymousHasher.hashUnion(commonTypes.toList())}'); + } + } + } + + return BuiltinType.primitiveType(PrimitiveType.any); +} + +Type deduceType(String name, TypeMap map) { + final referredType = + BuiltinType.referred(name.startsWith('JS') ? name.substring(2) : name); + if (referredType != null) return referredType; + return map[name] ?? BuiltinType.primitiveType(PrimitiveType.any); +} + +/// Checks if there is a type shared between the types, usually in the +/// case of a literal +Type? _getSharedPrimitiveTypeIfAny(List types, {bool isNullable = true}) { + LiteralKind? kind; + Type? equalType; + bool? isNull; + var allEqualTypes = true; + var allLiteralTypes = true; + + for (final t in types) { + if (t is LiteralType) { + if (t.kind == LiteralKind.$null) { + isNull ??= true; + continue; + } + kind ??= t.kind; + if (kind != t.kind) { + allEqualTypes = false; + break; + } + } else { + allLiteralTypes = false; + equalType ??= t; + if (equalType.id != t.id) { + allEqualTypes = false; + break; + } + } + } + + if (allEqualTypes) { + if (allLiteralTypes) { + final primitiveType = switch (kind) { + LiteralKind.string => PrimitiveType.string, + LiteralKind.$false || LiteralKind.$true => PrimitiveType.boolean, + LiteralKind.double => PrimitiveType.double, + LiteralKind.int => PrimitiveType.int, + _ => PrimitiveType.any + }; + + return BuiltinType.primitiveType(primitiveType, + isNullable: isNull ?? isNullable); + } else { + return equalType!; + } + } + + return null; +} diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 62a0bca5..4b37fe14 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -7,10 +7,12 @@ import 'dart:js_interop'; import 'package:code_builder/code_builder.dart'; import 'package:dart_style/dart_style.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import '../ast/base.dart'; import '../ast/declarations.dart'; +import '../ast/helpers.dart'; import '../config.dart'; import '../js/helpers.dart'; import '../js/typescript.dart' as ts; @@ -28,8 +30,11 @@ typedef ProgramDeclarationMap = Map; class TransformResult { ProgramDeclarationMap programDeclarationMap; + ProgramDeclarationMap commonTypes; + bool multiFileOutput; - TransformResult._(this.programDeclarationMap); + TransformResult._(this.programDeclarationMap, {this.commonTypes = const {}}) + : multiFileOutput = programDeclarationMap.length > 1; // TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads // (namespaces + functions, multiple interfaces, etc) @@ -39,7 +44,7 @@ class TransformResult { _setGlobalOptions(config); - return programDeclarationMap.map((file, declMap) { + return {...programDeclarationMap, ...commonTypes}.map((file, declMap) { final emitter = DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); final specs = declMap.values @@ -61,13 +66,21 @@ class TransformResult { })); } l - ..ignoreForFile.addAll([ + ..ignoreForFile.addAll({ 'constant_identifier_names', 'non_constant_identifier_names', if (declMap.values .any((d) => d is NestableDeclaration && d.parent != null)) 'camel_case_types', - ]) + if (declMap.values.any((v) => v.id.name.contains('Anonymous'))) ...[ + 'camel_case_types', + 'library_private_types_in_public_api', + 'unnecessary_parenthesis' + ], + if (declMap.values.whereType().isNotEmpty) ...[ + 'unnecessary_parenthesis' + ] + }) ..body.addAll(specs); }); return MapEntry( @@ -79,10 +92,11 @@ class TransformResult { } /// A map of declarations, where the key is the declaration's stringified [ID]. -extension type NodeMap._(Map decls) implements Map { - NodeMap([Map? decls]) : decls = decls ?? {}; +extension type NodeMap._(Map decls) + implements Map { + NodeMap([Map? decls]) : decls = decls ?? {}; - List findByName(String name) { + List findByName(String name) { return decls.entries .where((e) { final n = UniqueNamer.parse(e.key).name; @@ -95,7 +109,7 @@ extension type NodeMap._(Map decls) implements Map { .toList(); } - List findByQualifiedName(QualifiedName qName) { + List findByQualifiedName(QualifiedName qName) { return decls.entries .where((e) { final name = UniqueNamer.parse(e.key).name; @@ -106,7 +120,14 @@ extension type NodeMap._(Map decls) implements Map { .toList(); } - void add(Node decl) => decls[decl.id.toString()] = decl; + void add(N decl) => decls[decl.id.toString()] = decl; +} + +extension type TypeMap._(Map types) implements NodeMap { + TypeMap([Map? types]) : types = types ?? {}; + + @redeclare + void add(Type decl) => types[decl.id.toString()] = decl; } /// A program map is a map used for handling the context of @@ -136,6 +157,11 @@ class ProgramMap { /// The typescript program for the given project final ts.TSProgram program; + /// Common types shared across files in the program. + /// + /// This includes builtin supported types like `JSTuple` + final p.PathMap> _commonTypes = p.PathMap.of({}); + /// The type checker for the given program /// /// It is generated as this to prevent having to regenerate it multiple times @@ -188,6 +214,31 @@ class ProgramMap { return name == null ? null : nodeMap.findByName(name); } + (String, NamedDeclaration)? getCommonType(String name, + {(String, NamedDeclaration)? ifAbsent}) { + try { + final MapEntry(key: url, value: nodeMap) = _commonTypes.entries + .firstWhere((e) => e.value.containsKey(name), orElse: () { + if (ifAbsent case (final file, final decl)) { + _commonTypes.update( + file, + (nodeMap) => nodeMap..add(decl), + ifAbsent: () => NodeMap()..add(decl), + ); + return MapEntry(file, _commonTypes[file]!); + } + throw Exception('Could not find common type for decl $name'); + }); + + if ((url, nodeMap) case (final declUrl?, final declarationMap)) { + return (declUrl, declarationMap.findByName(name).first); + } + } on Exception { + return null; + } + return null; + } + /// Get the node map for a given [file], /// transforming it and generating it if needed. NodeMap getNodeMap(String file) { @@ -270,6 +321,7 @@ class TransformerManager { outputNodeMap[file!] = programMap.getNodeMap(file); } - return TransformResult._(outputNodeMap); + return TransformResult._(outputNodeMap, + commonTypes: programMap._commonTypes.cast()); } } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index d209f4d8..5dfeb6cb 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -14,6 +14,7 @@ import '../../js/annotations.dart'; import '../../js/helpers.dart'; import '../../js/typescript.dart' as ts; import '../../js/typescript.types.dart'; +import '../hasher.dart'; import '../namer.dart'; import '../qualified_name.dart'; import '../transform.dart'; @@ -51,7 +52,7 @@ class Transformer { final NodeMap nodeMap = NodeMap(); /// A map of types - final NodeMap typeMap = NodeMap(); + final TypeMap typeMap = TypeMap(); /// The program map final ProgramMap programMap; @@ -495,7 +496,7 @@ class Transformer { } PropertyDeclaration _transformProperty(TSPropertyEntity property, - {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + {required UniqueNamer parentNamer, TypeDeclaration? parent}) { final name = property.name.text; final (:id, name: dartName) = parentNamer.makeUnique(name, 'var'); @@ -508,13 +509,13 @@ class Transformer { // check if final referredType = type as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - propType = parent.asReferredType(type.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + propType = parent?.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (property.type case final type? when ts.isThisTypeNode(type)) { - propType = parent.asReferredType(parent.typeParameters); + propType = parent?.asReferredType(parent.typeParameters); } final propertyDeclaration = PropertyDeclaration( @@ -529,12 +530,12 @@ class Transformer { static: isStatic, readonly: isReadonly, isNullable: property.questionToken != null); - propertyDeclaration.parent = parent; + if (parent != null) propertyDeclaration.parent = parent; return propertyDeclaration; } MethodDeclaration _transformMethod(TSMethodEntity method, - {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + {required UniqueNamer parentNamer, TypeDeclaration? parent}) { final name = method.name.text; // TODO(nikeokoronkwo): Let's make the unique name types enums // or extension types to track the type more easily @@ -552,13 +553,13 @@ class Transformer { // check if final referredType = type as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - methodType = parent.asReferredType(type.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + methodType = parent?.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (method.type case final type? when ts.isThisTypeNode(type)) { - methodType = parent.asReferredType(parent.typeParameters); + methodType = parent?.asReferredType(parent.typeParameters); } final methodDeclaration = MethodDeclaration( @@ -573,13 +574,13 @@ class Transformer { if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { final referredType = ty as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - paramType = parent.asReferredType(ty.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + paramType = parent?.asReferredType(ty.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (paramRawType case final ty? when ts.isThisTypeNode(ty)) { - paramType = parent.asReferredType(parent.typeParameters); + paramType = parent?.asReferredType(parent.typeParameters); } return _transformParameter(t, paramType); }).toList(), @@ -591,7 +592,7 @@ class Transformer { : BuiltinType.anyType), isNullable: (method.kind == TSSyntaxKind.MethodSignature) && (method as TSMethodSignature).questionToken != null); - methodDeclaration.parent = parent; + if (parent != null) methodDeclaration.parent = parent; return methodDeclaration; } @@ -623,7 +624,7 @@ class Transformer { MethodDeclaration _transformCallSignature( TSCallSignatureDeclaration callSignature, {required UniqueNamer parentNamer, - required TypeDeclaration parent}) { + TypeDeclaration? parent}) { final (:id, name: dartName) = parentNamer.makeUnique('call', 'fun'); final params = callSignature.parameters.toDart; @@ -635,14 +636,14 @@ class Transformer { // check if final referredType = type as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - methodType = parent.asReferredType(type.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + methodType = parent?.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (callSignature.type case final type? when ts.isThisTypeNode(type)) { - methodType = parent.asReferredType(parent.typeParameters); + methodType = parent?.asReferredType(parent.typeParameters); } final methodDeclaration = MethodDeclaration( @@ -656,14 +657,14 @@ class Transformer { (callSignature.type != null ? _transformType(callSignature.type!) : BuiltinType.anyType)); - methodDeclaration.parent = parent; + if (parent != null) methodDeclaration.parent = parent; return methodDeclaration; } // TODO: Handling overloading of indexers (OperatorDeclaration, OperatorDeclaration?) _transformIndexer( TSIndexSignatureDeclaration indexSignature, - {required TypeDeclaration parent}) { + {TypeDeclaration? parent}) { final params = indexSignature.parameters.toDart; final typeParams = indexSignature.typeParameters?.toDart; @@ -676,14 +677,14 @@ class Transformer { // check if final referredType = type as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - indexerType = parent.asReferredType(type.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + indexerType = parent?.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (indexSignature.type case final type when ts.isThisTypeNode(type)) { - indexerType = parent.asReferredType(parent.typeParameters); + indexerType = parent?.asReferredType(parent.typeParameters); } final getOperatorDeclaration = OperatorDeclaration( @@ -705,13 +706,15 @@ class Transformer { static: isStatic) : null; - getOperatorDeclaration.parent = parent; - setOperatorDeclaration?.parent = parent; + if (parent != null) { + getOperatorDeclaration.parent = parent; + setOperatorDeclaration?.parent = parent; + } return (getOperatorDeclaration, setOperatorDeclaration); } MethodDeclaration _transformGetter(TSGetAccessorDeclaration getter, - {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + {required UniqueNamer parentNamer, TypeDeclaration? parent}) { final name = getter.name.text; final (:id, name: dartName) = parentNamer.makeUnique(name, 'get'); @@ -727,13 +730,13 @@ class Transformer { // check if final referredType = type as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - methodType = parent.asReferredType(type.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + methodType = parent?.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (getter.type case final type? when ts.isThisTypeNode(type)) { - methodType = parent.asReferredType(parent.typeParameters); + methodType = parent?.asReferredType(parent.typeParameters); } final methodDeclaration = MethodDeclaration( @@ -749,12 +752,12 @@ class Transformer { (getter.type != null ? _transformType(getter.type!) : BuiltinType.anyType)); - methodDeclaration.parent = parent; + if (parent != null) methodDeclaration.parent = parent; return methodDeclaration; } MethodDeclaration _transformSetter(TSSetAccessorDeclaration setter, - {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + {required UniqueNamer parentNamer, TypeDeclaration? parent}) { final name = setter.name.text; final (:id, name: dartName) = parentNamer.makeUnique(name, 'set'); @@ -776,13 +779,13 @@ class Transformer { if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { final referredType = ty as TSTypeReferenceNode; final referredTypeName = parseQualifiedName(referredType.typeName); - if (referredTypeName.asName == parent.name) { - paramType = parent.asReferredType(ty.typeArguments?.toDart + if (referredTypeName.asName == parent?.name) { + paramType = parent?.asReferredType(ty.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); } } else if (paramRawType case final ty? when ts.isThisTypeNode(ty)) { - paramType = parent.asReferredType(parent.typeParameters); + paramType = parent?.asReferredType(parent.typeParameters); } return _transformParameter(t, paramType); }).toList(), @@ -792,7 +795,7 @@ class Transformer { returnType: setter.type != null ? _transformType(setter.type!) : BuiltinType.anyType); - methodDeclaration.parent = parent; + if (parent != null) methodDeclaration.parent = parent; return methodDeclaration; } @@ -1025,36 +1028,190 @@ class Transformer { /// [typeArg] represents whether the [TSTypeNode] is being passed in the /// context of a type argument, as Dart core types are not allowed in /// type arguments + /// + /// [isNullable] means that the given type is nullable, usually when it is + /// unionized with `undefined` or `null` // TODO(nikeokoronkwo): Add support for constructor and function types, // https://github.com/dart-lang/web/issues/410 // https://github.com/dart-lang/web/issues/422 Type _transformType(TSTypeNode type, - {bool parameter = false, bool typeArg = false}) { + {bool parameter = false, bool typeArg = false, bool? isNullable}) { switch (type.kind) { case TSSyntaxKind.ParenthesizedType: return _transformType((type as TSParenthesizedTypeNode).type, - parameter: parameter, typeArg: typeArg); + parameter: parameter, typeArg: typeArg, isNullable: isNullable); case TSSyntaxKind.TypeReference: final refType = type as TSTypeReferenceNode; - return _getTypeFromTypeNode(refType, typeArg: typeArg); + return _getTypeFromTypeNode(refType, + typeArg: typeArg, isNullable: isNullable ?? false); + case TSSyntaxKind.TypeLiteral: + // type literal + final typeLiteralNode = type as TSTypeLiteralNode; + + // lists + final properties = []; + final methods = []; + final constructors = []; + final operators = []; + + final typeNamer = ScopedUniqueNamer({'get', 'set'}); + + // mark the default constructor as used + typeNamer.markUsed('', 'constructor'); + typeNamer.markUsed('unnamed', 'constructor'); + + // transform decls + for (final member in typeLiteralNode.members.toDart) { + switch (member.kind) { + case TSSyntaxKind.PropertySignature: + final prop = _transformProperty(member as TSPropertySignature, + parentNamer: typeNamer); + properties.add(prop); + case TSSyntaxKind.MethodSignature: + final method = _transformMethod(member as TSMethodSignature, + parentNamer: typeNamer); + methods.add(method); + case TSSyntaxKind.IndexSignature: + final (opGet, opSetOrNull) = _transformIndexer( + member as TSIndexSignatureDeclaration, + ); + operators.add(opGet); + if (opSetOrNull case final opSet?) { + operators.add(opSet); + } + case TSSyntaxKind.CallSignature: + final callSignature = _transformCallSignature( + member as TSCallSignatureDeclaration, + parentNamer: typeNamer, + ); + methods.add(callSignature); + case TSSyntaxKind.ConstructSignature: + final constructor = _transformConstructor( + member as TSConstructSignatureDeclaration, + parentNamer: typeNamer); + constructors.add(constructor); + case TSSyntaxKind.GetAccessor: + final getter = _transformGetter( + member as TSGetAccessorDeclaration, + parentNamer: typeNamer); + methods.add(getter); + break; + case TSSyntaxKind.SetAccessor: + final setter = _transformSetter( + member as TSSetAccessorDeclaration, + parentNamer: typeNamer); + methods.add(setter); + break; + default: + break; + } + } + + // get a name + final name = 'AnonymousType_${AnonymousHasher.hashObject([ + ...properties.map((p) => (p.name, p.type.id.name)), + ...methods.map((p) => (p.name, p.returnType.id.name)), + ...constructors.map((p) => ( + p.name ?? 'new', + p.parameters.map((a) => a.type.id.name).join(',') + )), + ...operators.map((p) => (p.name, p.returnType.id.name)), + ])}'; + + // get an expected id + final expectedId = ID(type: 'type', name: name); + if (typeMap.containsKey(expectedId.toString())) { + return typeMap[expectedId.toString()] as ObjectLiteralType; + } + + final anonymousTypeObject = ObjectLiteralType( + name: name, + id: expectedId, + properties: properties, + methods: methods, + operators: operators, + constructors: constructors, + ); + + final anonymousType = typeMap.putIfAbsent(expectedId.toString(), () { + namer.markUsed(name); + return anonymousTypeObject; + }) as ObjectLiteralType; + + return anonymousType..isNullable = isNullable ?? false; + case TSSyntaxKind.ConstructorType || TSSyntaxKind.FunctionType: + final funType = type as TSFunctionOrConstructorTypeNodeBase; + + final parameters = + funType.parameters.toDart.map(_transformParameter).toList(); + + final typeParameters = funType.typeParameters?.toDart + .map(_transformTypeParamDeclaration) + .toList() ?? + []; + + final returnType = _transformType(funType.type); + + final isConstructor = type.kind == TSSyntaxKind.ConstructorType; + + final name = '_Anonymous${isConstructor ? 'Constructor' : 'Function'}_' + '${AnonymousHasher.hashFun(parameters.map((a) => ( + a.name, + a.type.id.name + )).toList(), returnType.id.name, isConstructor)}'; + + final expectedId = ID(type: 'type', name: name); + if (typeMap.containsKey(expectedId.toString())) { + return typeMap[expectedId.toString()] as ClosureType; + } + + final closureTypeObject = isConstructor + ? ConstructorType( + name: name, + id: expectedId, + returnType: returnType, + parameters: parameters, + typeParameters: typeParameters) + : FunctionType( + name: name, + id: expectedId, + returnType: returnType, + parameters: parameters, + typeParameters: typeParameters); + + final closureType = typeMap.putIfAbsent(expectedId.toString(), () { + namer.markUsed(name); + return closureTypeObject; + }) as ClosureType; + + return closureType..isNullable = isNullable ?? false; case TSSyntaxKind.UnionType: final unionType = type as TSUnionTypeNode; - // TODO: Unions - final types = unionType.types.toDart.map(_transformType).toList(); + final unionTypes = unionType.types.toDart; + final nonNullableUnionTypes = unionTypes + .where((t) => + t.kind != TSSyntaxKind.UndefinedKeyword && + !(t.kind == TSSyntaxKind.LiteralType && + (t as TSLiteralTypeNode).literal.kind == + TSSyntaxKind.NullKeyword)) + .toList(); + final shouldBeNullable = + nonNullableUnionTypes.length != unionTypes.length; + + if (nonNullableUnionTypes.singleOrNull case final singleTypeNode?) { + return _transformType(singleTypeNode, isNullable: shouldBeNullable); + } + + final types = nonNullableUnionTypes.map(_transformType).toList(); var isHomogenous = true; final nonNullLiteralTypes = []; var onlyContainsBooleanTypes = true; - var isNullable = false; LiteralType? firstNonNullablePrimitiveType; for (final type in types) { if (type is LiteralType) { - if (type.kind == LiteralKind.$null) { - isNullable = true; - continue; - } firstNonNullablePrimitiveType ??= type; onlyContainsBooleanTypes &= (type.kind == LiteralKind.$true) || (type.kind == LiteralKind.$false); @@ -1068,38 +1225,62 @@ class Transformer { } } - // check if it is a union of literals - if (isHomogenous) { - if (nonNullLiteralTypes.isNotEmpty && onlyContainsBooleanTypes) { - return BuiltinType.primitiveType(PrimitiveType.boolean, - isNullable: isNullable); - } + if (isHomogenous && + nonNullLiteralTypes.isNotEmpty && + onlyContainsBooleanTypes) { + return BuiltinType.primitiveType(PrimitiveType.boolean, + isNullable: shouldBeNullable); + } - final expectedId = - ID(type: 'type', name: types.map((t) => t.id.name).join('|')); + final idMap = isHomogenous + ? nonNullLiteralTypes.map((t) => t.value.toString()) + : types.map((t) => t.id.name); - if (typeMap.containsKey(expectedId.toString())) { - return typeMap[expectedId.toString()] as UnionType; - } + final expectedId = ID(type: 'type', name: idMap.join('|')); - final (id: _, name: name) = - namer.makeUnique('AnonymousUnion', 'type'); + if (typeMap.containsKey(expectedId.toString())) { + return typeMap[expectedId.toString()] as UnionType; + } - // TODO: Handle similar types here... - final homogenousEnumType = HomogenousEnumType( - types: nonNullLiteralTypes, isNullable: isNullable, name: name); + final name = + 'AnonymousUnion_${AnonymousHasher.hashUnion(idMap.toList())}'; - return typeMap.putIfAbsent( - expectedId.toString(), () => homogenousEnumType) - as HomogenousEnumType; - } + final un = isHomogenous + ? HomogenousEnumType(types: nonNullLiteralTypes, name: name) + : UnionType(types: types, name: name); + + final unType = typeMap.putIfAbsent(expectedId.toString(), () { + namer.markUsed(name); + return un; + }); + return unType..isNullable = shouldBeNullable; + + case TSSyntaxKind.TupleType: + // tuple type is array + final tupleType = type as TSTupleTypeNode; + // TODO: Handle named tuple params (`[x: number, y: number]`) + final types = tupleType.elements.toDart + .map((t) => _transformType(t, typeArg: true)) + .toList(); + + // we will work based on the length of the types + final typeLength = types.length; + + // check if a tuple of a certain length already exists + // generate if not + final (tupleUrl, tupleDeclaration) = programMap.getCommonType( + 'JSTuple$typeLength', + ifAbsent: ('_tuples.dart', TupleDeclaration(count: typeLength)))!; + + return tupleDeclaration.asReferredType( + types, isNullable ?? false, tupleUrl); - return UnionType(types: types); case TSSyntaxKind.LiteralType: final literalType = type as TSLiteralTypeNode; final literal = literalType.literal; return LiteralType( + isNullable: isNullable ?? false, kind: switch (literal.kind) { // TODO: Will we support Regex? TSSyntaxKind.NumericLiteral => num.parse(literal.text) is int @@ -1129,12 +1310,16 @@ class Transformer { final typeArguments = typeQuery.typeArguments?.toDart; return _getTypeFromDeclaration(exprName, typeArguments, - typeArg: typeArg, isNotTypableDeclaration: true); + typeArg: typeArg, + isNotTypableDeclaration: true, + isNullable: isNullable ?? false); case TSSyntaxKind.ArrayType: - return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [ - getJSTypeAlternative( - _transformType((type as TSArrayTypeNode).elementType)) - ]); + return BuiltinType.primitiveType(PrimitiveType.array, + typeParams: [ + getJSTypeAlternative( + _transformType((type as TSArrayTypeNode).elementType)) + ], + isNullable: isNullable); default: // check for primitive type via its kind final primitiveType = switch (type.kind) { @@ -1155,7 +1340,8 @@ class Transformer { }; return BuiltinType.primitiveType(primitiveType, - shouldEmitJsType: typeArg ? true : null); + shouldEmitJsType: typeArg ? true : null, + isNullable: primitiveType == PrimitiveType.any ? true : isNullable); } } @@ -1193,13 +1379,12 @@ class Transformer { /// /// The referred type may accept [typeArguments], which are passed as well. Type _searchForDeclRecursive( - Iterable name, - TSSymbol symbol, { - NamespaceDeclaration? parent, - List? typeArguments, - bool isNotTypableDeclaration = false, - bool typeArg = false, - }) { + Iterable name, TSSymbol symbol, + {NamespaceDeclaration? parent, + List? typeArguments, + bool isNotTypableDeclaration = false, + bool typeArg = false, + bool isNullable = false}) { // get name and map final firstName = name.first.part; @@ -1227,12 +1412,14 @@ class Transformer { exportSet.removeWhere((e) => e.name == aliasedSymbolName); exportSet.add(ExportReference(aliasedSymbolName, as: firstName)); + // TODO: Is nullable return _getTypeFromSymbol( aliasedSymbol, typeChecker.getTypeOfSymbol(aliasedSymbol), typeArguments, typeArg, - isNotTypableDeclaration); + isNotTypableDeclaration, + isNullable); } while (firstDecl.name?.text != firstName && @@ -1294,19 +1481,23 @@ class Transformer { case TypeAliasDeclaration(type: final t): case EnumDeclaration(baseType: final t): final jsType = getJSTypeAlternative(t); - if (jsType != t && typeArg) return jsType; + if (jsType != t && typeArg) { + return jsType..isNullable = isNullable; + } } final asReferredType = decl.asReferredType( - (typeArguments ?? []) - .map((type) => _transformType(type, typeArg: true)) - .toList(), - ); + (typeArguments ?? []) + .map((type) => _transformType(type, typeArg: true)) + .toList(), + isNullable); if (asReferredType case ReferredDeclarationType(type: final type) when type is BuiltinType) { final jsType = getJSTypeAlternative(type); - if (jsType != type && typeArg) asReferredType.type = jsType; + if (jsType != type && typeArg) { + asReferredType.type = jsType..isNullable = isNullable; + } } return asReferredType; @@ -1320,12 +1511,16 @@ class Transformer { if (rest.singleOrNull?.part case final generic? when typeParams.any((t) => t.name == generic)) { final typeParam = typeParams.firstWhere((t) => t.name == generic); - return GenericType(name: typeParam.name, parent: decl); + return GenericType( + name: typeParam.name, parent: decl, isNullable: isNullable); } break; case final NamespaceDeclaration n: final searchForDeclRecursive = _searchForDeclRecursive(rest, symbol, - typeArguments: typeArguments, typeArg: typeArg, parent: n); + typeArguments: typeArguments, + typeArg: typeArg, + parent: n, + isNullable: isNullable); if (parent == null) { nodeMap.update(decl.id.toString(), (v) => n); } @@ -1344,7 +1539,8 @@ class Transformer { Type _getTypeFromTypeNode(TSTypeReferenceNode node, {List? typeArguments, bool typeArg = false, - bool isNotTypableDeclaration = false}) { + bool isNotTypableDeclaration = false, + bool isNullable = false}) { typeArguments ??= node.typeArguments?.toDart; final typeName = node.typeName; @@ -1362,8 +1558,8 @@ class Transformer { symbol = type?.aliasSymbol ?? type?.symbol; } - return _getTypeFromSymbol( - symbol, type, typeArguments, isNotTypableDeclaration, typeArg); + return _getTypeFromSymbol(symbol, type, typeArguments, + isNotTypableDeclaration, typeArg, isNullable); } /// Given a [TSSymbol] for a given TS node or declaration, and its associated @@ -1386,7 +1582,8 @@ class Transformer { TSType? type, List? typeArguments, bool isNotTypableDeclaration, - bool typeArg) { + bool typeArg, + bool isNullable) { final declarations = symbol!.getDeclarations()?.toDart ?? []; // get decl qualified name @@ -1401,7 +1598,8 @@ class Transformer { if (type?.isTypeParameter() ?? false) { // generic type - return GenericType(name: fullyQualifiedName.last.part); + return GenericType( + name: fullyQualifiedName.last.part, isNullable: isNullable); } // meaning others are imported @@ -1411,7 +1609,8 @@ class Transformer { final supportedType = BuiltinType.referred(firstName, typeParams: (typeArguments ?? []) .map((t) => getJSTypeAlternative(_transformType(t))) - .toList()); + .toList(), + isNullable: isNullable); if (supportedType case final resultType?) { return resultType; } @@ -1429,7 +1628,8 @@ class Transformer { typeParams: (typeArguments ?? []) .map(_transformType) .map(getJSTypeAlternative) - .toList()); + .toList(), + isNullable: isNullable); } // TODO(nikeokoronkwo): Update the version of typescript we are using @@ -1480,7 +1680,9 @@ class Transformer { case TypeAliasDeclaration(type: final t): case EnumDeclaration(baseType: final t): final jsType = getJSTypeAlternative(t); - if (jsType != t) return jsType; + if (jsType != t) { + return jsType..isNullable = isNullable; + } } } @@ -1489,12 +1691,15 @@ class Transformer { (typeArguments ?? []) .map((type) => _transformType(type, typeArg: true)) .toList(), + isNullable, relativePath?.replaceFirst('.d.ts', '.dart')); if (outputType case ReferredDeclarationType(type: final type) when type is BuiltinType && typeArg) { final jsType = getJSTypeAlternative(type); - if (jsType != type) outputType.type = jsType; + if (jsType != type) { + outputType.type = jsType..isNullable = isNullable; + } } return outputType; @@ -1509,14 +1714,16 @@ class Transformer { // let's just handle them before-hand if (type?.isTypeParameter() ?? false) { // generic type - return GenericType(name: fullyQualifiedName.last.part); + return GenericType( + name: fullyQualifiedName.last.part, isNullable: isNullable); } // recursiveness return _searchForDeclRecursive(fullyQualifiedName, symbol, typeArguments: typeArguments, typeArg: typeArg, - isNotTypableDeclaration: isNotTypableDeclaration); + isNotTypableDeclaration: isNotTypableDeclaration, + isNullable: isNullable); } else { // if import there and not this file, imported from specified file final importUrl = @@ -1540,7 +1747,9 @@ class Transformer { case TypeAliasDeclaration(type: final t): case EnumDeclaration(baseType: final t): final jsType = getJSTypeAlternative(t); - if (jsType != t) return jsType; + if (jsType != t) { + return jsType..isNullable = isNullable; + } } } @@ -1549,12 +1758,15 @@ class Transformer { (typeArguments ?? []) .map((type) => _transformType(type, typeArg: true)) .toList(), + isNullable, relativePath?.replaceFirst('.d.ts', '.dart')); if (outputType case ReferredDeclarationType(type: final type) when type is BuiltinType && typeArg) { final jsType = getJSTypeAlternative(type); - if (jsType != type) outputType.type = jsType; + if (jsType != type) { + outputType.type = jsType..isNullable = isNullable; + } } return outputType; @@ -1590,7 +1802,8 @@ class Transformer { @UnionOf([TSIdentifier, TSQualifiedName]) TSNode typeName, List? typeArguments, {bool typeArg = false, - bool isNotTypableDeclaration = false}) { + bool isNotTypableDeclaration = false, + bool isNullable = false}) { // union assertion assert(typeName.kind == TSSyntaxKind.Identifier || typeName.kind == TSSyntaxKind.QualifiedName); @@ -1598,7 +1811,7 @@ class Transformer { final symbol = typeChecker.getSymbolAtLocation(typeName); return _getTypeFromSymbol(symbol, typeChecker.getTypeOfSymbol(symbol!), - typeArguments, isNotTypableDeclaration, typeArg); + typeArguments, isNotTypableDeclaration, typeArg, isNullable); } /// Filters out the declarations generated from the [transform] function and @@ -1690,7 +1903,7 @@ class Transformer { void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) { switch (decl) { case final VariableDeclaration v: - if (v.type is! BuiltinType) filteredDeclarations.add(v.type); + filteredDeclarations.add(v.type); break; case final CallableDeclaration f: filteredDeclarations.addAll(getCallableDependencies(f)); @@ -1698,7 +1911,7 @@ class Transformer { case final EnumDeclaration _: break; case final TypeAliasDeclaration t: - if (decl.type is! BuiltinType) filteredDeclarations.add(t.type); + filteredDeclarations.add(t.type); break; case final TypeDeclaration t: for (final con in t.constructors) { @@ -1759,11 +1972,19 @@ class Transformer { case final HomogenousEnumType hu: filteredDeclarations.add(hu.declaration); break; + case final TupleType t: + filteredDeclarations.addAll({ + for (final t in t.types.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); case final UnionType u: filteredDeclarations.addAll({ for (final t in u.types.where((t) => t is! BuiltinType)) t.id.toString(): t }); + filteredDeclarations.add(u.declaration); + case final DeclarationType d: + filteredDeclarations.add(d.declaration); break; case BuiltinType(typeParams: final typeParams) when typeParams.isNotEmpty: diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index d43e0a52..acbd2112 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -90,8 +90,13 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind ThisType = TSSyntaxKind._(197); static const TSSyntaxKind TypeQuery = TSSyntaxKind._(186); static const TSSyntaxKind ParenthesizedType = TSSyntaxKind._(196); + static const TSSyntaxKind TupleType = TSSyntaxKind._(189); + static const TSSyntaxKind NamedTupleMember = TSSyntaxKind._(202); + static const TSSyntaxKind TypeLiteral = TSSyntaxKind._(187); + static const TSSyntaxKind FunctionType = TSSyntaxKind._(184); + static const TSSyntaxKind ConstructorType = TSSyntaxKind._(185); - /// Other + // Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); static const TSSyntaxKind QualifiedName = TSSyntaxKind._(166); static const TSSyntaxKind PropertyAccessExpression = TSSyntaxKind._(211); @@ -184,6 +189,47 @@ extension type TSParenthesizedTypeNode._(JSObject _) implements TSTypeNode { external TSTypeNode get type; } +@JS('TupleTypeNode') +extension type TSTupleTypeNode._(JSObject _) implements TSTypeNode { + external TSNodeArray get elements; +} + +@JS('NamedTupleMember') +extension type TSNamedTupleMember._(JSObject _) + implements TSTypeNode, TSDeclaration { + external TSToken? get dotDotDotToken; + external TSIdentifier get name; + external TSToken? get questionToken; + external TSTypeNode get type; +} + +@JS('TypeLiteralNode') +extension type TSTypeLiteralNode._(JSObject _) + implements TSTypeNode, TSDeclaration { + external TSNodeArray get members; +} + +@JS('FunctionOrConstructorTypeNodeBase') +extension type TSFunctionOrConstructorTypeNodeBase._(JSObject _) + implements TSTypeNode, TSSignatureDeclarationBase { + external TSTypeNode get type; +} + +@JS('FunctionTypeNode') +extension type TSFunctionTypeNode._(JSObject _) + implements TSFunctionOrConstructorTypeNodeBase { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.FunctionType; +} + +@JS('ConstructorTypeNode') +extension type TSConstructorTypeNode._(JSObject _) + implements TSFunctionOrConstructorTypeNodeBase { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.ConstructorType; + external TSNodeArray? get modifiers; +} + @JS('Expression') extension type TSExpression._(JSObject _) implements TSNode {} diff --git a/web_generator/lib/src/utils/case.dart b/web_generator/lib/src/utils/case.dart new file mode 100644 index 00000000..652494b8 --- /dev/null +++ b/web_generator/lib/src/utils/case.dart @@ -0,0 +1,7 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +String uppercaseFirstLetter(String word) { + return word.replaceFirst(word[0], word[0].toUpperCase()); +} diff --git a/web_generator/pubspec.yaml b/web_generator/pubspec.yaml index ccd0d045..f03237a9 100644 --- a/web_generator/pubspec.yaml +++ b/web_generator/pubspec.yaml @@ -11,6 +11,9 @@ dependencies: analyzer: ^7.4.0 args: ^2.4.0 code_builder: ^4.10.0 + collection: ^1.19.1 + convert: ^3.1.2 + crypto: ^3.0.6 dart_flutter_team_lints: ^3.0.0 dart_style: ^3.0.0 io: ^1.0.4 diff --git a/web_generator/test/integration/interop_gen/_tuples.dart b/web_generator/test/integration/interop_gen/_tuples.dart new file mode 100644 index 00000000..ff7ac1f6 --- /dev/null +++ b/web_generator/test/integration/interop_gen/_tuples.dart @@ -0,0 +1,35 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +extension type JSTuple2._( + _i1.JSArray<_i1.JSAny?> _) implements _i1.JSArray<_i1.JSAny?> { + A get $1 => (_[0] as A); + + B get $2 => (_[1] as B); + + set $1(A newValue) => _[0] = newValue; + + set $2(B newValue) => _[1] = newValue; +} +extension type JSTuple4._(_i1.JSArray<_i1.JSAny?> _) + implements _i1.JSArray<_i1.JSAny?> { + A get $1 => (_[0] as A); + + B get $2 => (_[1] as B); + + C get $3 => (_[2] as C); + + D get $4 => (_[3] as D); + + set $1(A newValue) => _[0] = newValue; + + set $2(B newValue) => _[1] = newValue; + + set $3(C newValue) => _[2] = newValue; + + set $4(D newValue) => _[3] = newValue; +} diff --git a/web_generator/test/integration/interop_gen/classes_expected.dart b/web_generator/test/integration/interop_gen/classes_expected.dart index 244db760..7bb8f55a 100644 --- a/web_generator/test/integration/interop_gen/classes_expected.dart +++ b/web_generator/test/integration/interop_gen/classes_expected.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -276,7 +278,7 @@ extension type EpahsImpl._(_i1.JSObject _) implements Epahs { external EpahsImpl( String name, [ - AnonymousUnion$1? type, + AnonymousUnion_1113974? type, ]); external factory EpahsImpl.$1(Epahs config); @@ -294,17 +296,20 @@ extension type EpahsImpl._(_i1.JSObject _) @_i2.redeclare external double area(); @_i1.JS('area') - external String area$1(AnonymousUnion unit); + external String area$1(AnonymousUnion_1594664 unit); external static EpahsImpl getById(String id); @_i1.JS('toString') external String toString$(); } -extension type const AnonymousUnion$1._(String _) { - static const AnonymousUnion$1 circle = AnonymousUnion$1._('circle'); +extension type const AnonymousUnion_1113974._(String _) { + static const AnonymousUnion_1113974 circle = + AnonymousUnion_1113974._('circle'); - static const AnonymousUnion$1 rectangle = AnonymousUnion$1._('rectangle'); + static const AnonymousUnion_1113974 rectangle = + AnonymousUnion_1113974._('rectangle'); - static const AnonymousUnion$1 polygon = AnonymousUnion$1._('polygon'); + static const AnonymousUnion_1113974 polygon = + AnonymousUnion_1113974._('polygon'); } extension type Epahs._(_i1.JSObject _) implements _i1.JSObject { @@ -313,11 +318,11 @@ extension type Epahs._(_i1.JSObject _) external String get id; external double area(); @_i1.JS('area') - external String area$1(AnonymousUnion unit); + external String area$1(AnonymousUnion_1594664 unit); external _i1.JSFunction? get onUpdate; } -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion cm2 = AnonymousUnion._('cm2'); +extension type const AnonymousUnion_1594664._(String _) { + static const AnonymousUnion_1594664 cm2 = AnonymousUnion_1594664._('cm2'); - static const AnonymousUnion in2 = AnonymousUnion._('in2'); + static const AnonymousUnion_1594664 in2 = AnonymousUnion_1594664._('in2'); } diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart index 8a495184..01e5be79 100644 --- a/web_generator/test/integration/interop_gen/enum_expected.dart +++ b/web_generator/test/integration/interop_gen/enum_expected.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -71,7 +73,7 @@ extension type const HttpMethod._(String _) { static const HttpMethod DELETE = HttpMethod._('DELETE'); } -extension type BooleanLike._(_i1.JSAny? _) { +extension type BooleanLike._(_i1.JSAny _) { static final BooleanLike No = BooleanLike._(0.toJS); static final BooleanLike Yes = BooleanLike._('YES'.toJS); @@ -98,7 +100,7 @@ extension type MathConstants._(_i1.JSNumber _) { external static MathConstants Length; } -extension type SomeRandomEnumValues._(_i1.JSAny? _) { +extension type SomeRandomEnumValues._(_i1.JSAny _) { static final SomeRandomEnumValues moment = SomeRandomEnumValues._(2.toJS); static final SomeRandomEnumValues true$ = SomeRandomEnumValues._(6.28.toJS); @@ -111,59 +113,64 @@ extension type SomeRandomEnumValues._(_i1.JSAny? _) { @_i1.JS() external Permissions get userPermissions; @_i1.JS() -external AnonymousUnion currentTheme; +external AnonymousUnion_3616711 currentTheme; @_i1.JS() -external AnonymousUnion$1 buttonState; +external AnonymousUnion_2355602 buttonState; @_i1.JS() -external AnonymousUnion$2 retriesLeft; +external AnonymousUnion_7456409 retriesLeft; @_i1.JS() -external AnonymousUnion$3? get direction; +external AnonymousUnion_1008525? get direction; @_i1.JS() -external AnonymousUnion$4 get someUnionEnum; +external AnonymousUnion_4522673 get someUnionEnum; @_i1.JS() external bool get myBooleanEnum; -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion light = AnonymousUnion._('light'); +extension type const AnonymousUnion_3616711._(String _) { + static const AnonymousUnion_3616711 light = AnonymousUnion_3616711._('light'); - static const AnonymousUnion dark = AnonymousUnion._('dark'); + static const AnonymousUnion_3616711 dark = AnonymousUnion_3616711._('dark'); - static const AnonymousUnion system = AnonymousUnion._('system'); + static const AnonymousUnion_3616711 system = + AnonymousUnion_3616711._('system'); } -extension type const AnonymousUnion$1._(String _) { - static const AnonymousUnion$1 default$ = AnonymousUnion$1._('default'); +extension type const AnonymousUnion_2355602._(String _) { + static const AnonymousUnion_2355602 default$ = + AnonymousUnion_2355602._('default'); - static const AnonymousUnion$1 hovered = AnonymousUnion$1._('hovered'); + static const AnonymousUnion_2355602 hovered = + AnonymousUnion_2355602._('hovered'); - static const AnonymousUnion$1 pressed = AnonymousUnion$1._('pressed'); + static const AnonymousUnion_2355602 pressed = + AnonymousUnion_2355602._('pressed'); - static const AnonymousUnion$1 disabled = AnonymousUnion$1._('disabled'); + static const AnonymousUnion_2355602 disabled = + AnonymousUnion_2355602._('disabled'); } -extension type const AnonymousUnion$2._(num _) { - static const AnonymousUnion$2 $0 = AnonymousUnion$2._(0); +extension type const AnonymousUnion_7456409._(num _) { + static const AnonymousUnion_7456409 $0 = AnonymousUnion_7456409._(0); - static const AnonymousUnion$2 $1 = AnonymousUnion$2._(1); + static const AnonymousUnion_7456409 $1 = AnonymousUnion_7456409._(1); - static const AnonymousUnion$2 $2 = AnonymousUnion$2._(2); + static const AnonymousUnion_7456409 $2 = AnonymousUnion_7456409._(2); - static const AnonymousUnion$2 $3 = AnonymousUnion$2._(3); + static const AnonymousUnion_7456409 $3 = AnonymousUnion_7456409._(3); } -extension type const AnonymousUnion$3._(String _) { - static const AnonymousUnion$3 N = AnonymousUnion$3._('N'); +extension type const AnonymousUnion_1008525._(String _) { + static const AnonymousUnion_1008525 N = AnonymousUnion_1008525._('N'); - static const AnonymousUnion$3 S = AnonymousUnion$3._('S'); + static const AnonymousUnion_1008525 S = AnonymousUnion_1008525._('S'); - static const AnonymousUnion$3 E = AnonymousUnion$3._('E'); + static const AnonymousUnion_1008525 E = AnonymousUnion_1008525._('E'); - static const AnonymousUnion$3 W = AnonymousUnion$3._('W'); + static const AnonymousUnion_1008525 W = AnonymousUnion_1008525._('W'); } -extension type const AnonymousUnion$4._(num _) { - static const AnonymousUnion$4 $2 = AnonymousUnion$4._(2); +extension type const AnonymousUnion_4522673._(num _) { + static const AnonymousUnion_4522673 $2 = AnonymousUnion_4522673._(2); - static const AnonymousUnion$4 $4 = AnonymousUnion$4._(4); + static const AnonymousUnion_4522673 $4 = AnonymousUnion_4522673._(4); - static const AnonymousUnion$4 $6 = AnonymousUnion$4._(6); + static const AnonymousUnion_4522673 $6 = AnonymousUnion_4522673._(6); - static const AnonymousUnion$4 $8 = AnonymousUnion$4._(8); + static const AnonymousUnion_4522673 $8 = AnonymousUnion_4522673._(8); - static const AnonymousUnion$4 $10 = AnonymousUnion$4._(10); + static const AnonymousUnion_4522673 $10 = AnonymousUnion_4522673._(10); } diff --git a/web_generator/test/integration/interop_gen/interfaces_expected.dart b/web_generator/test/integration/interop_gen/interfaces_expected.dart index 41262e73..a8a7ffaa 100644 --- a/web_generator/test/integration/interop_gen/interfaces_expected.dart +++ b/web_generator/test/integration/interop_gen/interfaces_expected.dart @@ -1,10 +1,12 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; extension type ILogger._(_i1.JSObject _) implements _i1.JSObject { - external AnonymousUnion? level; + external AnonymousUnion_1584724? level; external String get name; external void log(String message); @@ -66,14 +68,14 @@ external Dictionary get dict; external LinkedList get rootList; @_i1.JS() external Comparator<_i1.JSNumber> get compareNumbers; -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion debug = AnonymousUnion._('debug'); +extension type const AnonymousUnion_1584724._(String _) { + static const AnonymousUnion_1584724 debug = AnonymousUnion_1584724._('debug'); - static const AnonymousUnion info = AnonymousUnion._('info'); + static const AnonymousUnion_1584724 info = AnonymousUnion_1584724._('info'); - static const AnonymousUnion warn = AnonymousUnion._('warn'); + static const AnonymousUnion_1584724 warn = AnonymousUnion_1584724._('warn'); - static const AnonymousUnion error = AnonymousUnion._('error'); + static const AnonymousUnion_1584724 error = AnonymousUnion_1584724._('error'); } extension type LinkedList._(_i1.JSObject _) implements _i1.JSObject { external LinkedList next(); diff --git a/web_generator/test/integration/interop_gen/namespaces_expected.dart b/web_generator/test/integration/interop_gen/namespaces_expected.dart index 0194efb9..dddc3402 100644 --- a/web_generator/test/integration/interop_gen/namespaces_expected.dart +++ b/web_generator/test/integration/interop_gen/namespaces_expected.dart @@ -1,5 +1,6 @@ // ignore_for_file: camel_case_types, constant_identifier_names -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -100,7 +101,7 @@ extension type Security_IAuthToken._(_i1.JSObject _) implements _i1.JSObject { extension type Security_AuthService._(_i1.JSObject _) implements _i1.JSObject { external Security_AuthService(); - external Security_IAuthToken login( + external Security_IAuthToken? login( String username, String password, ); @@ -108,7 +109,7 @@ extension type Security_AuthService._(_i1.JSObject _) implements _i1.JSObject { @_i1.JS('Data.IRepository') extension type Data_IRepository._(_i1.JSObject _) implements _i1.JSObject { - external T findById(num id); + external T? findById(num id); external _i1.JSArray findAll(); external void save(T entity); } @@ -292,7 +293,7 @@ extension type EnterpriseApp_DataServices_ProductService._(_i1.JSObject _) external void save(EnterpriseApp_Models_Product item); external void add(EnterpriseApp_Models_Product product); @_i1.JS('get') - external EnterpriseApp_Models_Product get$(num id); + external EnterpriseApp_Models_Product get$(AnonymousUnion_1467782 id); @_i2.redeclare external _i1.JSArray getAll(); } @@ -308,3 +309,8 @@ extension type EnterpriseApp_UI_Components._(_i1.JSObject _) external static void renderUserList( _i1.JSArray users); } +extension type AnonymousUnion_1467782._(_i1.JSAny _) implements _i1.JSAny { + String get asString => (_ as _i1.JSString).toDart; + + double get asDouble => (_ as _i1.JSNumber).toDartDouble; +} diff --git a/web_generator/test/integration/interop_gen/namespaces_input.d.ts b/web_generator/test/integration/interop_gen/namespaces_input.d.ts index 7b1d3b80..724223f3 100644 --- a/web_generator/test/integration/interop_gen/namespaces_input.d.ts +++ b/web_generator/test/integration/interop_gen/namespaces_input.d.ts @@ -34,7 +34,7 @@ export declare namespace Security { */ class AuthService { private logs; - login(username: string, password: string): IAuthToken; + login(username: string, password: string): IAuthToken | undefined; } } export declare namespace Data { @@ -43,7 +43,7 @@ export declare namespace Data { * T can be a class from another namespace, like Models.User. */ interface IRepository { - findById(id: number): T; + findById(id: number): T | undefined; findAll(): T[]; save(entity: T): void; } @@ -133,7 +133,7 @@ export declare namespace EnterpriseApp { save(item: Models.Product): void; private products; add(product: Models.Product): void; - get(id: number): Models.Product; + get(id: string | number): Models.Product; getAll(): Models.Product[]; } } diff --git a/web_generator/test/integration/interop_gen/project/output/b.dart b/web_generator/test/integration/interop_gen/project/output/b.dart index 46e3a010..7d6edfd8 100644 --- a/web_generator/test/integration/interop_gen/project/output/b.dart +++ b/web_generator/test/integration/interop_gen/project/output/b.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -248,7 +250,7 @@ extension type EpahsImpl._(_i1.JSObject _) implements Epahs { external EpahsImpl( String name, [ - AnonymousUnion$1? type, + AnonymousUnion_1113974? type, ]); external factory EpahsImpl.$1(Epahs config); @@ -266,18 +268,21 @@ extension type EpahsImpl._(_i1.JSObject _) @_i2.redeclare external double area(); @_i1.JS('area') - external String area$1(AnonymousUnion unit); + external String area$1(AnonymousUnion_1594664 unit); external static EpahsImpl getById(String id); @_i1.JS('toString') external String toString$(); } extension type Point._(_i1.JSObject _) implements _i1.JSObject {} -extension type const AnonymousUnion$1._(String _) { - static const AnonymousUnion$1 circle = AnonymousUnion$1._('circle'); +extension type const AnonymousUnion_1113974._(String _) { + static const AnonymousUnion_1113974 circle = + AnonymousUnion_1113974._('circle'); - static const AnonymousUnion$1 rectangle = AnonymousUnion$1._('rectangle'); + static const AnonymousUnion_1113974 rectangle = + AnonymousUnion_1113974._('rectangle'); - static const AnonymousUnion$1 polygon = AnonymousUnion$1._('polygon'); + static const AnonymousUnion_1113974 polygon = + AnonymousUnion_1113974._('polygon'); } extension type Epahs._(_i1.JSObject _) implements _i1.JSObject { @@ -286,11 +291,11 @@ extension type Epahs._(_i1.JSObject _) external String get id; external double area(); @_i1.JS('area') - external String area$1(AnonymousUnion unit); + external String area$1(AnonymousUnion_1594664 unit); external _i1.JSFunction? get onUpdate; } -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion cm2 = AnonymousUnion._('cm2'); +extension type const AnonymousUnion_1594664._(String _) { + static const AnonymousUnion_1594664 cm2 = AnonymousUnion_1594664._('cm2'); - static const AnonymousUnion in2 = AnonymousUnion._('in2'); + static const AnonymousUnion_1594664 in2 = AnonymousUnion_1594664._('in2'); } diff --git a/web_generator/test/integration/interop_gen/project/output/c.dart b/web_generator/test/integration/interop_gen/project/output/c.dart index 5179b4be..f27d621c 100644 --- a/web_generator/test/integration/interop_gen/project/output/c.dart +++ b/web_generator/test/integration/interop_gen/project/output/c.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -11,7 +13,7 @@ extension type Comparator._(_i1.JSObject _) ); } extension type ILogger._(_i1.JSObject _) implements _i1.JSObject { - external AnonymousUnion? level; + external AnonymousUnion_1584724? level; external String get name; external void log(String message); @@ -66,14 +68,14 @@ external Dictionary get dict; external LinkedList get rootList; @_i1.JS() external Comparator<_i1.JSNumber> get compareNumbers; -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion debug = AnonymousUnion._('debug'); +extension type const AnonymousUnion_1584724._(String _) { + static const AnonymousUnion_1584724 debug = AnonymousUnion_1584724._('debug'); - static const AnonymousUnion info = AnonymousUnion._('info'); + static const AnonymousUnion_1584724 info = AnonymousUnion_1584724._('info'); - static const AnonymousUnion warn = AnonymousUnion._('warn'); + static const AnonymousUnion_1584724 warn = AnonymousUnion_1584724._('warn'); - static const AnonymousUnion error = AnonymousUnion._('error'); + static const AnonymousUnion_1584724 error = AnonymousUnion_1584724._('error'); } extension type LinkedList._(_i1.JSObject _) implements _i1.JSObject { external LinkedList next(); diff --git a/web_generator/test/integration/interop_gen/ts_typing_expected.dart b/web_generator/test/integration/interop_gen/ts_typing_expected.dart index 65e7fe92..9962ca38 100644 --- a/web_generator/test/integration/interop_gen/ts_typing_expected.dart +++ b/web_generator/test/integration/interop_gen/ts_typing_expected.dart @@ -1,13 +1,22 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; +import 'package:web/web.dart' as _i3; + +import '_tuples.dart' as _i2; + @_i1.JS() external String myFunction(String param); @_i1.JS() external String myEnclosingFunction(_i1.JSFunction func); @_i1.JS() +external _i1.JSArray> + indexedArray(_i1.JSArray arr); +@_i1.JS() external String get myString; @_i1.JS() external _i1.JSArray<_i1.JSNumber> get myNumberArray; @@ -40,7 +49,202 @@ external _i1.JSFunction get myEnclosingFunctionAlias; external ComposedType get myComposedType; @_i1.JS() external ComposedType<_i1.JSString> get myComposedMyString; +@_i1.JS() +external AnonymousUnion_5411652 get myUnion; +@_i1.JS() +external AnonymousUnion_5411652 get myCloneUnion; +@_i1.JS() +external AnonymousUnion_3781934 get mySecondUnion; +@_i1.JS() +external bool? get booleanOrUndefined; +@_i1.JS() +external AnonymousUnion_6216705? get image; +@_i1.JS() +external _i2.JSTuple2<_i1.JSString, _i1.JSNumber> get myTuple; +@_i1.JS() +external _i2.JSTuple2<_i1.JSString, _i1.JSString> get mySecondTuple; +@_i1.JS() +external _i2.JSTuple2<_i1.JSString, _i1.JSString> get myCloneTuple; +@_i1.JS() +external _i2.JSTuple4<_i1.JSString, _i1.JSNumber, _i1.JSBoolean, _i1.JSSymbol> + get typesAsTuple; +@_i1.JS() +external AnonymousUnion_7503220 get eightOrSixteen; +@_i1.JS() +external AnonymousType_2194029 get randomNonTypedProduct; +@_i1.JS() +external AnonymousType_1358595 get config; +extension type MyProduct._(_i1.JSObject _) implements Product { + external MyProduct( + num id, + String name, + num price, + ); + + external double id; + + external String name; + + external double price; +} +@_i1.JS() +external AnonymousUnion_1189263 get responseObject; +@_i1.JS() +external _AnonymousConstructor_1059824 get productConstr; +@_i1.JS() +external _AnonymousFunction_1331744 get createDiscountCalculator; +@_i1.JS() +external _AnonymousFunction_7147484 get applyDiscount; +@_i1.JS() +external _i1.JSArray get shoppingCart; +@_i1.JS() +external _AnonymousFunction_2181528 get createLogger; +@_i1.JS() +external _AnonymousFunction_1707607 get appLogger; +extension type AnonymousType_9143117._(_i1.JSObject _) + implements _i1.JSObject { + external AnonymousType_9143117({ + double id, + T value, + }); + + external double id; + + external T value; +} extension type ComposedType._(_i1.JSObject _) implements _i1.JSObject { external T enclosed; } +extension type AnonymousUnion_5411652._(_i1.JSAny _) implements _i1.JSAny { + bool get asBool => (_ as _i1.JSBoolean).toDart; + + String get asString => (_ as _i1.JSString).toDart; +} +extension type AnonymousUnion_3781934._(_i1.JSAny _) implements _i1.JSAny { + double get asDouble => (_ as _i1.JSNumber).toDartDouble; + + String get asString => (_ as _i1.JSString).toDart; + + MyEnum get asMyEnum => MyEnum._((_ as _i1.JSNumber).toDartInt); + + ComposedType get asComposedType => (_ as ComposedType); +} +extension type AnonymousUnion_6216705._(_i1.JSAny _) implements _i1.JSAny { + String get asString => (_ as _i1.JSString).toDart; + + _i3.URL get asURL => (_ as _i3.URL); +} +extension type AnonymousUnion_7503220._(_i1.JSTypedArray _) + implements _i1.JSTypedArray { + _i1.JSUint8Array get asJSUint8Array => (_ as _i1.JSUint8Array); + + _i1.JSUint16Array get asJSUint16Array => (_ as _i1.JSUint16Array); +} +extension type AnonymousType_2194029._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_2194029({ + double id, + String name, + double price, + }); + + external double id; + + external String name; + + external double price; +} +extension type AnonymousType_1358595._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1358595({ + double discountRate, + double taxRate, + }); + + external double discountRate; + + external double taxRate; +} +typedef Product = AnonymousType_2194029; +extension type AnonymousUnion_1189263._(_i1.JSObject _) + implements _i1.JSObject { + AnonymousType_2773310 get asAnonymousType_2773310 => + (_ as AnonymousType_2773310); + + AnonymousType_1487785 get asAnonymousType_1487785 => + (_ as AnonymousType_1487785); +} +extension type AnonymousType_2773310._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_2773310({ + String id, + _i1.JSAny? value, + }); + + external String id; + + external _i1.JSAny? value; +} +extension type AnonymousType_1487785._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1487785({ + String id, + String error, + _i1.JSAny? data, + }); + + external String id; + + external String error; + + external _i1.JSAny? data; +} +extension type _AnonymousConstructor_1059824._(_i1.JSFunction _) + implements _i1.JSFunction { + Product call( + num id, + String name, + num price, + ) => + Product( + id: id.toDouble(), + name: name, + price: price.toDouble(), + ); +} +extension type _AnonymousFunction_1331744._(_i1.JSFunction _) + implements _i1.JSFunction { + external _AnonymousFunction_7147484 call(num rate); +} +extension type _AnonymousFunction_7147484._(_i1.JSFunction _) + implements _i1.JSFunction { + external double call(num originalPrice); +} +extension type AnonymousType_5780756._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_5780756({ + double calculatedPrice, + _AnonymousFunction_4113003 displayInfo, + double id, + String name, + double price, + }); + + external double calculatedPrice; + + external _AnonymousFunction_4113003 displayInfo; + + external double id; + + external String name; + + external double price; +} +extension type _AnonymousFunction_4113003._(_i1.JSFunction _) + implements _i1.JSFunction { + external void call(); +} +extension type _AnonymousFunction_2181528._(_i1.JSFunction _) + implements _i1.JSFunction { + external _AnonymousFunction_1707607 call(String prefix); +} +extension type _AnonymousFunction_1707607._(_i1.JSFunction _) + implements _i1.JSFunction { + external void call(String message); +} diff --git a/web_generator/test/integration/interop_gen/ts_typing_input.d.ts b/web_generator/test/integration/interop_gen/ts_typing_input.d.ts index e665d780..b47a6cce 100644 --- a/web_generator/test/integration/interop_gen/ts_typing_input.d.ts +++ b/web_generator/test/integration/interop_gen/ts_typing_input.d.ts @@ -17,9 +17,59 @@ export declare const myEnumValue2: typeof MyEnum; export declare function myFunction(param: string): string; export declare let myFunctionAlias: typeof myFunction; export declare let myFunctionAlias2: typeof myFunctionAlias; -/** @todo [@nikeokoronkwo] support var declarations as well as var statements */ // export declare let myPreClone: typeof myComposedType; export declare function myEnclosingFunction(func: typeof myFunction): string; export declare const myEnclosingFunctionAlias: typeof myEnclosingFunction; export declare const myComposedType: ComposedType; export declare const myComposedMyString: ComposedType; +export declare const myUnion: boolean | string; +export declare const myCloneUnion: boolean | string; +export declare const mySecondUnion: number | string | MyEnum | ComposedType; +export declare const booleanOrUndefined: boolean | undefined; +export declare const image: string | URL | null; +export declare const myTuple: [string, number]; +export declare const mySecondTuple: [string, string]; +export declare const myCloneTuple: [string, string]; +export declare const typesAsTuple: [string, number, boolean, symbol]; +export declare const eightOrSixteen: Uint8Array | Uint16Array; +type Product = { + id: number; + name: string; + price: number; +}; +export declare const randomNonTypedProduct: { + id: number; + name: string; + price: number; +}; +export declare const config: { + discountRate: number; + taxRate: number; +}; +export declare class MyProduct implements Product { + id: number; + name: string; + price: number; + constructor(id: number, name: string, price: number); +} +export function indexedArray(arr: T[]): { id: number, value: T }[]; +export const responseObject: { + id: string; + value: any; +} | { + id: string; + error: string; + data: any; +} +export declare const productConstr: new (id: number, name: string, price: number) => Product; +export declare const createDiscountCalculator: (rate: number) => (originalPrice: number) => number; +export declare const applyDiscount: (originalPrice: number) => number; +export declare const shoppingCart: { + calculatedPrice: number; + displayInfo: () => void; + id: number; + name: string; + price: number; +}[]; +export declare const createLogger: (prefix: string) => (message: string) => void; +export declare const appLogger: (message: string) => void; diff --git a/web_generator/test/integration/interop_gen/typealias_expected.dart b/web_generator/test/integration/interop_gen/typealias_expected.dart index f5aa6ce7..44e22f80 100644 --- a/web_generator/test/integration/interop_gen/typealias_expected.dart +++ b/web_generator/test/integration/interop_gen/typealias_expected.dart @@ -1,5 +1,6 @@ // ignore_for_file: camel_case_types, constant_identifier_names -// ignore_for_file: non_constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -17,14 +18,25 @@ external PromisedArray<_i1.JSString, _i1.JSArray<_i1.JSString>> fetchNames(); typedef IsActive = bool; @_i1.JS() external String isUserActive(IsActive status); +extension type JSTuple2._( + _i1.JSArray<_i1.JSAny?> _) implements _i1.JSArray<_i1.JSAny?> { + A get $1 => (_[0] as A); + + B get $2 => (_[1] as B); + + set $1(A newValue) => _[0] = newValue; + + set $2(B newValue) => _[1] = newValue; +} +typedef NameAndAge = JSTuple2<_i1.JSString, _i1.JSNumber>; typedef Username = String; typedef Age = double; typedef Tags = _i1.JSArray<_i1.JSString>; typedef List = _i1.JSArray; typedef Box = _i1.JSArray<_i1.JSArray>; typedef Logger = LoggerType; -typedef Direction = AnonymousUnion; -typedef Method = AnonymousUnion$1; +typedef Direction = AnonymousUnion_1008525; +typedef Method = AnonymousUnion_1614079; typedef Planet = Space_Planet; @_i1.JS() external LoggerContainer<_i1.JSNumber> get loggerContainers; @@ -53,27 +65,29 @@ extension type const LoggerType._(int _) { static const LoggerType Other = LoggerType._(4); } -extension type const AnonymousUnion._(String _) { - static const AnonymousUnion N = AnonymousUnion._('N'); +extension type const AnonymousUnion_1008525._(String _) { + static const AnonymousUnion_1008525 N = AnonymousUnion_1008525._('N'); - static const AnonymousUnion S = AnonymousUnion._('S'); + static const AnonymousUnion_1008525 S = AnonymousUnion_1008525._('S'); - static const AnonymousUnion E = AnonymousUnion._('E'); + static const AnonymousUnion_1008525 E = AnonymousUnion_1008525._('E'); - static const AnonymousUnion W = AnonymousUnion._('W'); + static const AnonymousUnion_1008525 W = AnonymousUnion_1008525._('W'); } -extension type const AnonymousUnion$1._(String _) { - static const AnonymousUnion$1 GET = AnonymousUnion$1._('GET'); +extension type const AnonymousUnion_1614079._(String _) { + static const AnonymousUnion_1614079 GET = AnonymousUnion_1614079._('GET'); - static const AnonymousUnion$1 POST = AnonymousUnion$1._('POST'); + static const AnonymousUnion_1614079 POST = AnonymousUnion_1614079._('POST'); - static const AnonymousUnion$1 PUT = AnonymousUnion$1._('PUT'); + static const AnonymousUnion_1614079 PUT = AnonymousUnion_1614079._('PUT'); - static const AnonymousUnion$1 DELETE = AnonymousUnion$1._('DELETE'); + static const AnonymousUnion_1614079 DELETE = + AnonymousUnion_1614079._('DELETE'); - static const AnonymousUnion$1 PATCH = AnonymousUnion$1._('PATCH'); + static const AnonymousUnion_1614079 PATCH = AnonymousUnion_1614079._('PATCH'); - static const AnonymousUnion$1 OPTIONS = AnonymousUnion$1._('OPTIONS'); + static const AnonymousUnion_1614079 OPTIONS = + AnonymousUnion_1614079._('OPTIONS'); } @_i1.JS('Space.Planet') extension type Space_Planet._(_i1.JSObject _) implements _i1.JSObject { diff --git a/web_generator/test/integration/interop_gen/typealias_input.d.ts b/web_generator/test/integration/interop_gen/typealias_input.d.ts index 52e2b7ae..debe1e87 100644 --- a/web_generator/test/integration/interop_gen/typealias_input.d.ts +++ b/web_generator/test/integration/interop_gen/typealias_input.d.ts @@ -11,6 +11,7 @@ declare namespace Space { } const earth: Planet; } +export type NameAndAge = [string, number]; export type Username = string; export type Age = number; export type IsActive = boolean; diff --git a/web_generator/test/integration/interop_gen/web_types_expected.dart b/web_generator/test/integration/interop_gen/web_types_expected.dart index 3de6a1d5..ff9c5d47 100644 --- a/web_generator/test/integration/interop_gen/web_types_expected.dart +++ b/web_generator/test/integration/interop_gen/web_types_expected.dart @@ -1,4 +1,6 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -13,7 +15,8 @@ external _i2.URL generateUrl(String path); external _i1.JSPromise<_i2.WebGLBuffer> convertToWebGL( _i1.JSArrayBuffer buffer); @_i1.JS() -external String getHTMLElementContent(T element); +external AnonymousType_7051203 + getHTMLElementContent(T element); @_i1.JS() external void handleButtonClick(_i2.MouseEvent event); @_i1.JS() @@ -28,6 +31,15 @@ external _i1.JSAny? handleEvents( _i2.Event event, _i1.JSArray onCallbacks, ); +extension type ElementStamp._(_i1.JSObject _) + implements _i1.JSObject { + external String id; + + external AnonymousUnion_1506805 stampType; + + external T get target; + external Date get stampedAt; +} @_i1.JS() external _i2.CustomEvent get myCustomEvent; @_i1.JS() @@ -36,6 +48,23 @@ external _i2.ShadowRoot myShadowRoot; external _i2.HTMLButtonElement get button; @_i1.JS() external _i2.HTMLDivElement get output; +extension type AnonymousType_7051203._(_i1.JSObject _) + implements _i1.JSObject { + external AnonymousType_7051203({ + AnonymousUnion_1500406? ref, + _i2.HTMLElement parent, + }); + + external AnonymousUnion_1500406? ref; + + external _i2.HTMLElement parent; +} +extension type AnonymousUnion_1500406._(_i1.JSAny _) + implements _i1.JSAny { + T get asT => (_ as T); + + String get asString => (_ as _i1.JSString).toDart; +} extension type HTMLTransformFunc._(_i1.JSObject _) implements _i1.JSObject { external R call(T element); @@ -43,3 +72,92 @@ extension type HTMLTransformFunc { interface EventManipulationFunc { (event: Event): any; } -interface ElementStamp { +export interface ElementStamp { readonly target: T; readonly stampedAt: Date; id: string; @@ -16,11 +16,14 @@ declare let myURL: URL; export declare function handleMouseEvent(event: MouseEvent): void; export declare function generateUrl(path: string): URL; export declare function convertToWebGL(buffer: ArrayBuffer): Promise; -export declare function getHTMLElementContent(element: T): string; +export declare function getHTMLElementContent(element: T): { + ref: T | string | null; + parent: HTMLElement; +}; export declare const button: HTMLButtonElement; declare const input: HTMLInputElement; export declare const output: HTMLDivElement; export declare function handleButtonClick(event: MouseEvent): void; export declare function handleInputChange(event: Event): void; -export declare function transformElements(el: HTMLElement[], transformer: HTMLTransformFunc); +export declare function transformElements(el: HTMLElement[], transformer: HTMLTransformFunc): any; export declare function handleEvents(event: Event, onCallbacks: EventManipulationFunc[]): any; diff --git a/web_generator/test/type_map_test.dart b/web_generator/test/type_map_test.dart new file mode 100644 index 00000000..24835b75 --- /dev/null +++ b/web_generator/test/type_map_test.dart @@ -0,0 +1,205 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('node') +library; + +import 'package:test/test.dart'; +import 'package:web_generator/src/ast/base.dart'; +import 'package:web_generator/src/ast/builtin.dart'; +import 'package:web_generator/src/ast/declarations.dart'; +import 'package:web_generator/src/ast/types.dart'; +import 'package:web_generator/src/interop_gen/namer.dart'; +import 'package:web_generator/src/interop_gen/sub_type.dart'; + +void main() { + group('Type Map Test', () { + test('Builtin Types Test', () { + final anyTypeMap = getTypeHierarchy(BuiltinType.anyType); + expect(anyTypeMap.nodes, isEmpty, + reason: 'JSAny should have no ancestors'); + + final booleanTypeMap = + getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.boolean)); + expect(booleanTypeMap.nodes, isNotEmpty, + reason: 'JSBoolean inherits JSAny'); + expect(booleanTypeMap.nodes.length, equals(1), + reason: 'JSBoolean only inherits JSAny'); + + final booleanToJSObjectLookup = booleanTypeMap.lookup('JSObject'); + expect(booleanToJSObjectLookup, isNull, + reason: 'JSBoolean does not inherit JSObject'); + + final booleanToJSAnyLookup = booleanTypeMap.lookup('JSAny'); + expect(booleanToJSAnyLookup, isNotNull, + reason: 'JSBoolean inherits JSAny'); + final (level: booleanToJSAnyLevel, path: booleanToJSAnyPath) = + booleanToJSAnyLookup!; + expect(booleanToJSAnyLevel, equals(1)); + expect(booleanToJSAnyPath, equals([0])); + + final objectTypeMap = + getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.object)); + expect(objectTypeMap.nodes.length, equals(1), + reason: 'JSObject only inherits JSAny'); + final objectToObjectLookup = objectTypeMap.lookup('JSObject'); + expect(objectToObjectLookup, isNotNull, reason: 'JSObject is JSObject'); + final (level: objectToObjectLevel, path: objectToObjectPath) = + objectToObjectLookup!; + expect(objectToObjectLevel, equals(isZero), + reason: 'Lookup search on self'); + expect(objectToObjectPath, isEmpty); + + final arrayTypeMap = getTypeHierarchy(BuiltinType.primitiveType( + PrimitiveType.array, + typeParams: [BuiltinType.anyType])); + expect(arrayTypeMap.nodes.length, equals(1), + reason: 'JSArray only inherits JSObject'); + }); + + test('Sub Type Primitive Test', () { + expect( + (getLowestCommonAncestorOfTypes( + [BuiltinType.anyType, BuiltinType.anyType]) as NamedType) + .name, + equals('JSAny')); + + final numStringSubType = getLowestCommonAncestorOfTypes([ + BuiltinType.primitiveType(PrimitiveType.num), + BuiltinType.primitiveType(PrimitiveType.string) + ]); + expect((numStringSubType as NamedType).name, equals('JSAny')); + }); + + group('LCA Test (small)', () {}); + + group('LCA Test (medium)', () { + final a = InterfaceDeclaration( + name: 'A', + exported: true, + id: const ID(type: 'interface', name: 'A')); + final b = InterfaceDeclaration( + name: 'B', + exported: true, + id: const ID(type: 'interface', name: 'B')); + final c = InterfaceDeclaration( + name: 'C', + exported: true, + id: const ID(type: 'interface', name: 'C'), + extendedTypes: [a.asReferredType()]); + final d = InterfaceDeclaration( + name: 'D', + exported: true, + id: const ID(type: 'interface', name: 'D'), + extendedTypes: [a.asReferredType()]); + final e = InterfaceDeclaration( + name: 'E', + exported: true, + id: const ID(type: 'interface', name: 'E'), + extendedTypes: [a.asReferredType(), b.asReferredType()]); + final f = InterfaceDeclaration( + name: 'F', + exported: true, + id: const ID(type: 'interface', name: 'F'), + extendedTypes: [a.asReferredType(), c.asReferredType()]); + final g = InterfaceDeclaration( + name: 'G', + exported: true, + id: const ID(type: 'interface', name: 'G'), + extendedTypes: [ + a.asReferredType(), + b.asReferredType(), + d.asReferredType() + ]); + final h = InterfaceDeclaration( + name: 'H', + exported: true, + id: const ID(type: 'interface', name: 'H'), + extendedTypes: [g.asReferredType(), f.asReferredType()]); + + test('Topological List Test', () { + final abTopoMap = topologicalList([ + a.asReferredType(), + b.asReferredType() + ].map(getTypeHierarchy).toList()); + + expect(abTopoMap.first, equals({'A', 'B'}), + reason: 'Root Values should be interface types'); + expect(abTopoMap[1], equals({'JSObject'}), + reason: 'A and B inherit JSObject'); + assert( + abTopoMap.last.single == 'JSAny', + 'A and B must always inherit JSAny, ' + 'and should be last in graph chain'); + + final cfTopoMap = topologicalList([ + c.asReferredType(), + f.asReferredType() + ].map(getTypeHierarchy).toList()); + + expect(cfTopoMap[1], contains(equals('A')), + reason: 'C and F inherit A'); + + final egTopoMap = topologicalList([ + e.asReferredType(), + g.asReferredType() + ].map(getTypeHierarchy).toList()); + expect(egTopoMap[1], containsAll(['A', 'B']), + reason: 'E and G both inherit from A and B'); + }); + + test('Sub Type Test', () { + final aType = getLowestCommonAncestorOfTypes([a.asReferredType()]); + expect(aType, isA(), + reason: 'Union of a single referred type is a referred typed'); + expect(aType, equals(aType), reason: 'Union of just A is A'); + + final abType = getLowestCommonAncestorOfTypes( + [a.asReferredType(), b.asReferredType()]); + expect(abType, isA(), + reason: 'Union of A and B is a builtin type'); + expect((abType as NamedType).name, equals('JSObject')); + + final acType = getLowestCommonAncestorOfTypes( + [a.asReferredType(), c.asReferredType()]); + expect(acType, isA()); + expect((acType as ReferredType).declaration.name, equals('A')); + + final acdType = getLowestCommonAncestorOfTypes( + [a.asReferredType(), c.asReferredType(), d.asReferredType()]); + expect(acdType, isA()); + expect((acdType as ReferredType).declaration.name, equals('A')); + + final cfType = getLowestCommonAncestorOfTypes( + [c.asReferredType(), f.asReferredType()]); + expect(cfType, isA()); + expect((cfType as ReferredType).declaration.name, equals('C')); + + final egType = getLowestCommonAncestorOfTypes( + [e.asReferredType(), g.asReferredType()]); + expect(egType, isA(), + reason: 'Common types between E and G are more than one'); + final UnionType(types: egUnionTypes) = egType as UnionType; + expect(egUnionTypes.length, equals(2), + reason: 'Common types between E and G are two'); + expect( + egUnionTypes.map((t) => (t as NamedType).name), equals(['A', 'B']), + reason: 'Common types between E and G are two: A and B'); + + final eghType = getLowestCommonAncestorOfTypes( + [e.asReferredType(), g.asReferredType(), h.asReferredType()]); + expect(eghType, isA(), + reason: 'Common types between E, G and H is more than one'); + final UnionType(types: eghUnionTypes) = eghType as UnionType; + expect(eghUnionTypes.length, equals(2), + reason: 'Common types between E, G and H are two'); + expect( + eghUnionTypes.map((t) => (t as NamedType).name), equals(['A', 'B']), + reason: 'Common types between E, G and H are two: A and B'); + }); + }); + + group('LCA Test (large)', () {}); + }); +}