diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart index 6ebc13c4..c9d9296a 100644 --- a/web_generator/lib/src/ast/base.dart +++ b/web_generator/lib/src/ast/base.dart @@ -8,15 +8,17 @@ import '../interop_gen/namer.dart'; import 'types.dart'; class GlobalOptions { - static int variardicArgsCount = 4; + static int variadicArgsCount = 4; static bool shouldEmitJsTypes = false; + static bool redeclareOverrides = true; } class Options {} -// TODO(nikeokoronkwo): Remove this once we address isNullable class DeclarationOptions extends Options { - DeclarationOptions(); + bool override; + + DeclarationOptions({this.override = false}); TypeOptions toTypeOptions({bool nullable = false}) => TypeOptions(nullable: nullable); @@ -31,11 +33,11 @@ class TypeOptions extends Options { class ASTOptions { bool parameter; bool emitJSTypes; - int variardicArgsCount; + int variadicArgsCount; ASTOptions( {this.parameter = false, - this.variardicArgsCount = 4, + this.variadicArgsCount = 4, this.emitJSTypes = false}); } @@ -74,3 +76,39 @@ abstract class Type extends Node { @override Reference emit([covariant TypeOptions? options]); } + +abstract class FieldDeclaration extends NamedDeclaration { + abstract final Type type; +} + +abstract class CallableDeclaration extends NamedDeclaration { + abstract final List parameters; + + abstract final List typeParameters; + + abstract final Type returnType; +} + +enum DeclScope { private, protected, public } + +class ParameterDeclaration { + final String name; + + final bool optional; + + final Type type; + + final bool variadic; + + ParameterDeclaration( + {required this.name, + this.optional = false, + required this.type, + this.variadic = false}); + + Parameter emit([DeclarationOptions? options]) { + return Parameter((p) => p + ..name = name + ..type = type.emit(TypeOptions(nullable: optional))); + } +} diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index a57a129a..fc572ecf 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -6,10 +6,117 @@ import 'package:code_builder/code_builder.dart'; import '../interop_gen/namer.dart'; import 'base.dart'; +import 'builtin.dart'; import 'helpers.dart'; import 'types.dart'; -class VariableDeclaration extends NamedDeclaration +/// A declaration that defines a type +/// +// TODO: Add support for `ClassOrInterfaceDeclaration` +// once implementing namespaces and module support +sealed class TypeDeclaration extends NamedDeclaration + implements ExportableDeclaration { + @override + final String name; + + @override + final String? dartName; + + @override + final bool exported; + + final List typeParameters; + + final List methods; + + final List properties; + + final List operators; + + final List constructors; + + TypeDeclaration( + {required this.name, + this.dartName, + required this.exported, + this.typeParameters = const [], + this.methods = const [], + this.properties = const [], + this.operators = const [], + this.constructors = const []}); + + ExtensionType _emit( + [covariant DeclarationOptions? options, + bool abstract = false, + List extendees = const [], + List implementees = const []]) { + options ??= DeclarationOptions(); + + final hierarchy = getMemberHierarchy(this); + + final fieldDecs = []; + final methodDecs = []; + + bool isOverride(String name) => + hierarchy.contains(name) && GlobalOptions.redeclareOverrides; + + for (final prop in properties.where((p) => p.scope == DeclScope.public)) { + final spec = + prop.emit(options..override = isOverride(prop.dartName ?? prop.name)); + if (spec is Method) { + methodDecs.add(spec); + } else { + fieldDecs.add(spec as Field); + } + } + + methodDecs.addAll(methods.where((p) => p.scope == DeclScope.public).map( + (m) => m.emit(options!..override = isOverride(m.dartName ?? m.name)))); + 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) + : BuiltinType.primitiveType(PrimitiveType.object, isNullable: false); + + return ExtensionType((e) => e + ..name = dartName ?? name + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name) + ]) + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((r) => r + ..declaredRepresentationType = repType.emit(options?.toTypeOptions()) + ..name = '_') + ..implements.addAll([ + if (extendees.isEmpty && implementees.isEmpty) + refer('JSObject', 'dart:js_interop') + else ...[ + ...extendees.map((e) => e.emit(options?.toTypeOptions())), + ...implementees.map((i) => i.emit(options?.toTypeOptions())) + ] + ]) + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..constructors.addAll([ + if (!abstract) + if (constructors.isEmpty && this is ClassDeclaration) + ConstructorDeclaration.defaultFor(this).emit(options) + else + ...constructors.map((c) => c.emit(options)) + ]) + ..fields.addAll(fieldDecs) + ..methods.addAll(methodDecs)); + } +} + +abstract class MemberDeclaration { + late final TypeDeclaration parent; + + abstract final DeclScope scope; +} + +class VariableDeclaration extends FieldDeclaration implements ExportableDeclaration { /// The variable modifier, as represented in TypeScript VariableModifier modifier; @@ -17,6 +124,7 @@ class VariableDeclaration extends NamedDeclaration @override String name; + @override Type type; @override @@ -39,13 +147,13 @@ class VariableDeclaration extends NamedDeclaration ..type = MethodType.getter ..annotations.add(generateJSAnnotation()) ..external = true - ..returns = type.emit()); + ..returns = type.emit(options?.toTypeOptions())); } else { // getter and setter -> single variable return Field((f) => f ..external = true ..name = name - ..type = type.emit() + ..type = type.emit(options?.toTypeOptions()) ..annotations.add(generateJSAnnotation())); } } @@ -56,7 +164,7 @@ class VariableDeclaration extends NamedDeclaration enum VariableModifier { let, $const, $var } -class FunctionDeclaration extends NamedDeclaration +class FunctionDeclaration extends CallableDeclaration implements ExportableDeclaration { @override final String name; @@ -64,10 +172,13 @@ class FunctionDeclaration extends NamedDeclaration @override final String? dartName; + @override final List parameters; + @override final List typeParameters; + @override final Type returnType; @override @@ -92,8 +203,8 @@ class FunctionDeclaration extends NamedDeclaration final requiredParams = []; final optionalParams = []; for (final p in parameters) { - if (p.variardic) { - optionalParams.addAll(spreadParam(p, GlobalOptions.variardicArgsCount)); + if (p.variadic) { + optionalParams.addAll(spreadParam(p, GlobalOptions.variadicArgsCount)); requiredParams.add(p.emit(options)); } else { if (p.optional) { @@ -111,34 +222,12 @@ class FunctionDeclaration extends NamedDeclaration dartName == null || dartName == name ? null : name)) ..types .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) - ..returns = returnType.emit() + ..returns = returnType.emit(options?.toTypeOptions()) ..requiredParameters.addAll(requiredParams) ..optionalParameters.addAll(optionalParams)); } } -class ParameterDeclaration { - final String name; - - final bool optional; - - final Type type; - - final bool variardic; - - ParameterDeclaration( - {required this.name, - this.optional = false, - required this.type, - this.variardic = false}); - - Parameter emit([DeclarationOptions? options]) { - return Parameter((p) => p - ..name = name - ..type = type.emit(TypeOptions(nullable: optional))); - } -} - class EnumDeclaration extends NamedDeclaration implements ExportableDeclaration { @override @@ -269,3 +358,399 @@ class TypeAliasDeclaration extends NamedDeclaration ..definition = type.emit(options?.toTypeOptions())); } } + +/// The declaration node for a TypeScript/JavaScript Class +/// +/// ```ts +/// class A {} +/// ``` +class ClassDeclaration extends TypeDeclaration { + final bool abstract; + + final Type? extendedType; + + final List implementedTypes; + + ClassDeclaration( + {required super.name, + super.dartName, + this.abstract = false, + required super.exported, + super.typeParameters, + this.extendedType, + this.implementedTypes = const [], + super.constructors, + required super.methods, + required super.properties, + super.operators}); + + @override + ExtensionType emit([covariant DeclarationOptions? options]) { + return super._emit(options, abstract, + [if (extendedType case final extendee?) extendee], implementedTypes); + } + + @override + ID get id => ID(type: 'class', name: name); +} + +/// The declaration node for a TypeScript [Interface]() +/// +/// ```ts +/// interface Movable { +/// +/// } +/// ``` +class InterfaceDeclaration extends TypeDeclaration { + @override + ID id; + + final List extendedTypes; + + InterfaceDeclaration( + {required super.name, + required super.exported, + required this.id, + super.dartName, + super.typeParameters, + this.extendedTypes = const [], + super.methods, + super.properties, + super.operators, + super.constructors}); + + @override + ExtensionType emit([covariant DeclarationOptions? options]) { + return super._emit( + options, + false, + extendedTypes, + ); + } +} + +/// The declaration node for a field/property on a [TypeDeclaration] +class PropertyDeclaration extends FieldDeclaration + implements MemberDeclaration { + @override + final String name; + + @override + final ID id; + + @override + final String? dartName; + + @override + late final TypeDeclaration parent; + + @override + final DeclScope scope; + + final bool isNullable; + + final bool readonly; + + final bool static; + + @override + Type type; + + PropertyDeclaration( + {required this.name, + this.dartName, + required this.id, + required this.type, + this.scope = DeclScope.public, + this.readonly = false, + required this.static, + this.isNullable = false}); + + @override + Spec emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + assert(scope == DeclScope.public, 'Only public members can be emitted'); + + if (readonly) { + return Method((m) => m + ..external = true + ..name = dartName ?? name + ..type = MethodType.getter + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name), + if (options?.override ?? false) _redeclareExpression + ]) + ..returns = type.emit(options?.toTypeOptions(nullable: isNullable))); + } else { + return Field((f) => f + ..external = true + ..name = dartName ?? name + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name), + ]) + ..type = type.emit(options?.toTypeOptions(nullable: isNullable))); + } + } +} + +/// The declaration node for a method on a [TypeDeclaration] +class MethodDeclaration extends CallableDeclaration + implements MemberDeclaration { + @override + String name; + + @override + String? dartName; + + @override + ID id; + + MethodKind? kind; + + @override + List parameters; + + @override + Type returnType; + + @override + List typeParameters; + + @override + late final TypeDeclaration parent; + + @override + final DeclScope scope; + + final bool static; + + final bool isNullable; + + MethodDeclaration( + {required this.name, + this.dartName, + required this.id, + this.kind = MethodKind.none, + this.parameters = const [], + this.typeParameters = const [], + required this.returnType, + this.static = false, + this.scope = DeclScope.public, + this.isNullable = false}); + + @override + Method emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + final requiredParams = []; + final optionalParams = []; + for (final p in parameters) { + if (p.variadic) { + optionalParams.addAll(spreadParam(p, GlobalOptions.variadicArgsCount)); + requiredParams.add(p.emit(options)); + } else { + if (p.optional) { + optionalParams.add(p.emit(options)); + } else { + requiredParams.add(p.emit(options)); + } + } + } + + assert(scope == DeclScope.public, 'Only public members can be emitted'); + + if (isNullable) { + return Method((m) => m + ..external = true + ..name = dartName ?? name + ..type = MethodType.getter + ..static = static + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name), + if (options?.override ?? false) _redeclareExpression + ]) + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + // TODO(nikeokoronkwo): We can make this function more typed in the future, https://github.com/dart-lang/sdk/issues/54557 + ..returns = TypeReference((t) => t + ..symbol = 'JSFunction' + ..isNullable = true + ..url = 'dart:js_interop')); + } + + return Method((m) => m + ..external = true + ..name = dartName ?? name + ..type = switch (kind) { + MethodKind.getter => MethodType.getter, + MethodKind.setter => MethodType.setter, + _ => null + } + ..static = static + ..annotations.addAll([ + if (dartName != null && dartName != name) generateJSAnnotation(name), + if (options?.override ?? false) _redeclareExpression + ]) + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..returns = kind == MethodKind.setter + ? null + : returnType.emit(options?.toTypeOptions()) + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams)); + } +} + +enum MethodKind { getter, setter, none } + +/// The declaration node for a constructor on a [ClassDeclaration] +/// +/// ```ts +/// class A { +/// num: number; +/// +/// constructor(num: number) { +/// this.num = num; +/// } +/// } +/// ``` +// TODO: Suggesting a config option for adding custom constructors (factories) +class ConstructorDeclaration implements MemberDeclaration { + @override + late final TypeDeclaration parent; + + @override + final DeclScope scope; + + final List parameters; + + final String? name; + + final ID id; + + final String? dartName; + + ConstructorDeclaration( + {this.parameters = const [], + this.name, + String? dartName, + required this.id, + this.scope = DeclScope.public}) + : dartName = dartName == 'unnamed' ? null : dartName; + + static ConstructorDeclaration defaultFor(TypeDeclaration decl) { + return ConstructorDeclaration(id: const ID(type: 'constructor', name: '')) + ..parent = decl; + } + + Constructor emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + final requiredParams = []; + final optionalParams = []; + final isFactory = dartName != null && dartName != name; + + for (final p in parameters) { + if (p.variadic) { + optionalParams.addAll(spreadParam(p, GlobalOptions.variadicArgsCount)); + requiredParams.add(p.emit(options)); + } else { + if (p.optional) { + optionalParams.add(p.emit(options)); + } else { + requiredParams.add(p.emit(options)); + } + } + } + + return Constructor((c) => c + ..external = true + ..name = dartName ?? name + ..annotations + .addAll([if (name != null && isFactory) generateJSAnnotation(name)]) + ..factory = isFactory + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams)); + } +} + +/// The declaration node for an operator member on a class or interface, +/// usually an indexed accessor for classes, and could be what represents +/// callable/indexable interfaces +class OperatorDeclaration extends CallableDeclaration + implements MemberDeclaration { + @override + String get name => kind.expression; + + OperatorKind kind; + + @override + String? dartName; + + @override + List parameters; + + @override + Type returnType; + + @override + List typeParameters; + + @override + late final TypeDeclaration parent; + + @override + final DeclScope scope; + + final bool static; + + OperatorDeclaration( + {required this.kind, + this.dartName, + this.parameters = const [], + required this.returnType, + this.typeParameters = const [], + this.scope = DeclScope.public, + this.static = false}); + + @override + Method emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + + final requiredParams = []; + final optionalParams = []; + for (final p in parameters) { + if (p.variadic) { + throw UnsupportedError('Variadic parameters are not supported for ' + 'operators.'); + } else if (p.optional) { + optionalParams.add(p.emit(options)); + } else { + requiredParams.add(p.emit(options)); + } + } + + return Method((m) => m + ..external = true + ..name = 'operator $name' + ..types + .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) + ..returns = returnType.emit(options?.toTypeOptions()) + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams)); + } + + @override + ID get id => ID(type: 'op', name: name); +} + +enum OperatorKind { + squareBracket('[]'), + squareBracketSet('[]='); + + const OperatorKind(this.expression); + final String expression; +} + +Expression get _redeclareExpression => + refer('redeclare', 'package:meta/meta.dart'); diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index 923692a8..f9140b73 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -50,3 +50,65 @@ List spreadParam(ParameterDeclaration p, int count) { return ParameterDeclaration(name: paramName, type: p.type).emit(); }); } + +final Map> _memberHierarchyCache = {}; + +Set getMemberHierarchy(TypeDeclaration type, + [bool addDirectMembers = false]) { + final members = {}; + + void addMembersIfReferredType(Type type) { + if (type case ReferredType(declaration: final d) + when d is TypeDeclaration) { + members.addAll(getMemberHierarchy(d, true)); + } + } + + if (addDirectMembers) { + if (_memberHierarchyCache.containsKey(type.name)) { + return _memberHierarchyCache[type.name]!; + } + // add direct members + members.addAll(type.methods.map((m) => m.name)); + members.addAll(type.properties.map((m) => m.name)); + members.addAll(type.operators.map((m) => m.name)); + } + + switch (type) { + case ClassDeclaration( + extendedType: final extendee, + implementedTypes: final implementees + ): + if (extendee case final extendedType?) { + addMembersIfReferredType(extendedType); + } + implementees.forEach(addMembersIfReferredType); + break; + case InterfaceDeclaration(extendedTypes: final extendees): + extendees.forEach(addMembersIfReferredType); + break; + } + + if (addDirectMembers) { + _memberHierarchyCache[type.name] ??= members; + } + + return members; +} + +Type getClassRepresentationType(ClassDeclaration cl) { + if (cl.extendedType case final extendee?) { + return switch (extendee) { + final ClassDeclaration classExtendee => + getClassRepresentationType(classExtendee), + _ => BuiltinType.primitiveType(PrimitiveType.object, isNullable: false) + }; + } else { + final primitiveType = switch (cl.name) { + 'Array' => PrimitiveType.array, + _ => PrimitiveType.object + }; + + return BuiltinType.primitiveType(primitiveType, isNullable: false); + } +} diff --git a/web_generator/lib/src/banned_names.dart b/web_generator/lib/src/banned_names.dart index 700b913c..39e13aea 100644 --- a/web_generator/lib/src/banned_names.dart +++ b/web_generator/lib/src/banned_names.dart @@ -69,6 +69,7 @@ const keywords = { 'while', 'with', 'yield', + 'toString' }; const bannedNames = { diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart index 085fb820..3cce7a7e 100644 --- a/web_generator/lib/src/interop_gen/namer.dart +++ b/web_generator/lib/src/interop_gen/namer.dart @@ -13,6 +13,16 @@ class ID { bool get isUnnamed => name == 'unnamed'; + String get rename { + if (index != null && index != 0) { + final s = + name.endsWith(r'$') ? name.substring(0, name.indexOf(r'$')) : name; + return '$s\$$index'; + } else { + return name; + } + } + @override String toString() => '$type#$name${index != null ? '#$index' : ''}'; } @@ -20,8 +30,9 @@ class ID { class UniqueNamer { final Set _usedNames; - UniqueNamer([Iterable used = const []]) - : _usedNames = used.toSet(); + UniqueNamer([ + Iterable used = const [], + ]) : _usedNames = used.toSet(); /// Makes a name that does not conflict with dart keywords static String makeNonConflicting(String name) { @@ -84,3 +95,49 @@ class UniqueNamer { _usedNames.add(name); } } + +class ScopedUniqueNamer implements UniqueNamer { + final Set _usedIDs; + final Set _allowedEquals; + + @override + Set get _usedNames => _usedIDs.map((i) => i.rename).toSet(); + + ScopedUniqueNamer( + [Set? allowedEquals, Iterable used = const []]) + : _usedIDs = used.map(UniqueNamer.parse).toSet(), + _allowedEquals = allowedEquals ?? {}; + + @override + ({ID id, String name}) makeUnique(String name, String type) { + // nested structures (and anonymous structures) may not have a name + + final newName = UniqueNamer.makeNonConflicting(name); + + var i = 0; + var id = ID(name: newName, type: type); + while (_usedIDs.any((usedID) { + if (usedID.name == id.name && usedID.index == id.index) { + // check if both types are allowed + if (_allowedEquals.contains(usedID.type) && + _allowedEquals.contains(id.type)) { + return false; + } + return true; + } + return false; + })) { + ++i; + id = ID(name: newName, index: i, type: type); + } + + markUsed(id.toString()); + + return (id: id, name: id.rename); + } + + @override + void markUsed(String name) { + _usedIDs.add(UniqueNamer.parse(name)); + } +} diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 64462325..48831a41 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -17,7 +17,7 @@ import 'parser.dart'; import 'transform/transformer.dart'; void _setGlobalOptions(Config config) { - GlobalOptions.variardicArgsCount = config.functions?.varArgs ?? 4; + GlobalOptions.variadicArgsCount = config.functions?.varArgs ?? 4; } class TransformResult { @@ -65,7 +65,7 @@ class TransformResult { /// A map of declarations, where the key is the declaration's stringified [ID]. extension type NodeMap._(Map decls) implements Map { - NodeMap() : decls = {}; + NodeMap([Map? decls]) : decls = decls ?? {}; List findByName(String name) { return decls.entries diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 4a8ba4a5..6c38a8c8 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -21,6 +21,9 @@ class Transformer { /// A map of declarations final NodeMap nodeMap = NodeMap(); + /// A map of types + final NodeMap typeMap = NodeMap(); + /// The type checker for the given program final ts.TSTypeChecker typeChecker; @@ -63,6 +66,9 @@ class Transformer { _transformEnum(node as TSEnumDeclaration), TSSyntaxKind.TypeAliasDeclaration => _transformTypeAlias(node as TSTypeAliasDeclaration), + TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration => + _transformClassOrInterface(node as TSObjectDeclaration), _ => throw Exception('Unsupported Declaration Kind: ${node.kind}') }; // ignore: dead_code This line will not be dead in future decl additions @@ -72,6 +78,486 @@ class Transformer { nodes.add(node); } + TypeDeclaration _transformClassOrInterface(TSObjectDeclaration typeDecl) { + final name = typeDecl.name.text; + + final modifiers = typeDecl.modifiers?.toDart; + var isExported = false; + var isAbstract = false; + + for (final mod in modifiers ?? []) { + if (mod.kind == TSSyntaxKind.ExportKeyword) { + isExported = true; + } else if (mod.kind == TSSyntaxKind.AbstractKeyword) { + isAbstract = true; + } + } + + final heritageClauses = typeDecl.heritageClauses?.toDart ?? []; + + final extendees = []; + final implementees = []; + + for (final clause in heritageClauses) { + if (clause.token == TSSyntaxKind.ExtendsKeyword) { + // extends + extendees.addAll( + clause.types.toDart.map(_transformTypeExpressionWithTypeArguments)); + } else { + implementees.addAll( + clause.types.toDart.map(_transformTypeExpressionWithTypeArguments)); + } + } + + final isInterface = typeDecl.kind == TSSyntaxKind.InterfaceDeclaration; + + final (:id, name: dartName) = + namer.makeUnique(name, isInterface ? 'interface' : 'class'); + final typeParams = typeDecl.typeParameters?.toDart; + + final outputType = isInterface + ? InterfaceDeclaration( + name: name, + dartName: dartName, + id: id, + exported: isExported, + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + extendedTypes: extendees, + methods: [], + properties: [], + operators: [], + constructors: []) + : ClassDeclaration( + name: name, + dartName: dartName, + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + extendedType: extendees.firstOrNull, + implementedTypes: implementees, + exported: isExported, + abstract: isAbstract, + constructors: [], + methods: [], + properties: [], + operators: []); + + final typeNamer = ScopedUniqueNamer({'get', 'set'}); + + for (final member in typeDecl.members.toDart) { + switch (member.kind) { + case TSSyntaxKind.PropertySignature: + case TSSyntaxKind.PropertyDeclaration: + final prop = _transformProperty(member as TSPropertyEntity, + parentNamer: typeNamer, parent: outputType); + outputType.properties.add(prop); + break; + case TSSyntaxKind.MethodSignature: + final method = _transformMethod(member as TSMethodSignature, + parentNamer: typeNamer, parent: outputType); + outputType.methods.add(method); + break; + case TSSyntaxKind.MethodDeclaration: + final method = _transformMethod(member as TSMethodDeclaration, + parentNamer: typeNamer, parent: outputType); + outputType.methods.add(method); + break; + case TSSyntaxKind.IndexSignature: + final (opGet, opSetOrNull) = _transformIndexer( + member as TSIndexSignatureDeclaration, + parent: outputType); + outputType.operators.add(opGet); + if (opSetOrNull case final opSet?) { + outputType.operators.add(opSet); + } + break; + case TSSyntaxKind.CallSignature: + final callSignature = _transformCallSignature( + member as TSCallSignatureDeclaration, + parentNamer: typeNamer, + parent: outputType); + outputType.methods.add(callSignature); + break; + case TSSyntaxKind.ConstructSignature: + final constructor = _transformConstructor( + member as TSConstructSignatureDeclaration, + parentNamer: typeNamer); + constructor.parent = outputType; + outputType.constructors.add(constructor); + break; + case TSSyntaxKind.Constructor: + final constructor = _transformConstructor( + member as TSConstructorDeclaration, + parentNamer: typeNamer); + constructor.parent = outputType; + outputType.constructors.add(constructor); + break; + case TSSyntaxKind.GetAccessor: + final getter = _transformGetter(member as TSGetAccessorDeclaration, + parentNamer: typeNamer, parent: outputType); + outputType.methods.add(getter); + break; + case TSSyntaxKind.SetAccessor: + final setter = _transformSetter(member as TSSetAccessorDeclaration, + parentNamer: typeNamer, parent: outputType); + outputType.methods.add(setter); + break; + default: + // skipping + break; + } + } + + return outputType; + } + + PropertyDeclaration _transformProperty(TSPropertyEntity property, + {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + final name = property.name.text; + + final (:id, name: dartName) = parentNamer.makeUnique(name, 'var'); + + final (:isStatic, :isReadonly, :scope) = + _parseModifiers(property.modifiers); + + ReferredType? propType; + if (property.type case final type? when ts.isTypeReferenceNode(type)) { + // check if + final referredType = type as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + + final propertyDeclaration = PropertyDeclaration( + name: name, + dartName: dartName, + id: id, + scope: scope, + type: propType ?? + (property.type == null + ? BuiltinType.anyType + : _transformType(property.type!)), + static: isStatic, + readonly: isReadonly, + isNullable: property.questionToken != null); + propertyDeclaration.parent = parent; + return propertyDeclaration; + } + + MethodDeclaration _transformMethod(TSMethodEntity method, + {required UniqueNamer parentNamer, required 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 + final (:id, name: dartName) = parentNamer.makeUnique(name, 'fun'); + + final params = method.parameters.toDart; + + final typeParams = method.typeParameters?.toDart; + + final (:isStatic, isReadonly: _, :scope) = + _parseModifiers(method.modifiers); + + ReferredType? methodType; + if (method.type case final type? when ts.isTypeReferenceNode(type)) { + // check if + final referredType = type as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + + final methodDeclaration = MethodDeclaration( + name: name, + dartName: dartName, + id: id, + scope: scope, + static: isStatic, + parameters: params.map((t) { + ReferredType? paramType; + final paramRawType = t.type; + if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { + final referredType = ty as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + return _transformParameter(t, paramType); + }).toList(), + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + returnType: methodType ?? + (method.type != null + ? _transformType(method.type!) + : BuiltinType.anyType), + isNullable: (method.kind == TSSyntaxKind.MethodSignature) && + (method as TSMethodSignature).questionToken != null); + methodDeclaration.parent = parent; + return methodDeclaration; + } + + ConstructorDeclaration _transformConstructor(TSConstructorEntity constructor, + {required UniqueNamer parentNamer}) { + final name = constructor.name?.text; + final (:id, name: dartName) = + parentNamer.makeUnique(name ?? '', 'constructor'); + + final params = constructor.parameters.toDart; + + final ( + isStatic: _, + isReadonly: _, + :scope + ) = (constructor.isA() || + constructor.kind == TSSyntaxKind.Constructor) + ? _parseModifiers((constructor as TSConstructorDeclaration).modifiers) + : (isStatic: false, isReadonly: false, scope: DeclScope.public); + + return ConstructorDeclaration( + id: id, + dartName: dartName.isEmpty ? null : dartName, + name: name, + parameters: params.map(_transformParameter).toList(), + scope: scope); + } + + MethodDeclaration _transformCallSignature( + TSCallSignatureDeclaration callSignature, + {required UniqueNamer parentNamer, + required TypeDeclaration parent}) { + final (:id, name: dartName) = parentNamer.makeUnique('call', 'fun'); + + final params = callSignature.parameters.toDart; + + final typeParams = callSignature.typeParameters?.toDart; + + ReferredType? methodType; + if (callSignature.type case final type? when ts.isTypeReferenceNode(type)) { + // check if + final referredType = type as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + + final methodDeclaration = MethodDeclaration( + name: 'call', + dartName: dartName, + id: id, + parameters: params.map(_transformParameter).toList(), + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + returnType: methodType ?? + (callSignature.type != null + ? _transformType(callSignature.type!) + : BuiltinType.anyType)); + methodDeclaration.parent = parent; + return methodDeclaration; + } + + // TODO: Handling overloading of indexers + (OperatorDeclaration, OperatorDeclaration?) _transformIndexer( + TSIndexSignatureDeclaration indexSignature, + {required TypeDeclaration parent}) { + final params = indexSignature.parameters.toDart; + + final typeParams = indexSignature.typeParameters?.toDart; + + final (:isStatic, :isReadonly, :scope) = + _parseModifiers(indexSignature.modifiers); + + ReferredType? indexerType; + if (indexSignature.type case final type when ts.isTypeReferenceNode(type)) { + // check if + final referredType = type as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + + final getOperatorDeclaration = OperatorDeclaration( + kind: OperatorKind.squareBracket, + parameters: params.map(_transformParameter).toList(), + returnType: indexerType ?? _transformType(indexSignature.type), + scope: scope, + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + static: isStatic); + final setOperatorDeclaration = isReadonly + ? OperatorDeclaration( + kind: OperatorKind.squareBracketSet, + parameters: params.map(_transformParameter).toList(), + returnType: indexerType ?? _transformType(indexSignature.type), + scope: scope, + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + static: isStatic) + : null; + + getOperatorDeclaration.parent = parent; + setOperatorDeclaration?.parent = parent; + return (getOperatorDeclaration, setOperatorDeclaration); + } + + MethodDeclaration _transformGetter(TSGetAccessorDeclaration getter, + {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + final name = getter.name.text; + final (:id, name: dartName) = parentNamer.makeUnique(name, 'get'); + + final params = getter.parameters.toDart; + + final typeParams = getter.typeParameters?.toDart; + + final (isStatic: _, isReadonly: _, :scope) = + _parseModifiers(getter.modifiers); + + ReferredType? methodType; + if (getter.type case final type? when ts.isTypeReferenceNode(type)) { + // check if + final referredType = type as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + + final methodDeclaration = MethodDeclaration( + name: name, + dartName: dartName, + id: id, + kind: MethodKind.getter, + scope: scope, + parameters: params.map(_transformParameter).toList(), + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + returnType: methodType ?? + (getter.type != null + ? _transformType(getter.type!) + : BuiltinType.anyType)); + methodDeclaration.parent = parent; + return methodDeclaration; + } + + MethodDeclaration _transformSetter(TSSetAccessorDeclaration setter, + {required UniqueNamer parentNamer, required TypeDeclaration parent}) { + final name = setter.name.text; + final (:id, name: dartName) = parentNamer.makeUnique(name, 'set'); + + final params = setter.parameters.toDart; + + final typeParams = setter.typeParameters?.toDart; + + final (isStatic: _, isReadonly: _, :scope) = + _parseModifiers(setter.modifiers); + + final methodDeclaration = MethodDeclaration( + name: name, + dartName: dartName, + kind: MethodKind.setter, + id: id, + parameters: params.map((t) { + ReferredType? paramType; + final paramRawType = t.type; + if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { + final referredType = ty as TSTypeReferenceNode; + if (referredType.typeName.text == 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); + } + return _transformParameter(t, paramType); + }).toList(), + scope: scope, + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + returnType: setter.type != null + ? _transformType(setter.type!) + : BuiltinType.anyType); + methodDeclaration.parent = parent; + return methodDeclaration; + } + + FunctionDeclaration _transformFunction(TSFunctionDeclaration function) { + final name = function.name.text; + + final modifiers = function.modifiers.toDart; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + final params = function.parameters.toDart; + + final typeParams = function.typeParameters?.toDart; + + final (id: id, name: uniqueName) = namer.makeUnique(name, 'fun'); + + return FunctionDeclaration( + name: name, + id: id, + dartName: uniqueName, + exported: isExported, + parameters: params.map(_transformParameter).toList(), + typeParameters: + typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], + returnType: function.type != null + ? _transformType(function.type!) + : BuiltinType.anyType); + } + + List _transformVariable(TSVariableStatement variable) { + // get the modifier of the declaration + final modifiers = variable.modifiers.toDart; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + var modifier = VariableModifier.$var; + + if ((variable.declarationList.flags & TSNodeFlags.Const) != 0) { + modifier = VariableModifier.$const; + } else if ((variable.declarationList.flags & TSNodeFlags.Let) != 0) { + modifier = VariableModifier.let; + } + + return variable.declarationList.declarations.toDart.map((d) { + namer.markUsed(d.name.text); + return VariableDeclaration( + name: d.name.text, + type: d.type == null ? BuiltinType.anyType : _transformType(d.type!), + modifier: modifier, + exported: isExported); + }).toList(); + } + EnumDeclaration _transformEnum(TSEnumDeclaration enumeration) { final modifiers = enumeration.modifiers?.toDart; final isExported = modifiers?.any((m) { @@ -156,31 +642,6 @@ class Transformer { return stringLiteral.text; } - List _transformVariable(TSVariableStatement variable) { - // get the modifier of the declaration - final modifiers = variable.modifiers.toDart; - final isExported = modifiers.any((m) { - return m.kind == TSSyntaxKind.ExportKeyword; - }); - - var modifier = VariableModifier.$var; - - if ((variable.declarationList.flags & TSNodeFlags.Const) != 0) { - modifier = VariableModifier.$const; - } else if ((variable.declarationList.flags & TSNodeFlags.Let) != 0) { - modifier = VariableModifier.let; - } - - return variable.declarationList.declarations.toDart.map((d) { - namer.markUsed(d.name.text); - return VariableDeclaration( - name: d.name.text, - type: d.type == null ? BuiltinType.anyType : _transformType(d.type!), - modifier: modifier, - exported: isExported); - }).toList(); - } - TypeAliasDeclaration _transformTypeAlias(TSTypeAliasDeclaration typealias) { final name = typealias.name.text; @@ -202,39 +663,13 @@ class Transformer { exported: isExported); } - FunctionDeclaration _transformFunction(TSFunctionDeclaration function) { - final name = function.name.text; - - final modifiers = function.modifiers.toDart; - final isExported = modifiers.any((m) { - return m.kind == TSSyntaxKind.ExportKeyword; - }); - - final params = function.parameters.toDart; - - final typeParams = function.typeParameters?.toDart; - - final (id: id, name: uniqueName) = namer.makeUnique(name, 'fun'); - - return FunctionDeclaration( - name: name, - id: id, - dartName: uniqueName, - exported: isExported, - parameters: params.map(_transformParameter).toList(), - typeParameters: - typeParams?.map(_transformTypeParamDeclaration).toList() ?? [], - returnType: function.type != null - ? _transformType(function.type!) - : BuiltinType.anyType); - } - - ParameterDeclaration _transformParameter(TSParameterDeclaration parameter) { - final type = parameter.type != null + ParameterDeclaration _transformParameter(TSParameterDeclaration parameter, + [Type? type]) { + type ??= parameter.type != null ? _transformType(parameter.type!, parameter: true) : BuiltinType.anyType; final isOptional = parameter.questionToken != null; - final isVariardic = parameter.dotDotDotToken != null; + final isVariadic = parameter.dotDotDotToken != null; // what kind of parameter is this switch (parameter.name.kind) { @@ -242,7 +677,7 @@ class Transformer { return ParameterDeclaration( name: (parameter.name as TSIdentifier).text, type: type, - variardic: isVariardic, + variadic: isVariadic, optional: isOptional); default: // TODO: Support Destructured Object Parameters @@ -263,7 +698,6 @@ class Transformer { /// Parses the type /// - /// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`) /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types Type _transformType(TSTypeNode type, {bool parameter = false, bool typeArg = false}) { @@ -271,81 +705,11 @@ class Transformer { case TSSyntaxKind.TypeReference: final refType = type as TSTypeReferenceNode; - final name = refType.typeName.text; + final typeName = refType.typeName; final typeArguments = refType.typeArguments?.toDart; - var declarationsMatching = nodeMap.findByName(name); - - if (declarationsMatching.isEmpty) { - // check if builtin - // TODO(https://github.com/dart-lang/web/issues/380): A better name - // for this, and adding support for "supported declarations" - // (also a better name for that) - final supportedType = BuiltinType.referred(name, - typeParams: (typeArguments ?? []) - .map((t) => getJSTypeAlternative(_transformType(t))) - .toList()); - if (supportedType case final resultType?) { - return resultType; - } - - final symbol = typeChecker.getSymbolAtLocation(refType.typeName); - final declarations = symbol?.getDeclarations(); - - // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? - // TODO(https://github.com/dart-lang/web/issues/387): Some declarations may not be defined on file, - // and may be from an import statement - // We should be able to handle these - final declaration = declarations?.toDart.first; - - if (declaration == null) { - 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 (declaration.kind == TSSyntaxKind.TypeParameter) { - return GenericType(name: name); - } - - transform(declaration); - - declarationsMatching = nodeMap.findByName(name); - } - - // 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; - } - - return firstNode.asReferredType( - (typeArguments ?? []) - .map((type) => _transformType(type, typeArg: true)) - .toList(), - ); - // TODO: Union types are also anonymous by design - // Unless we are making typedefs for them, we should - // try to handle not making multiple of them for a given use-case + return _getTypeFromDeclaration(typeName, typeArguments, + typeArg: typeArg); case TSSyntaxKind.UnionType: final unionType = type as TSUnionTypeNode; // TODO: Unions @@ -383,12 +747,23 @@ class Transformer { isNullable: isNullable); } + final expectedId = + ID(type: 'type', name: types.map((t) => t.id.name).join('|')); + + if (typeMap.containsKey(expectedId.toString())) { + return typeMap[expectedId.toString()] as UnionType; + } + final (id: _, name: name) = namer.makeUnique('AnonymousUnion', 'type'); // TODO: Handle similar types here... - return HomogenousEnumType( + final homogenousEnumType = HomogenousEnumType( types: nonNullLiteralTypes, isNullable: isNullable, name: name); + + return typeMap.putIfAbsent( + expectedId.toString(), () => homogenousEnumType) + as HomogenousEnumType; } return UnionType(types: types); @@ -448,6 +823,95 @@ class Transformer { } } + Type _transformTypeExpressionWithTypeArguments( + TSExpressionWithTypeArguments type) { + if (type.expression.kind == TSSyntaxKind.Identifier) { + final identifier = type.expression as TSIdentifier; + + final getTypeFromDeclaration = + _getTypeFromDeclaration(identifier, type.typeArguments?.toDart); + + return getTypeFromDeclaration; + } else { + throw UnimplementedError("The given type expression's expression of kind " + '${type.expression.kind} is not supported yet'); + } + } + + Type _getTypeFromDeclaration( + TSIdentifier typeName, List? typeArguments, + {bool typeArg = false}) { + final name = typeName.text; + var declarationsMatching = nodeMap.findByName(name); + + if (declarationsMatching.isEmpty) { + // check if builtin + // TODO(https://github.com/dart-lang/web/issues/380): A better name + // for this, and adding support for "supported declarations" + // (also a better name for that) + final supportedType = BuiltinType.referred(name, + typeParams: (typeArguments ?? []) + .map((t) => getJSTypeAlternative(_transformType(t))) + .toList()); + if (supportedType case final resultType?) { + return resultType; + } + + final symbol = typeChecker.getSymbolAtLocation(typeName); + final declarations = symbol?.getDeclarations(); + + // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? + // TODO(https://github.com/dart-lang/web/issues/387): Some declarations may not be defined on file, + // and may be from an import statement + // We should be able to handle these + final declaration = declarations?.toDart.first; + + if (declaration == null) { + 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 (declaration.kind == TSSyntaxKind.TypeParameter) { + return GenericType(name: name); + } + + transform(declaration); + + declarationsMatching = nodeMap.findByName(name); + } + + // 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; + } + + return firstNode.asReferredType( + (typeArguments ?? []) + .map((type) => _transformType(type, typeArg: true)) + .toList(), + ); + } + NodeMap filter() { final filteredDeclarations = NodeMap(); @@ -484,32 +948,73 @@ class Transformer { /// Given an already filtered declaration [decl], /// filter out dependencies of [decl] recursively /// and return them as a declaration map - NodeMap _getDependenciesOfDecl([Node? decl]) { + NodeMap _getDependenciesOfDecl(Node? decl, [NodeMap? context]) { + NodeMap getCallableDependencies(CallableDeclaration callable) { + return NodeMap({ + for (final node in callable.parameters.map((p) => p.type)) + node.id.toString(): node, + for (final node in callable.typeParameters + .map((p) => p.constraint) + .whereType()) + node.id.toString(): node, + callable.returnType.id.toString(): callable.returnType + }); + } + final filteredDeclarations = NodeMap(); switch (decl) { case final VariableDeclaration v: if (v.type is! BuiltinType) filteredDeclarations.add(v.type); break; - case final FunctionDeclaration f: - if (f.returnType is! BuiltinType) { - filteredDeclarations.add(f.returnType); - } - filteredDeclarations.addAll({ - for (final node in f.parameters.map((p) => p.type)) - node.id.toString(): node - }); - filteredDeclarations.addAll({ - for (final node - in f.typeParameters.map((p) => p.constraint).whereType()) - node.id.toString(): node - }); + case final CallableDeclaration f: + filteredDeclarations.addAll(getCallableDependencies(f)); break; case final EnumDeclaration _: break; case final TypeAliasDeclaration t: if (decl.type is! BuiltinType) filteredDeclarations.add(t.type); break; + case final TypeDeclaration t: + for (final con in t.constructors) { + filteredDeclarations.addAll({ + for (final param in con.parameters.map((p) => p.type)) + param.id.toString(): param + }); + } + for (final methods in t.methods) { + filteredDeclarations.addAll(getCallableDependencies(methods)); + } + for (final operators in t.operators) { + filteredDeclarations.addAll(getCallableDependencies(operators)); + } + filteredDeclarations.addAll({ + for (final prop in t.properties + .map((p) => p.type) + .where((p) => p is! BuiltinType)) + prop.id.toString(): prop, + }); + switch (t) { + case ClassDeclaration( + extendedType: final extendedType, + implementedTypes: final implementedTypes + ): + if (extendedType case final ext? when ext is! BuiltinType) { + filteredDeclarations.add(ext); + } + filteredDeclarations.addAll({ + for (final impl + in implementedTypes.where((i) => i is! BuiltinType)) + impl.id.toString(): impl, + }); + break; + case InterfaceDeclaration(extendedTypes: final extendedTypes): + filteredDeclarations.addAll({ + for (final impl in extendedTypes.where((i) => i is! BuiltinType)) + impl.id.toString(): impl, + }); + break; + } // TODO: We can make (DeclarationAssociatedType) and use that // rather than individual type names case final HomogenousEnumType hu: @@ -526,15 +1031,20 @@ class Transformer { break; case final ReferredType r: filteredDeclarations.add(r.declaration); + break; default: print('WARN: The given node type ${decl.runtimeType.toString()} ' 'is not supported for filtering. Skipping...'); break; } + filteredDeclarations + .removeWhere((k, v) => context?.containsKey(k) ?? false); + if (filteredDeclarations.isNotEmpty) { final otherDecls = filteredDeclarations.entries - .map((e) => _getDependenciesOfDecl(e.value)) + .map((e) => _getDependenciesOfDecl( + e.value, NodeMap({...(context ?? {}), ...filteredDeclarations}))) .reduce((value, element) => value..addAll(element)); filteredDeclarations.addAll(otherDecls); @@ -543,3 +1053,34 @@ class Transformer { return filteredDeclarations; } } + +({bool isReadonly, bool isStatic, DeclScope scope}) _parseModifiers( + [TSNodeArray? modifiers]) { + var isReadonly = false; + var isStatic = false; + var scope = DeclScope.public; + + for (final modifier in modifiers?.toDart ?? []) { + switch (modifier.kind) { + case TSSyntaxKind.StaticKeyword: + isStatic = true; + break; + case TSSyntaxKind.ReadonlyKeyword: + isReadonly = true; + break; + case TSSyntaxKind.PrivateKeyword: + scope = DeclScope.private; + break; + case TSSyntaxKind.ProtectedKeyword: + scope = DeclScope.protected; + break; + case TSSyntaxKind.PublicKeyword: + scope = DeclScope.public; + break; + default: + break; + } + } + + return (isStatic: isStatic, isReadonly: isReadonly, scope: scope); +} diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart index d3754346..af8fd98c 100644 --- a/web_generator/lib/src/js/typescript.dart +++ b/web_generator/lib/src/js/typescript.dart @@ -24,6 +24,12 @@ external void forEachChild( TSNode node, TSNodeCallback cbNode, [TSNodeArrayCallback? cdNodes]); +@JS() +external bool isTypeReferenceNode(TSNode node); + +@JS() +external bool isThisTypeNode(TSNode node); + @JS('CompilerOptions') extension type TSCompilerOptions._(JSObject _) implements JSObject { external TSCompilerOptions({bool? allowJs, bool? declaration}); diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index c4acbd03..ce841378 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -27,6 +27,16 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind TypeAliasDeclaration = TSSyntaxKind._(265); static const TSSyntaxKind Parameter = TSSyntaxKind._(169); static const TSSyntaxKind EnumDeclaration = TSSyntaxKind._(266); + static const TSSyntaxKind PropertyDeclaration = TSSyntaxKind._(172); + static const TSSyntaxKind MethodDeclaration = TSSyntaxKind._(174); + static const TSSyntaxKind Constructor = TSSyntaxKind._(176); + static const TSSyntaxKind GetAccessor = TSSyntaxKind._(177); + static const TSSyntaxKind SetAccessor = TSSyntaxKind._(178); + static const TSSyntaxKind IndexSignature = TSSyntaxKind._(181); + static const TSSyntaxKind PropertySignature = TSSyntaxKind._(171); + static const TSSyntaxKind MethodSignature = TSSyntaxKind._(173); + static const TSSyntaxKind CallSignature = TSSyntaxKind._(179); + static const TSSyntaxKind ConstructSignature = TSSyntaxKind._(180); /// expressions static const TSSyntaxKind NumericLiteral = TSSyntaxKind._(9); @@ -40,6 +50,14 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind DeclareKeyword = TSSyntaxKind._(138); static const TSSyntaxKind ExtendsKeyword = TSSyntaxKind._(96); static const TSSyntaxKind ImplementsKeyword = TSSyntaxKind._(119); + static const TSSyntaxKind AbstractKeyword = TSSyntaxKind._(128); + + // keywords for scope + static const TSSyntaxKind PrivateKeyword = TSSyntaxKind._(123); + static const TSSyntaxKind ProtectedKeyword = TSSyntaxKind._(124); + static const TSSyntaxKind PublicKeyword = TSSyntaxKind._(125); + static const TSSyntaxKind StaticKeyword = TSSyntaxKind._(126); + static const TSSyntaxKind ReadonlyKeyword = TSSyntaxKind._(148); // types that are keywords static const TSSyntaxKind StringKeyword = TSSyntaxKind._(154); @@ -59,6 +77,7 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind TypeReference = TSSyntaxKind._(183); static const TSSyntaxKind ArrayType = TSSyntaxKind._(188); static const TSSyntaxKind LiteralType = TSSyntaxKind._(201); + static const TSSyntaxKind ThisType = TSSyntaxKind._(197); /// Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); @@ -85,6 +104,12 @@ extension type TSNode._(JSObject _) implements JSObject { external TSSourceFile getSourceFile(); } +@JS('Token') +extension type TSToken._(JSObject _) implements TSNode { + @redeclare + external TSNode get kind; +} + @JS('TypeNode') extension type TSTypeNode._(JSObject _) implements TSNode {} @@ -172,16 +197,169 @@ extension type TSVariableDeclaration._(JSObject _) implements TSDeclaration { external TSTypeNode? get type; } -@JS('FunctionDeclaration') -extension type TSFunctionDeclaration._(JSObject _) implements TSDeclaration { - external TSIdentifier get name; - external TSTypeNode? get type; - external TSNode? get asteriskToken; +@JS('SignatureDeclarationBase') +extension type TSSignatureDeclarationBase._(JSObject _) + implements TSDeclaration { external TSNodeArray get parameters; external TSNodeArray? get typeParameters; + external TSTypeNode? get type; + external TSIdentifier? get name; +} + +@JS('FunctionLikeDeclarationBase') +extension type TSFunctionLikeDeclarationBase._(JSObject _) + implements TSSignatureDeclarationBase { + external TSNode? get asteriskToken; +} + +@JS('FunctionDeclaration') +extension type TSFunctionDeclaration._(JSObject _) + implements TSFunctionLikeDeclarationBase { + external TSIdentifier get name; external TSNodeArray get modifiers; } +/// A common API for Classes and Interfaces +extension type TSObjectDeclaration._(JSObject _) + implements TSDeclaration, TSStatement { + // TODO: May be undefined for classes in default exports + external TSIdentifier get name; + external TSNodeArray? get modifiers; + external TSNodeArray? get typeParameters; + external TSNodeArray? get heritageClauses; + external TSNodeArray get members; +} + +// TODO: Will we consider class expressions? +@JS('ClassDeclaration') +extension type TSClassDeclaration._(JSObject _) + implements TSObjectDeclaration {} + +@JS('InterfaceDeclaration') +extension type TSInterfaceDeclaration._(JSObject _) + implements TSObjectDeclaration {} + +@JS('HeritageClause') +extension type TSHeritageClause._(JSObject _) implements TSNode { + external TSObjectDeclaration get parent; + external TSSyntaxKind get token; + external TSNodeArray get types; +} + +@JS('ExpressionWithTypeArguments') +extension type TSExpressionWithTypeArguments._(JSObject _) + implements TSExpression, TSTypeNode { + external TSExpression get expression; + external TSNodeArray? get typeArguments; +} + +extension type TSPropertyEntity._(JSObject _) implements TSDeclaration { + external TSNodeArray? get modifiers; + external TSIdentifier get name; + external TSToken? get questionToken; + external TSTypeNode? get type; +} + +extension type TSMethodEntity._(JSObject _) + implements TSFunctionLikeDeclarationBase { + external TSNodeArray? get modifiers; + external TSIdentifier get name; +} + +extension type TSConstructorEntity._(JSObject _) + implements TSSignatureDeclarationBase { + external TSIdentifier? get name; +} + +@JS('ClassElement') +extension type TSClassElement._(JSObject _) implements TSDeclaration { + external TSIdentifier? get name; +} + +@JS('PropertyDeclaration') +extension type TSPropertyDeclaration._(JSObject _) + implements TSClassElement, TSPropertyEntity { + @redeclare + external TSIdentifier get name; +} + +@JS('MethodDeclaration') +extension type TSMethodDeclaration._(JSObject _) + implements TSMethodEntity, TSClassElement { + @redeclare + external TSIdentifier get name; +} + +@JS('ConstructorDeclaration') +extension type TSConstructorDeclaration._(JSObject _) + implements + TSConstructorEntity, + TSFunctionLikeDeclarationBase, + TSClassElement { + @redeclare + external TSIdentifier? get name; + external TSNodeArray? get modifiers; +} + +@JS('TypeElement') +extension type TSTypeElement._(JSObject _) implements TSDeclaration { + external TSIdentifier? get name; + external TSToken? get questionToken; +} + +@JS('PropertySignature') +extension type TSPropertySignature._(JSObject _) + implements TSTypeElement, TSPropertyEntity { + @redeclare + external TSIdentifier get name; + @redeclare + external TSToken? get questionToken; +} + +@JS('MethodSignature') +extension type TSMethodSignature._(JSObject _) + implements TSMethodEntity, TSTypeElement { + @redeclare + external TSIdentifier get name; +} + +@JS('CallSignatureDeclaration') +extension type TSCallSignatureDeclaration._(JSObject _) + implements TSSignatureDeclarationBase, TSTypeElement { + @redeclare + external TSIdentifier? get name; +} + +@JS('ConstructSignatureDeclaration') +extension type TSConstructSignatureDeclaration._(JSObject _) + implements TSConstructorEntity, TSTypeElement { + @redeclare + external TSIdentifier? get name; +} + +@JS('IndexSignatureDeclaration') +extension type TSIndexSignatureDeclaration._(JSObject _) + implements TSSignatureDeclarationBase, TSClassElement, TSTypeElement { + external TSNodeArray? get modifiers; + external TSTypeNode get type; + external TSIdentifier? get name; +} + +// TODO: ObjectLiteralElement implementation as well +@JS('GetAccessorDeclaration') +extension type TSGetAccessorDeclaration._(JSObject _) + implements TSFunctionLikeDeclarationBase, TSClassElement, TSTypeElement { + external TSIdentifier get name; + external TSNodeArray? get modifiers; +} + +@JS('SetAccessorDeclaration') +extension type TSSetAccessorDeclaration._(JSObject _) + implements TSFunctionLikeDeclarationBase, TSClassElement, TSTypeElement { + external TSIdentifier get name; + external TSNodeArray? get modifiers; +} + @JS('TypeAliasDeclaration') extension type TSTypeAliasDeclaration._(JSObject _) implements TSDeclaration, TSStatement { diff --git a/web_generator/test/assets/test_config.dart b/web_generator/test/assets/test_config.dart index d9e1fb8c..d1564c96 100644 --- a/web_generator/test/assets/test_config.dart +++ b/web_generator/test/assets/test_config.dart @@ -54,7 +54,6 @@ external User get user1; external User get user2; @_i1.JS() external User get adminUser; -typedef User = String; extension type const UserRole._(int _) { static const UserRole Guest = UserRole._(0); @@ -64,3 +63,4 @@ extension type const UserRole._(int _) { static const UserRole Administrator = UserRole._(3); } +typedef User = String; diff --git a/web_generator/test/assets/test_no_config.dart b/web_generator/test/assets/test_no_config.dart index e947ccb8..305d2c6e 100644 --- a/web_generator/test/assets/test_no_config.dart +++ b/web_generator/test/assets/test_no_config.dart @@ -49,7 +49,6 @@ external User get user1; external User get user2; @_i1.JS() external User get adminUser; -typedef User = String; extension type const UserRole._(int _) { static const UserRole Guest = UserRole._(0); @@ -59,6 +58,7 @@ extension type const UserRole._(int _) { static const UserRole Administrator = UserRole._(3); } +typedef User = String; extension type const HttpStatusCode._(String _) { static const HttpStatusCode OK = HttpStatusCode._('200 OK'); diff --git a/web_generator/test/integration/interop_gen/classes_expected.dart b/web_generator/test/integration/interop_gen/classes_expected.dart new file mode 100644 index 00000000..10859352 --- /dev/null +++ b/web_generator/test/integration/interop_gen/classes_expected.dart @@ -0,0 +1,322 @@ +// 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; +import 'package:meta/meta.dart' as _i2; + +extension type Configuration._(_i1.JSObject _) implements _i1.JSObject { + external Configuration( + String version, + String apiUrl, + ); + + external String get version; + external String get apiUrl; +} +extension type Product._(_i1.JSObject _) implements _i1.JSObject { + external Product( + String name, + num price, + num quantity, + ); + + external String get name; + external set price(num newPrice); + external double get price; + external set quantity(num newQuantity); + external double get quantity; + external double get totalPrice; +} +extension type User._(_i1.JSObject _) implements _i1.JSObject { + external User( + num id, + String username, + String email, + ); + + external double id; + + external String greet(); + external String getEmail(); +} +extension type Shape._(_i1.JSObject _) implements _i1.JSObject {} +extension type Shape2D._(_i1.JSObject _) implements Shape { + external double? get sides; + external double get perimeter; + external double get area; +} +extension type Shape3D._(_i1.JSObject _) implements Shape { + external double get volume; + external double surfaceArea(); +} +extension type Rectangle._(_i1.JSObject _) implements Shape2D { + external Rectangle( + num length, + num width, + ); + + external double length; + + external double width; + + external double sides; + + @_i2.redeclare + external double get area; + @_i2.redeclare + external double get perimeter; +} +extension type Square._(_i1.JSObject _) implements Rectangle { + external Square(num length); + + external double length; +} +extension type Circle._(_i1.JSObject _) implements Shape2D { + external Circle(num radius); + + external double radius; + + @_i2.redeclare + external double get area; + @_i2.redeclare + external double get perimeter; +} +extension type Prism._(_i1.JSObject _) implements Shape3D { + external Prism( + S surface, + num height, + ); + + external double height; + + external S get surface; + @_i2.redeclare + external double get volume; + @_i2.redeclare + external double surfaceArea(); +} +extension type Pyramid._(_i1.JSObject _) implements Shape3D { + external Pyramid( + S surface, + num height, + ); + + external double height; + + external S get surface; + @_i2.redeclare + external double get volume; + @_i2.redeclare + external double surfaceArea(); +} +extension type Cylinder._(_i1.JSObject _) implements Prism { + external Cylinder( + num radius, + num height, + ); + + external double radius; + + @_i2.redeclare + external double surfaceArea(); +} +extension type Cuboid._(_i1.JSObject _) implements Prism { + external Cuboid( + num length, + num width, + num height, + ); + + external double length; + + external double width; + + external double height; +} +extension type Cube._(_i1.JSObject _) implements Prism { + external Cube(num length); + + external double length; + + @_i2.redeclare + external double get volume; + @_i2.redeclare + external double surfaceArea(); +} +extension type Cone._(_i1.JSObject _) implements Pyramid { + external Cone( + num radius, + num height, + ); + + external double radius; + + external double height; + + @_i2.redeclare + external double surfaceArea(); +} +extension type Sphere._(_i1.JSObject _) implements Shape3D { + external Sphere(num radius); + + external double radius; + + @_i2.redeclare + external double get volume; + @_i2.redeclare + external double surfaceArea(); +} +extension type Hemi._(_i1.JSObject _) implements Shape3D { + external Hemi(S shape); + + external S get shape; + external static Prism prism(Prism p); + @_i2.redeclare + external double get volume; + @_i2.redeclare + external double surfaceArea(); +} +typedef HemiSphere = Hemi; +extension type Point2D._(_i1.JSObject _) implements _i1.JSObject { + external double x; + + external double y; +} +extension type Point3D._(_i1.JSObject _) implements _i1.JSObject { + external double x; + + external double y; + + external double z; +} +@_i1.JS() +external Point2D get origin2D; +@_i1.JS() +external Point3D get origin3D; +extension type Vector._(_i1.JSObject _) implements _i1.JSObject { + external double get magnitude; + external double get directionAngle; +} +extension type Vector2D._(_i1.JSObject _) implements Vector { + external Vector2D( + num x, + num y, + ); + + external double x; + + external double y; + + external Vector2D unit(); + @_i2.redeclare + external double get magnitude; + @_i2.redeclare + external double get directionAngle; + external Point2D moveFrom(Point2D point); + external static Vector2D from( + num magnitude, + num at, + ); + external static Vector2D fromPoints( + Point2D start, + Point2D end, + ); +} +extension type DirectionAngles._(_i1.JSObject _) implements _i1.JSObject { + external double alpha; + + external double beta; + + external double gamma; +} +extension type Vector3D._(_i1.JSObject _) implements Vector { + external Vector3D( + num x, + num y, + num z, + ); + + external double x; + + external double y; + + external double z; + + external Vector3D unit(); + @_i2.redeclare + external double get magnitude; + external DirectionAngles get directionAngles; + @_i2.redeclare + external double get directionAngle; + external Point3D moveFrom(Point3D point); + external static Vector3D from( + num magnitude, + DirectionAngles at, + ); + external static Vector3D fromPoints( + Point3D start, + Point3D end, + ); +} +@_i1.JS() +external Circle drawCircle( + Point2D center, + num radius, +); +@_i1.JS() +external Square drawSquare( + Point2D start, + num length, [ + num? angle, +]); +@_i1.JS() +external Sphere drawSphere(Point3D center); +extension type EpahsImpl._(_i1.JSObject _) + implements Epahs { + external EpahsImpl( + String name, [ + AnonymousUnion$1? type, + ]); + + external factory EpahsImpl.$1(Epahs config); + + external String name; + + external TMeta? metadata; + + @_i2.redeclare + external String get id; + @_i2.redeclare + external void onUpdate(Epahs prev); + external String get location; + external set location(String value); + @_i2.redeclare + external double area(); + @_i1.JS('area') + external String area$1(AnonymousUnion 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'); + + static const AnonymousUnion$1 rectangle = AnonymousUnion$1._('rectangle'); + + static const AnonymousUnion$1 polygon = AnonymousUnion$1._('polygon'); +} +extension type Epahs._(_i1.JSObject _) + implements _i1.JSObject { + external String name; + + external String get id; + external double area(); + @_i1.JS('area') + external String area$1(AnonymousUnion unit); + external _i1.JSFunction? get onUpdate; +} +extension type const AnonymousUnion._(String _) { + static const AnonymousUnion cm2 = AnonymousUnion._('cm2'); + + static const AnonymousUnion in2 = AnonymousUnion._('in2'); +} diff --git a/web_generator/test/integration/interop_gen/classes_input.d.ts b/web_generator/test/integration/interop_gen/classes_input.d.ts new file mode 100644 index 00000000..d36a4237 --- /dev/null +++ b/web_generator/test/integration/interop_gen/classes_input.d.ts @@ -0,0 +1,177 @@ +export declare class Configuration { + readonly version: string; + readonly apiUrl: string; + constructor(version: string, apiUrl: string); +} +export declare class Product { + private _name; + private _price; + private _quantity; + constructor(name: string, price: number, quantity: number); + get name(): string; + set price(newPrice: number); + get price(): number; + set quantity(newQuantity: number); + get quantity(): number; + get totalPrice(): number; +} +export declare class User { + id: number; + protected username: string; + private email; + constructor(id: number, // Public property + username: string, // Protected property + email: string); + greet(): string; + getEmail(): string; +} +export interface Shape { +} +export interface Shape2D extends Shape { + get perimeter(): number; + get area(): number; + readonly sides?: number; +} +export interface Shape3D extends Shape { + get volume(): number; + surfaceArea(): number; +} +export declare class Rectangle implements Shape2D { + length: number; + width: number; + sides: number; + constructor(length: number, width: number); + get area(): number; + get perimeter(): number; +} +export declare class Square extends Rectangle { + length: number; + constructor(length: number); +} +export declare class Circle implements Shape2D { + radius: number; + constructor(radius: number); + get area(): number; + get perimeter(): number; +} +export declare class Prism implements Shape3D { + readonly surface: S; + height: number; + constructor(surface: S, height: number); + get volume(): number; + surfaceArea(): number; +} +export declare class Pyramid implements Shape3D { + readonly surface: S; + height: number; + constructor(surface: S, height: number); + get volume(): number; + surfaceArea(): number; +} +export declare class Cylinder extends Prism { + radius: number; + constructor(radius: number, height: number); + surfaceArea(): number; +} +export declare class Cuboid extends Prism { + length: number; + width: number; + height: number; + constructor(length: number, width: number, height: number); +} +export declare class Cube extends Prism { + length: number; + constructor(length: number); + get volume(): number; + surfaceArea(): number; +} +export declare class Cone extends Pyramid { + radius: number; + height: number; + constructor(radius: number, height: number); + surfaceArea(): number; +} +export declare class Sphere implements Shape3D { + radius: number; + constructor(radius: number); + get volume(): number; + surfaceArea(): number; +} +export declare class Hemi implements Shape3D { + readonly shape: S; + constructor(shape: S); + static prism(p: Prism): Prism; + get volume(): number; + surfaceArea(): number; +} +export type HemiSphere = Hemi; +export interface Point2D { + x: number; + y: number; +} +export interface Point3D { + x: number; + y: number; + z: number; +} +export declare const origin2D: Point2D; +export declare const origin3D: Point3D; +export interface Vector { + get magnitude(): number; + get directionAngle(): number; +} +export declare class Vector2D implements Vector { + x: number; + y: number; + constructor(x: number, y: number); + unit(): Vector2D; + get magnitude(): number; + get directionAngle(): number; + moveFrom(point: Point2D): Point2D; + static from(magnitude: number, at: number): Vector2D; + static fromPoints(start: Point2D, end: Point2D): Vector2D; +} +export declare class Vector3D implements Vector { + x: number; + y: number; + z: number; + constructor(x: number, y: number, z: number); + unit(): Vector3D; + get magnitude(): number; + get directionAngles(): DirectionAngles; + get directionAngle(): number; + moveFrom(point: Point3D): Point3D; + static from(magnitude: number, at: DirectionAngles): Vector3D; + static fromPoints(start: Point3D, end: Point3D): Vector3D; +} +export interface DirectionAngles { + alpha: number; + beta: number; + gamma: number; +} +export declare function drawCircle(center: Point2D, radius: number): Circle; +export declare function drawSquare(start: Point2D, length: number, angle?: number): Square; +export declare function drawSphere(center: Point3D): Sphere; + +interface Epahs { + readonly id: string; + name: string; + area(): number; + area(unit: 'cm2' | 'in2'): string; + onUpdate?(prev: Epahs): void; +} +export declare class EpahsImpl implements Epahs { + readonly id: string; + name: string; + /* other decls in Shape */ + metadata?: TMeta; + constructor(name: string, type?: 'circle' | 'rectangle' | 'polygon'); + onUpdate?(prev: Epahs): void; + constructor(config: Epahs); + get location(): string; + set location(value: string); + area(): number; + area(unit: 'cm2' | 'in2'): string; + static getById(id: string): EpahsImpl; + toString(): string; +} diff --git a/web_generator/test/integration/interop_gen/interfaces_expected.dart b/web_generator/test/integration/interop_gen/interfaces_expected.dart new file mode 100644 index 00000000..41262e73 --- /dev/null +++ b/web_generator/test/integration/interop_gen/interfaces_expected.dart @@ -0,0 +1,80 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:js_interop' as _i1; + +extension type ILogger._(_i1.JSObject _) implements _i1.JSObject { + external AnonymousUnion? level; + + external String get name; + external void log(String message); + external void error(String message); + external _i1.JSFunction? get flush; +} +extension type Dictionary._(_i1.JSObject _) implements _i1.JSObject { + external String operator [](String key); +} +extension type Comparator._(_i1.JSObject _) + implements _i1.JSObject { + external double call( + T a, + T b, + ); +} +extension type Repository._(_i1.JSObject _) + implements _i1.JSObject { + external T findById(String id); + external void save(T entity); +} +extension type RepoConstructor._(_i1.JSObject _) implements _i1.JSObject { + external RepoConstructor(_i1.JSArray<_i1.JSString> args); +} +extension type AsyncService._(_i1.JSObject _) implements _i1.JSObject { + external _i1.JSPromise<_i1.JSAny?> fetchData(String url); + external _i1.JSPromise<_i1.JSBoolean> updateData( + String id, + String payload, + ); +} +extension type User._(_i1.JSObject _) implements _i1.JSObject { + external String id; + + external String email; + + external _i1.JSFunction? get describe; +} +extension type Admin._(_i1.JSObject _) implements User, ILogger { + external String role; + + external void grantPermission(String permission); +} +extension type Config._(_i1.JSObject _) implements _i1.JSObject { + external String env; +} +@_i1.JS('Config') +extension type Config$1._(_i1.JSObject _) implements _i1.JSObject { + external bool debug; +} +extension type SecureResource._(_i1.JSObject _) implements _i1.JSObject { + external String accessToken; + + external bool authenticate(); +} +@_i1.JS() +external Dictionary get dict; +@_i1.JS() +external LinkedList get rootList; +@_i1.JS() +external Comparator<_i1.JSNumber> get compareNumbers; +extension type const AnonymousUnion._(String _) { + static const AnonymousUnion debug = AnonymousUnion._('debug'); + + static const AnonymousUnion info = AnonymousUnion._('info'); + + static const AnonymousUnion warn = AnonymousUnion._('warn'); + + static const AnonymousUnion error = AnonymousUnion._('error'); +} +extension type LinkedList._(_i1.JSObject _) implements _i1.JSObject { + external LinkedList next(); +} diff --git a/web_generator/test/integration/interop_gen/interfaces_input.d.ts b/web_generator/test/integration/interop_gen/interfaces_input.d.ts new file mode 100644 index 00000000..63dcadab --- /dev/null +++ b/web_generator/test/integration/interop_gen/interfaces_input.d.ts @@ -0,0 +1,49 @@ +export interface ILogger { + readonly name: string; + level?: "debug" | "info" | "warn" | "error"; + log(message: string): void; + error(message: string): void; + flush?(): Promise; +} +export interface Dictionary { + [key: string]: string; +} +export interface Comparator { + (a: T, b: T): number; +} +export interface Repository { + findById(id: string): T; + save(entity: T): void; +} +export interface RepoConstructor { + new (args: string[]): any; +} +export interface AsyncService { + fetchData(url: string): Promise; + updateData(id: string, payload: string): Promise; +} +export interface User { + id: string; + email: string; + describe?(): string; +} +export interface Admin extends User, ILogger { + role: string; + grantPermission(permission: string): void; +} +export interface Config { + env: string; +} +export interface Config { + debug: boolean; +} +export interface SecureResource { + accessToken: string; + authenticate(): boolean; +} +interface LinkedList { + next(): this; +} +export declare const dict: Dictionary; +export declare const rootList: LinkedList; +export declare const compareNumbers: Comparator; diff --git a/web_generator/test/namer_test.dart b/web_generator/test/namer_test.dart new file mode 100644 index 00000000..d8187676 --- /dev/null +++ b/web_generator/test/namer_test.dart @@ -0,0 +1,58 @@ +import 'package:test/test.dart'; +import 'package:web_generator/src/interop_gen/namer.dart'; + +void main() { + group('Namer Test', () { + final namer = UniqueNamer(); + + test('Names', () { + var newName = namer.makeUnique('foo', 'fun'); + expectNameMatches(newName, 'foo', 'fun'); + + newName = namer.makeUnique('bar', 'fun'); + expectNameMatches(newName, 'bar', 'fun'); + + newName = namer.makeUnique('bar', 'var'); + expectNameMatches(newName, r'bar$1', 'var'); + + newName = namer.makeUnique('bar', 'var'); + expectNameMatches(newName, r'bar$2', 'var'); + + newName = namer.makeUnique('box', 'var'); + expectNameMatches(newName, 'box', 'var'); + + newName = namer.makeUnique('foo', 'fun'); + expectNameMatches(newName, r'foo$1', 'fun'); + }); + }); + + group('Scoped Namer Test', () { + final namer = ScopedUniqueNamer({'get', 'set'}); + + test('Names', () { + var newName = namer.makeUnique('foo', 'fun'); + expectNameMatches(newName, 'foo', 'fun'); + + newName = namer.makeUnique('bar', 'get'); + expectNameMatches(newName, 'bar', 'get'); + + newName = namer.makeUnique('bar', 'set'); + expectNameMatches(newName, 'bar', 'set'); + + newName = namer.makeUnique('bar', 'var'); + expectNameMatches(newName, r'bar$1', 'var'); + + newName = namer.makeUnique('box', 'var'); + expectNameMatches(newName, 'box', 'var'); + + newName = namer.makeUnique('foo', 'fun'); + expectNameMatches(newName, r'foo$1', 'fun'); + }); + }); +} + +void expectNameMatches( + ({ID id, String name}) name, String newName, String type) { + expect(name.name, newName); + expect(name.id.type, type); +}