diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index fc572ecf..8aae8966 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -160,6 +160,12 @@ class VariableDeclaration extends FieldDeclaration @override String? get dartName => null; + + @override + ReferredType asReferredType([List? typeArgs]) { + return ReferredType.fromType(type, this, + typeParams: typeArgs ?? []); + } } enum VariableModifier { let, $const, $var } @@ -226,6 +232,14 @@ class FunctionDeclaration extends CallableDeclaration ..requiredParameters.addAll(requiredParams) ..optionalParameters.addAll(optionalParams)); } + + @override + ReferredType asReferredType([List? typeArgs]) { + // TODO: We could do better here and make the function type typed + return ReferredType.fromType( + BuiltinType.referred('Function', typeParams: typeArgs ?? [])!, this, + typeParams: typeArgs ?? []); + } } class EnumDeclaration extends NamedDeclaration diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 7ba0e7fe..e0358da6 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -8,6 +8,7 @@ import 'base.dart'; import 'builtin.dart'; import 'declarations.dart'; +/// A type referring to a type in the TypeScript AST class ReferredType extends Type { @override String name; @@ -24,6 +25,9 @@ class ReferredType extends Type { required this.declaration, this.typeParams = const []}); + factory ReferredType.fromType(Type type, T declaration, + {List typeParams}) = ReferredDeclarationType; + @override Reference emit([TypeOptions? options]) { // TODO: Support referred types imported from URL @@ -34,6 +38,21 @@ class ReferredType extends Type { } } +class ReferredDeclarationType extends ReferredType { + Type type; + + @override + String get name => type.name ?? declaration.name; + + ReferredDeclarationType(this.type, T declaration, {super.typeParams}) + : super(name: declaration.name, declaration: declaration); + + @override + Reference emit([covariant TypeOptions? options]) { + return type.emit(options); + } +} + // TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`) class UnionType extends Type { final List types; diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 48831a41..0290c4f5 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -115,7 +115,7 @@ void transformFile(ts.TSProgram program, String file, }).toJS as ts.TSNodeCallback); // filter - final resolvedMap = transformer.filter(); + final resolvedMap = transformer.filterAndReturn(); programDeclarationMap.addAll({file: resolvedMap}); } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 6c38a8c8..87eef42d 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -51,6 +51,7 @@ class Transformer { filterDeclSet = filterDeclSet.toList(), namer = UniqueNamer(); + /// Transforms a TypeScript AST Node [TSNode] into a Dart representable [Node] void transform(TSNode node) { if (nodes.contains(node)) return; @@ -71,7 +72,6 @@ class Transformer { _transformClassOrInterface(node as TSObjectDeclaration), _ => throw Exception('Unsupported Declaration Kind: ${node.kind}') }; - // ignore: dead_code This line will not be dead in future decl additions nodeMap.add(decl); } @@ -696,9 +696,16 @@ class Transformer { constraint: getJSTypeAlternative(constraint)); } - /// Parses the type + /// Parses a TypeScript AST Type Node [TSTypeNode] into a [Type] Node + /// used to represent a type /// - /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types + /// [parameter] represents whether the [TSTypeNode] is being passed in + /// the context of a parameter, which is mainly used to differentiate between + /// using [num] and [double] in the context of a [JSNumber] + /// + /// [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 Type _transformType(TSTypeNode type, {bool parameter = false, bool typeArg = false}) { switch (type.kind) { @@ -794,6 +801,15 @@ class Transformer { _ => throw UnimplementedError( 'Unsupported Literal Kind ${literal.kind}') }); + case TSSyntaxKind.TypeQuery: + final typeQuery = type as TSTypeQueryNode; + + // TODO(nikeokoronkwo): Refactor this once #402 lands, https://github.com/dart-lang/web/pull/415 + final exprName = typeQuery.exprName; + final typeArguments = typeQuery.typeArguments?.toDart; + + return _getTypeFromDeclaration(exprName, typeArguments, + typeArg: typeArg, isNotTypableDeclaration: true); case TSSyntaxKind.ArrayType: return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [ getJSTypeAlternative( @@ -838,9 +854,27 @@ class Transformer { } } + /// Get the type of a type node named [typeName] by referencing its + /// declaration + /// + /// This method uses the TypeScript type checker [ts.TSTypeChecker] to get the + /// declaration associated with the [TSTypeNode] using its [typeName], and + /// refer to that type either as a [ReferredType] if defined in the file, or + /// not directly supported by `dart:js_interop`, or as a [BuiltinType] if + /// supported by `dart:js_interop` + /// + /// [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 + /// + /// [isNotTypableDeclaration] represents whether the declaration to search for + /// or refer to is not a typable declaration (i.e a declaration suitable for + /// use in a `typeof` type node, such as a variable). This reduces checks on + /// supported `dart:js_interop` types and related [EnumDeclaration]-like and + /// [TypeDeclaration]-like checks Type _getTypeFromDeclaration( TSIdentifier typeName, List? typeArguments, - {bool typeArg = false}) { + {bool typeArg = false, bool isNotTypableDeclaration = false}) { final name = typeName.text; var declarationsMatching = nodeMap.findByName(name); @@ -849,12 +883,14 @@ class Transformer { // TODO(https://github.com/dart-lang/web/issues/380): A better name // for this, and adding support for "supported declarations" // (also a better name for that) - final supportedType = BuiltinType.referred(name, - typeParams: (typeArguments ?? []) - .map((t) => getJSTypeAlternative(_transformType(t))) - .toList()); - if (supportedType case final resultType?) { - return resultType; + if (!isNotTypableDeclaration) { + final supportedType = BuiltinType.referred(name, + typeParams: (typeArguments ?? []) + .map((t) => getJSTypeAlternative(_transformType(t))) + .toList()); + if (supportedType case final resultType?) { + return resultType; + } } final symbol = typeChecker.getSymbolAtLocation(typeName); @@ -870,22 +906,24 @@ class Transformer { throw Exception('Found no declaration matching $name'); } - // check if this is from dom - final declarationSource = declaration.getSourceFile().fileName; - if (p.basename(declarationSource) == 'lib.dom.d.ts' || - declarationSource.contains('dom')) { - // dom declaration: supported by package:web - // TODO(nikeokoronkwo): It is possible that we may get a type - // that isn't in `package:web` - return PackageWebType.parse(name, - typeParams: (typeArguments ?? []) - .map(_transformType) - .map(getJSTypeAlternative) - .toList()); - } + if (!isNotTypableDeclaration) { + // check if this is from dom + final declarationSource = declaration.getSourceFile().fileName; + if (p.basename(declarationSource) == 'lib.dom.d.ts' || + declarationSource.contains('dom')) { + // dom declaration: supported by package:web + // TODO(nikeokoronkwo): It is possible that we may get a type + // that isn't in `package:web` + return PackageWebType.parse(name, + typeParams: typeArguments + ?.map((t) => getJSTypeAlternative(_transformType(t))) + .toList() ?? + []); + } - if (declaration.kind == TSSyntaxKind.TypeParameter) { - return GenericType(name: name); + if (declaration.kind == TSSyntaxKind.TypeParameter) { + return GenericType(name: name); + } } transform(declaration); @@ -896,23 +934,45 @@ class Transformer { // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? final firstNode = declarationsMatching.whereType().first; - // For Typealiases, we can either return the type itself - // or the JS Alternative (if its underlying type isn't a JS type) - switch (firstNode) { - case TypeAliasDeclaration(type: final t): - case EnumDeclaration(baseType: final t): - final jsType = getJSTypeAlternative(t); - if (jsType != t && typeArg) return jsType; + if (!isNotTypableDeclaration) { + // For Typealiases, we can either return the type itself + // or the JS Alternative (if its underlying type isn't a JS type) + switch (firstNode) { + case TypeAliasDeclaration(type: final t): + case EnumDeclaration(baseType: final t): + final jsType = getJSTypeAlternative(t); + if (jsType != t && typeArg) return jsType; + } } - return firstNode.asReferredType( + final asReferredType = firstNode.asReferredType( (typeArguments ?? []) .map((type) => _transformType(type, typeArg: true)) .toList(), ); + + if (asReferredType case ReferredDeclarationType(type: final type) + when type is BuiltinType) { + final jsType = getJSTypeAlternative(type); + if (jsType != type && typeArg) asReferredType.type = jsType; + } + + return asReferredType; } - NodeMap filter() { + /// Filters out the declarations generated from the [transform] function and + /// returns the declarations needed based on: + /// + /// - Whether they are exported (contains the `export` keyword, or is in an + /// export declaration captured by [exportSet]) + /// - Whether they are denoted to be included in configuration + /// ([filterDeclSet]) + /// + /// The function also goes through declaration dependencies and filters those + /// in too + /// + /// Returns a [NodeMap] containing a map of the declared nodes and IDs. + NodeMap filterAndReturn() { final filteredDeclarations = NodeMap(); // filter out for export declarations @@ -1026,8 +1086,11 @@ class Transformer { t.id.toString(): t }); break; - case final BuiltinType _: - // primitive types are generated by default + case BuiltinType(typeParams: final typeParams) when typeParams.isNotEmpty: + filteredDeclarations.addAll({ + for (final t in typeParams.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); break; case final ReferredType r: filteredDeclarations.add(r.declaration); diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index ce841378..bf75e374 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -78,6 +78,7 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind ArrayType = TSSyntaxKind._(188); static const TSSyntaxKind LiteralType = TSSyntaxKind._(201); static const TSSyntaxKind ThisType = TSSyntaxKind._(197); + static const TSSyntaxKind TypeQuery = TSSyntaxKind._(186); /// Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); @@ -127,6 +128,21 @@ extension type TSUnionTypeNode._(JSObject _) implements TSTypeNode { external TSNodeArray get types; } +// TODO(nikeokoronkwo): Implements TSNodeWithTypeArguments +// once #402 and #409 are closed +@JS('TypeQueryNode') +extension type TSTypeQueryNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.TypeQuery; + + // TODO(nikeokoronkwo): Change to EntityName to support + // qualified names, https://github.com/dart-lang/web/issues/416 + external TSIdentifier get exprName; + external TSNodeArray? get typeArguments; +} + +// TODO(nikeokoronkwo): Implements TSNodeWithTypeArguments +// once #402 and #409 are closed @JS('TypeReferenceNode') extension type TSTypeReferenceNode._(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 new file mode 100644 index 00000000..c4c96786 --- /dev/null +++ b/web_generator/test/integration/interop_gen/ts_typing_expected.dart @@ -0,0 +1,46 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +@_i1.JS() +external String get myString; +@_i1.JS() +external _i1.JSArray<_i1.JSNumber> get myNumberArray; +@_i1.JS() +external String get myCloneString; +@_i1.JS() +external _i1.JSArray<_i1.JSArray<_i1.JSNumber>> get myCloneNumberArray; +extension type const MyEnum._(int _) { + static const MyEnum A = MyEnum._(0); + + static const MyEnum B = MyEnum._(1); + + static const MyEnum C = MyEnum._(2); + + static const MyEnum D = MyEnum._(4); +} +@_i1.JS() +external String myFunction(String param); +@_i1.JS() +external String myEnclosingFunction(_i1.JSFunction func); +@_i1.JS() +external _i1.JSFunction copyOfmyEnclosingFunction; +@_i1.JS() +external MyEnum get myEnumValue; +@_i1.JS() +external MyEnum get myEnumValue2; +@_i1.JS() +external _i1.JSFunction myFunctionAlias; +@_i1.JS() +external _i1.JSFunction myFunctionAlias2; +@_i1.JS() +external _i1.JSFunction get myEnclosingFunctionAlias; +@_i1.JS() +external ComposedType get myComposedType; +@_i1.JS() +external ComposedType<_i1.JSString> get myComposedMyString; +extension type ComposedType._(_i1.JSObject _) + implements _i1.JSObject { + external T enclosed; +} 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 new file mode 100644 index 00000000..e665d780 --- /dev/null +++ b/web_generator/test/integration/interop_gen/ts_typing_input.d.ts @@ -0,0 +1,25 @@ +export declare const myString: string; +export declare const myNumberArray: number[]; +export declare const myCloneString: typeof myString; +export declare const myCloneNumberArray: typeof myNumberArray[]; +export declare enum MyEnum { + A = 0, + B = 1, + C = 2, + D = 4 +} +interface ComposedType { + enclosed: T; +} +export declare let copyOfmyEnclosingFunction: typeof myEnclosingFunction; +export declare const myEnumValue: MyEnum; +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; 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 afd1db61..95cc26a6 100644 --- a/web_generator/test/integration/interop_gen/web_types_expected.dart +++ b/web_generator/test/integration/interop_gen/web_types_expected.dart @@ -25,3 +25,20 @@ external _i2.HTMLDivElement get output; external void handleButtonClick(_i2.MouseEvent event); @_i1.JS() external void handleInputChange(_i2.Event event); +@_i1.JS() +external _i1.JSAny? transformElements( + _i1.JSArray<_i2.HTMLElement> el, + HTMLTransformFunc<_i2.HTMLElement, _i2.HTMLElement> transformer, +); +@_i1.JS() +external _i1.JSAny? handleEvents( + _i2.Event event, + _i1.JSArray onCallbacks, +); +extension type HTMLTransformFunc._(_i1.JSObject _) implements _i1.JSObject { + external R call(T element); +} +extension type EventManipulationFunc._(_i1.JSObject _) implements _i1.JSObject { + external _i1.JSAny? call(_i2.Event event); +} diff --git a/web_generator/test/integration/interop_gen/web_types_input.d.ts b/web_generator/test/integration/interop_gen/web_types_input.d.ts index 7e1a497d..366cf538 100644 --- a/web_generator/test/integration/interop_gen/web_types_input.d.ts +++ b/web_generator/test/integration/interop_gen/web_types_input.d.ts @@ -1,3 +1,15 @@ +interface HTMLTransformFunc { + (element: T): R; +} +interface EventManipulationFunc { + (event: Event): any; +} +interface ElementStamp { + readonly target: T; + readonly stampedAt: Date; + id: string; + stampType: "emit" | "none"; +} export declare const myCustomEvent: CustomEvent; export declare var myShadowRoot: ShadowRoot; declare let myURL: URL; @@ -10,3 +22,5 @@ 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 handleEvents(event: Event, onCallbacks: EventManipulationFunc[]): any;