Skip to content

Commit e2daa3a

Browse files
nikeokoronkwonikechukwu
andauthored
[interop] Add support for Intersection types (#451)
* wip: intersection types * Implemented intersection types fixes due to rep type issues Signed-off-by: Nike Okoronkwo <nikechukwu@gmail.com> * Update ts_typing_expected.dart Signed-off-by: Nike Okoronkwo <nikechukwu@gmail.com> * fixed issues and added support for unioning `undefined` * added test case for `& undefined` --------- Signed-off-by: Nike Okoronkwo <nikechukwu@gmail.com> Co-authored-by: nikechukwu <nikechukwu@googlemail.com>
1 parent a152054 commit e2daa3a

File tree

5 files changed

+291
-23
lines changed

5 files changed

+291
-23
lines changed

web_generator/lib/src/ast/types.dart

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,41 @@ class UnionType extends DeclarationType {
146146
}
147147
}
148148

149+
class IntersectionType extends DeclarationType {
150+
final List<Type> types;
151+
152+
@override
153+
bool isNullable = false;
154+
155+
@override
156+
String declarationName;
157+
158+
IntersectionType({required this.types, required String name})
159+
: declarationName = name;
160+
161+
@override
162+
ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('&'));
163+
164+
@override
165+
Declaration get declaration =>
166+
_IntersectionDeclaration(name: declarationName, types: types);
167+
168+
@override
169+
Reference emit([TypeOptions? options]) {
170+
return TypeReference((t) => t
171+
..symbol = declarationName
172+
..isNullable = (options?.nullable ?? false) || isNullable);
173+
}
174+
175+
@override
176+
int get hashCode => Object.hashAllUnordered(types);
177+
178+
@override
179+
bool operator ==(Object other) {
180+
return other is TupleType && other.types.every(types.contains);
181+
}
182+
}
183+
149184
class HomogenousEnumType<T extends LiteralType, D extends Declaration>
150185
extends UnionType implements DeclarationType {
151186
final List<T> _types;
@@ -557,25 +592,27 @@ class _ConstructorDeclaration extends CallableDeclaration
557592
}
558593
}
559594

560-
// TODO: Merge properties/methods of related types
561-
class _UnionDeclaration extends NamedDeclaration
595+
sealed class _UnionOrIntersectionDeclaration extends NamedDeclaration
562596
implements ExportableDeclaration {
563597
@override
564598
bool get exported => true;
565599

566600
@override
567-
ID get id => ID(type: 'union', name: name);
568-
569-
bool isNullable;
601+
ID get id;
570602

571603
List<Type> types;
572604

573605
List<GenericType> typeParameters;
574606

575-
_UnionDeclaration(
607+
@override
608+
String name;
609+
610+
@override
611+
String? dartName;
612+
613+
_UnionOrIntersectionDeclaration(
576614
{required this.name,
577615
this.types = const [],
578-
this.isNullable = false,
579616
List<GenericType>? typeParams})
580617
: typeParameters = typeParams ?? [] {
581618
if (typeParams == null) {
@@ -588,26 +625,42 @@ class _UnionDeclaration extends NamedDeclaration
588625
}
589626
}
590627

591-
@override
592-
String? dartName;
593-
594-
@override
595-
String name;
596-
597-
@override
598-
Spec emit([covariant DeclarationOptions? options]) {
628+
Spec _emit(
629+
{covariant DeclarationOptions? options,
630+
bool extendTypes = false,
631+
bool isNullable = false}) {
599632
options ??= DeclarationOptions();
600633

601634
final repType =
602635
getLowestCommonAncestorOfTypes(types, isNullable: isNullable);
603636

637+
final extendees = <Type>[];
638+
if (extendTypes) {
639+
// check if any types are primitive
640+
// TODO: We can be much smarter about this, but this works best so far
641+
if (types.any((t) {
642+
final jsAltType = getJSTypeAlternative(t);
643+
return jsAltType is BuiltinType &&
644+
_nonObjectRepTypeTypes.contains(jsAltType.name);
645+
}) ||
646+
(repType is BuiltinType && repType.name == 'JSAny')) {
647+
extendees.add(
648+
BuiltinType.primitiveType(PrimitiveType.any, isNullable: false));
649+
} else {
650+
extendees.addAll(types.map(getJSTypeAlternative));
651+
}
652+
} else {
653+
extendees.add(repType);
654+
}
655+
604656
return ExtensionType((e) => e
605657
..name = name
606658
..primaryConstructorName = '_'
607659
..representationDeclaration = RepresentationDeclaration((r) => r
608660
..name = '_'
609661
..declaredRepresentationType = repType.emit(options?.toTypeOptions()))
610-
..implements.addAll([repType.emit(options?.toTypeOptions())])
662+
..implements
663+
.addAll(extendees.map((e) => e.emit(options?.toTypeOptions())))
611664
..types
612665
.addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
613666
..methods.addAll(types.map((t) {
@@ -719,3 +772,45 @@ class _EnumObjDeclaration extends NamedDeclaration
719772
@override
720773
ID get id => ID(type: 'enum-rep', name: name);
721774
}
775+
776+
List<String> _nonObjectRepTypeTypes = [
777+
'JSAny',
778+
'JSString',
779+
'JSBoolean',
780+
'JSNumber',
781+
'JSSymbol',
782+
'JSBigInt'
783+
];
784+
785+
class _IntersectionDeclaration extends _UnionOrIntersectionDeclaration {
786+
@override
787+
bool get exported => true;
788+
789+
@override
790+
ID get id => ID(type: 'intersection', name: name);
791+
792+
_IntersectionDeclaration({required super.name, super.types}) : super();
793+
794+
@override
795+
Spec emit([covariant DeclarationOptions? options]) {
796+
return super._emit(options: options, extendTypes: true);
797+
}
798+
}
799+
800+
class _UnionDeclaration extends _UnionOrIntersectionDeclaration {
801+
@override
802+
bool get exported => true;
803+
804+
@override
805+
ID get id => ID(type: 'union', name: name);
806+
807+
bool isNullable;
808+
809+
_UnionDeclaration({required super.name, super.types, this.isNullable = false})
810+
: super();
811+
812+
@override
813+
Spec emit([covariant DeclarationOptions? options]) {
814+
return super._emit(options: options, isNullable: isNullable);
815+
}
816+
}

web_generator/lib/src/interop_gen/transform/transformer.dart

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,8 @@ class Transformer {
12681268
nonNullableUnionTypes.length != unionTypes.length;
12691269

12701270
if (nonNullableUnionTypes.singleOrNull case final singleTypeNode?) {
1271-
return _transformType(singleTypeNode, isNullable: shouldBeNullable);
1271+
return _transformType(singleTypeNode,
1272+
isNullable: shouldBeNullable || (isNullable ?? false));
12721273
}
12731274

12741275
final types = nonNullableUnionTypes.map<Type>(_transformType).toList();
@@ -1307,7 +1308,8 @@ class Transformer {
13071308
final expectedId = ID(type: 'type', name: idMap.join('|'));
13081309

13091310
if (typeMap.containsKey(expectedId.toString())) {
1310-
return typeMap[expectedId.toString()] as UnionType;
1311+
return (typeMap[expectedId.toString()] as UnionType)
1312+
..isNullable = (isNullable ?? false);
13111313
}
13121314

13131315
final name =
@@ -1323,6 +1325,50 @@ class Transformer {
13231325
});
13241326
return unType..isNullable = shouldBeNullable;
13251327

1328+
case TSSyntaxKind.IntersectionType:
1329+
final intersectionType = type as TSIntersectionTypeNode;
1330+
final intersectionTypes = intersectionType.types.toDart;
1331+
final nonNullableIntersectionTypes = intersectionTypes
1332+
.where((t) =>
1333+
t.kind != TSSyntaxKind.UndefinedKeyword &&
1334+
!(t.kind == TSSyntaxKind.LiteralType &&
1335+
(t as TSLiteralTypeNode).literal.kind ==
1336+
TSSyntaxKind.NullKeyword))
1337+
.toList();
1338+
final shouldBeNullable =
1339+
nonNullableIntersectionTypes.length != intersectionTypes.length;
1340+
1341+
if (shouldBeNullable) {
1342+
return BuiltinType.primitiveType(PrimitiveType.never,
1343+
isNullable: isNullable);
1344+
}
1345+
1346+
if (nonNullableIntersectionTypes.singleOrNull
1347+
case final singleTypeNode?) {
1348+
return _transformType(singleTypeNode, isNullable: isNullable);
1349+
}
1350+
1351+
final types =
1352+
nonNullableIntersectionTypes.map<Type>(_transformType).toList();
1353+
1354+
final idMap = types.map((t) => t.id.name);
1355+
final expectedId = ID(type: 'type', name: idMap.join('&'));
1356+
if (typeMap.containsKey(expectedId.toString())) {
1357+
return (typeMap[expectedId.toString()] as IntersectionType)
1358+
..isNullable = (isNullable ?? false);
1359+
}
1360+
1361+
final intersectionHash = AnonymousHasher.hashUnion(idMap.toList());
1362+
final name = 'AnonymousIntersection_$intersectionHash';
1363+
1364+
final un = IntersectionType(types: types, name: name);
1365+
1366+
final unType = typeMap.putIfAbsent(expectedId.toString(), () {
1367+
namer.markUsed(name);
1368+
return un;
1369+
});
1370+
1371+
return unType..isNullable = isNullable ?? shouldBeNullable;
13261372
case TSSyntaxKind.TupleType:
13271373
// tuple type is array
13281374
final tupleType = type as TSTupleTypeNode;
@@ -2332,12 +2378,13 @@ class Transformer {
23322378
for (final t in t.types.where((t) => t is! BuiltinType))
23332379
t.id.toString(): t
23342380
});
2335-
case final UnionType u:
2381+
case UnionType(types: final uTypes, declaration: final uDecl) ||
2382+
IntersectionType(types: final uTypes, declaration: final uDecl):
23362383
filteredDeclarations.addAll({
2337-
for (final t in u.types.where((t) => t is! BuiltinType))
2384+
for (final t in uTypes.where((t) => t is! BuiltinType))
23382385
t.id.toString(): t
23392386
});
2340-
filteredDeclarations.add(u.declaration);
2387+
filteredDeclarations.add(uDecl);
23412388
case final DeclarationType d:
23422389
filteredDeclarations.add(d.declaration);
23432390
break;

web_generator/lib/src/js/typescript.types.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ extension type const TSSyntaxKind._(num _) {
8787

8888
// types
8989
static const TSSyntaxKind UnionType = TSSyntaxKind._(192);
90+
static const TSSyntaxKind IntersectionType = TSSyntaxKind._(193);
9091
static const TSSyntaxKind TypeReference = TSSyntaxKind._(183);
9192
static const TSSyntaxKind ArrayType = TSSyntaxKind._(188);
9293
static const TSSyntaxKind LiteralType = TSSyntaxKind._(201);
@@ -158,6 +159,13 @@ extension type TSUnionTypeNode._(JSObject _) implements TSTypeNode {
158159
external TSNodeArray<TSTypeNode> get types;
159160
}
160161

162+
@JS('IntersectionTypeNode')
163+
extension type TSIntersectionTypeNode._(JSObject _) implements TSTypeNode {
164+
@redeclare
165+
TSSyntaxKind get kind => TSSyntaxKind.IntersectionType;
166+
external TSNodeArray<TSTypeNode> get types;
167+
}
168+
161169
@JS('TypeQueryNode')
162170
extension type TSTypeQueryNode._(JSObject _) implements TSTypeNode {
163171
@redeclare

0 commit comments

Comments
 (0)