diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 4bc6bb83..fc2585c6 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -134,6 +134,41 @@ class UnionType extends DeclarationType { } } +class IntersectionType extends DeclarationType { + final List types; + + @override + bool isNullable = false; + + @override + String declarationName; + + IntersectionType({required this.types, required String name}) + : declarationName = name; + + @override + ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('&')); + + @override + Declaration get declaration => + _IntersectionDeclaration(name: declarationName, types: types); + + @override + Reference emit([TypeOptions? options]) { + 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); + } +} + class HomogenousEnumType extends UnionType implements DeclarationType { final List _types; @@ -491,25 +526,27 @@ class _ConstructorDeclaration extends CallableDeclaration } } -// TODO: Merge properties/methods of related types -class _UnionDeclaration extends NamedDeclaration +sealed class _UnionOrIntersectionDeclaration extends NamedDeclaration implements ExportableDeclaration { @override bool get exported => true; @override - ID get id => ID(type: 'union', name: name); - - bool isNullable; + ID get id; List types; List typeParameters; - _UnionDeclaration( + @override + String name; + + @override + String? dartName; + + _UnionOrIntersectionDeclaration( {required this.name, this.types = const [], - this.isNullable = false, List? typeParams}) : typeParameters = typeParams ?? [] { if (typeParams == null) { @@ -522,14 +559,10 @@ class _UnionDeclaration extends NamedDeclaration } } - @override - String? dartName; - - @override - String name; - - @override - Spec emit([covariant DeclarationOptions? options]) { + Spec _emit( + {covariant DeclarationOptions? options, + bool extendTypes = false, + bool isNullable = false}) { options ??= DeclarationOptions(); final repType = @@ -541,7 +574,13 @@ class _UnionDeclaration extends NamedDeclaration ..representationDeclaration = RepresentationDeclaration((r) => r ..name = '_' ..declaredRepresentationType = repType.emit(options?.toTypeOptions())) - ..implements.addAll([repType.emit(options?.toTypeOptions())]) + ..implements.addAll([ + if (extendTypes) + ...types.map( + (t) => getJSTypeAlternative(t).emit(options?.toTypeOptions())) + else + repType.emit(options?.toTypeOptions()) + ]) ..types .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) ..methods.addAll(types.map((t) { @@ -600,3 +639,36 @@ class _UnionDeclaration extends NamedDeclaration }))); } } + +class _IntersectionDeclaration extends _UnionOrIntersectionDeclaration { + @override + bool get exported => true; + + @override + ID get id => ID(type: 'intersection', name: name); + + _IntersectionDeclaration({required super.name, super.types}) : super(); + + @override + Spec emit([covariant DeclarationOptions? options]) { + return super._emit(options: options, extendTypes: true); + } +} + +class _UnionDeclaration extends _UnionOrIntersectionDeclaration { + @override + bool get exported => true; + + @override + ID get id => ID(type: 'union', name: name); + + bool isNullable; + + _UnionDeclaration({required super.name, super.types, this.isNullable = false}) + : super(); + + @override + Spec emit([covariant DeclarationOptions? options]) { + return super._emit(options: options, isNullable: isNullable); + } +} diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 167ed26e..c68bb389 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -1284,6 +1284,46 @@ class Transformer { }); return unType..isNullable = shouldBeNullable; + case TSSyntaxKind.IntersectionType: + final intersectionType = type as TSIntersectionTypeNode; + final intersectionTypes = intersectionType.types.toDart; + final nonNullableIntersectionTypes = intersectionTypes + .where((t) => + t.kind != TSSyntaxKind.UndefinedKeyword && + !(t.kind == TSSyntaxKind.LiteralType && + (t as TSLiteralTypeNode).literal.kind == + TSSyntaxKind.NullKeyword)) + .toList(); + final shouldBeNullable = + nonNullableIntersectionTypes.length != intersectionTypes.length; + + if (shouldBeNullable) return BuiltinType.$voidType; + + if (nonNullableIntersectionTypes.singleOrNull + case final singleTypeNode?) { + return _transformType(singleTypeNode, isNullable: shouldBeNullable); + } + + final types = + nonNullableIntersectionTypes.map(_transformType).toList(); + + final idMap = types.map((t) => t.id.name); + final expectedId = ID(type: 'type', name: idMap.join('&')); + if (typeMap.containsKey(expectedId.toString())) { + return typeMap[expectedId.toString()] as UnionType; + } + + final intersectionHash = AnonymousHasher.hashUnion(idMap.toList()); + final name = 'AnonymousIntersection_$intersectionHash'; + + final un = IntersectionType(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; @@ -2138,12 +2178,13 @@ class Transformer { for (final t in t.types.where((t) => t is! BuiltinType)) t.id.toString(): t }); - case final UnionType u: + case UnionType(types: final uTypes, declaration: final uDecl) || + IntersectionType(types: final uTypes, declaration: final uDecl): filteredDeclarations.addAll({ - for (final t in u.types.where((t) => t is! BuiltinType)) + for (final t in uTypes.where((t) => t is! BuiltinType)) t.id.toString(): t }); - filteredDeclarations.add(u.declaration); + filteredDeclarations.add(uDecl); case final DeclarationType d: filteredDeclarations.add(d.declaration); break; diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index d2c322db..6174d55b 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -84,6 +84,7 @@ extension type const TSSyntaxKind._(num _) { // types static const TSSyntaxKind UnionType = TSSyntaxKind._(192); + static const TSSyntaxKind IntersectionType = TSSyntaxKind._(193); static const TSSyntaxKind TypeReference = TSSyntaxKind._(183); static const TSSyntaxKind ArrayType = TSSyntaxKind._(188); static const TSSyntaxKind LiteralType = TSSyntaxKind._(201); @@ -153,6 +154,13 @@ extension type TSUnionTypeNode._(JSObject _) implements TSTypeNode { external TSNodeArray get types; } +@JS('IntersectionTypeNode') +extension type TSIntersectionTypeNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.IntersectionType; + external TSNodeArray get types; +} + @JS('TypeQueryNode') extension type TSTypeQueryNode._(JSObject _) implements TSTypeNode { @redeclare 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 c583c9bc..e70c68bd 100644 --- a/web_generator/test/integration/interop_gen/ts_typing_expected.dart +++ b/web_generator/test/integration/interop_gen/ts_typing_expected.dart @@ -102,6 +102,12 @@ external _i1.JSArray get shoppingCart; external _AnonymousFunction_2181528 get createLogger; @_i1.JS() external _AnonymousFunction_1707607 get appLogger; +@_i1.JS() +external AnonymousIntersection_1467782 get myIntersection; +@_i1.JS() +external AnonymousIntersection_4895242 get mySecondIntersection; +@_i1.JS() +external AnonymousIntersection_1711585 get myTypeGymnastic; extension type AnonymousType_9143117._(_i1.JSObject _) implements _i1.JSObject { external AnonymousType_9143117({ @@ -253,3 +259,91 @@ extension type _AnonymousFunction_1707607._(_i1.JSFunction _) implements _i1.JSFunction { external void call(String message); } +extension type AnonymousIntersection_1467782._(_i1.JSAny _) + implements _i1.JSString, _i1.JSNumber { + String get asString => (_ as _i1.JSString).toDart; + + double get asDouble => (_ as _i1.JSNumber).toDartDouble; +} +extension type AnonymousIntersection_4895242._(_i1.JSAny _) + implements _i1.JSString, AnonymousType_1178025, AnonymousType_8266437 { + String get asString => (_ as _i1.JSString).toDart; + + AnonymousType_1178025 get asAnonymousType_1178025 => + (_ as AnonymousType_1178025); + + AnonymousType_8266437 get asAnonymousType_8266437 => + (_ as AnonymousType_8266437); +} +extension type AnonymousType_1178025._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1178025({ + String debugInfo, + _i1.JSSymbol tag, + }); + + external String debugInfo; + + external _i1.JSSymbol tag; +} +extension type AnonymousType_8266437._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_8266437(); + + @_i1.JS('toString') + external String toString$(); +} +extension type AnonymousIntersection_1711585._(_i1.JSAny _) + implements AnonymousUnion_2392544, AnonymousUnion_5737239 { + AnonymousUnion_2392544 get asAnonymousUnion_2392544 => + (_ as AnonymousUnion_2392544); + + AnonymousUnion_5737239 get asAnonymousUnion_5737239 => + (_ as AnonymousUnion_5737239); +} +extension type AnonymousUnion_2392544._(_i1.JSObject _) + implements _i1.JSObject { + AnonymousType_4207514 get asAnonymousType_4207514 => + (_ as AnonymousType_4207514); + + AnonymousType_1806035 get asAnonymousType_1806035 => + (_ as AnonymousType_1806035); +} +extension type AnonymousType_4207514._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_4207514({double a}); + + external double a; +} +extension type AnonymousType_1806035._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1806035({String b}); + + external String b; +} +extension type AnonymousUnion_5737239._(_i1.JSAny _) implements _i1.JSAny { + AnonymousType_1417026 get asAnonymousType_1417026 => + (_ as AnonymousType_1417026); + + AnonymousIntersection_4953753 get asAnonymousIntersection_4953753 => + (_ as AnonymousIntersection_4953753); +} +extension type AnonymousType_1417026._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1417026({bool c}); + + external bool c; +} +extension type AnonymousIntersection_4953753._(_i1.JSObject _) + implements AnonymousType_1229657, AnonymousType_4825273 { + AnonymousType_1229657 get asAnonymousType_1229657 => + (_ as AnonymousType_1229657); + + AnonymousType_4825273 get asAnonymousType_4825273 => + (_ as AnonymousType_4825273); +} +extension type AnonymousType_1229657._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_1229657({_i1.JSBigInt d}); + + external _i1.JSBigInt d; +} +extension type AnonymousType_4825273._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousType_4825273({_i1.JSSymbol e}); + + external _i1.JSSymbol e; +} 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 713db27d..9e86184f 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 @@ -74,3 +74,13 @@ export declare const shoppingCart: { }[]; export declare const createLogger: (prefix: string) => (message: string) => void; export declare const appLogger: (message: string) => void; +export declare const myIntersection: string & number; +export declare const mySecondIntersection: string & { + debugInfo: string; + tag: symbol +} & { + toString(): string +}; +export declare const myTypeGymnastic: + ({ a: number } | { b: string }) & + ({ c: boolean } | ({ d: bigint } & { e: symbol }));