diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart index fc5e53ec..c158715c 100644 --- a/web_generator/lib/src/ast/base.dart +++ b/web_generator/lib/src/ast/base.dart @@ -17,8 +17,9 @@ class Options {} class DeclarationOptions extends Options { bool override; + bool static; - DeclarationOptions({this.override = false}); + DeclarationOptions({this.override = false, this.static = false}); TypeOptions toTypeOptions({bool nullable = false}) => TypeOptions(nullable: nullable); diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart index 2494b5ee..2c7367b5 100644 --- a/web_generator/lib/src/ast/declarations.dart +++ b/web_generator/lib/src/ast/declarations.dart @@ -5,16 +5,30 @@ import 'package:code_builder/code_builder.dart'; import '../interop_gen/namer.dart'; +import '../js/typescript.types.dart'; import 'base.dart'; import 'builtin.dart'; import 'helpers.dart'; import 'types.dart'; -/// A declaration that defines a type -/// -// TODO: Add support for `ClassOrInterfaceDeclaration` -// once implementing namespaces and module support -sealed class TypeDeclaration extends NamedDeclaration +abstract class NestableDeclaration extends NamedDeclaration { + NestableDeclaration? get parent; + + String get qualifiedName => + parent != null ? '${parent!.qualifiedName}.$name' : name; + + String get completedDartName => parent != null + ? '${parent!.completedDartName}_${dartName ?? name}' + : (dartName ?? name); +} + +abstract class ParentDeclaration { + Set get nodes; +} + +/// A declaration that defines a type (class or interface) +/// which contains declarations +sealed class TypeDeclaration extends NestableDeclaration implements ExportableDeclaration { @override String name; @@ -25,6 +39,9 @@ sealed class TypeDeclaration extends NamedDeclaration @override final bool exported; + @override + NestableDeclaration? parent; + final List typeParameters; final List methods; @@ -43,7 +60,8 @@ sealed class TypeDeclaration extends NamedDeclaration this.methods = const [], this.properties = const [], this.operators = const [], - this.constructors = const []}); + this.constructors = const [], + this.parent}); ExtensionType _emit( [covariant DeclarationOptions? options, @@ -80,9 +98,12 @@ sealed class TypeDeclaration extends NamedDeclaration : BuiltinType.primitiveType(PrimitiveType.object, isNullable: false); return ExtensionType((e) => e - ..name = dartName ?? name + ..name = completedDartName ..annotations.addAll([ - if (dartName != null && dartName != name) generateJSAnnotation(name) + if (parent != null) + generateJSAnnotation(qualifiedName) + else if (dartName != null && dartName != name) + generateJSAnnotation(name) ]) ..primaryConstructorName = '_' ..representationDeclaration = RepresentationDeclaration((r) => r @@ -150,11 +171,13 @@ class VariableDeclaration extends FieldDeclaration ..type = MethodType.getter ..annotations.add(generateJSAnnotation()) ..external = true + ..static = options?.static ?? false ..returns = type.emit(options?.toTypeOptions())); } else { // getter and setter -> single variable return Field((f) => f ..external = true + ..static = options?.static ?? false ..name = name ..type = type.emit(options?.toTypeOptions()) ..annotations.add(generateJSAnnotation())); @@ -204,29 +227,18 @@ class FunctionDeclaration extends CallableDeclaration required this.returnType}); @override - Spec emit([DeclarationOptions? options]) { + Method emit([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)); - } - } - } + final (requiredParams, optionalParams) = + emitParameters(parameters, options); return Method((m) => m ..external = true ..name = dartName ?? name ..annotations.add(generateJSAnnotation( dartName == null || dartName == name ? null : name)) + ..static = options?.static ?? false ..types .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions()))) ..returns = returnType.emit(options?.toTypeOptions()) @@ -244,7 +256,7 @@ class FunctionDeclaration extends CallableDeclaration } } -class EnumDeclaration extends NamedDeclaration +class EnumDeclaration extends NestableDeclaration implements ExportableDeclaration { @override String name; @@ -260,6 +272,9 @@ class EnumDeclaration extends NamedDeclaration @override String? dartName; + @override + NestableDeclaration? parent; + EnumDeclaration( {required this.name, required this.baseType, @@ -275,11 +290,14 @@ class EnumDeclaration extends NamedDeclaration return ExtensionType((e) => e ..annotations.addAll([ - if (dartName != null && dartName != name && externalMember) - generateJSAnnotation(name) + if (externalMember) + if (parent != null) + generateJSAnnotation(qualifiedName) + else if (dartName != null && dartName != name) + generateJSAnnotation(name) ]) ..constant = !shouldUseJSRepType - ..name = dartName ?? name + ..name = completedDartName ..primaryConstructorName = '_' ..representationDeclaration = RepresentationDeclaration((r) => r ..declaredRepresentationType = ( @@ -293,7 +311,7 @@ class EnumDeclaration extends NamedDeclaration } @override - ID get id => ID(type: 'enum', name: name); + ID get id => ID(type: 'enum', name: qualifiedName); } class EnumMember { @@ -375,6 +393,155 @@ class TypeAliasDeclaration extends NamedDeclaration } } +/// The declaration node for a TypeScript Namespace +// TODO: Refactor into shared class when supporting modules +class NamespaceDeclaration extends NestableDeclaration + implements ExportableDeclaration, ParentDeclaration { + @override + String name; + + @override + String? dartName; + + final ID _id; + + @override + ID get id => ID(type: _id.type, name: qualifiedName, index: _id.index); + + @override + bool exported; + + @override + NamespaceDeclaration? parent; + + final Set namespaceDeclarations; + + final Set topLevelDeclarations; + + final Set nestableDeclarations; + + @override + Set nodes = {}; + + NamespaceDeclaration( + {required this.name, + this.exported = true, + required ID id, + this.dartName, + this.topLevelDeclarations = const {}, + this.namespaceDeclarations = const {}, + this.nestableDeclarations = const {}}) + : _id = id; + + @override + ExtensionType emit([covariant DeclarationOptions? options]) { + options ??= DeclarationOptions(); + options.static = true; + // static props and vars + final methods = []; + final fields = []; + + for (final decl in topLevelDeclarations) { + if (decl case final VariableDeclaration variable) { + if (variable.modifier == VariableModifier.$const) { + methods.add(variable.emit(options) as Method); + } else { + fields.add(variable.emit(options) as Field); + } + } else if (decl case final FunctionDeclaration fn) { + methods.add(fn.emit(options)); + } + } + + // namespace refs + for (final NamespaceDeclaration( + name: namespaceName, + dartName: namespaceDartName, + ) in namespaceDeclarations) { + methods.add(Method((m) => m + ..name = namespaceDartName ?? namespaceName + ..annotations + .addAll([generateJSAnnotation('$qualifiedName.$namespaceName')]) + ..type = MethodType.getter + ..returns = + refer('${completedDartName}_${namespaceDartName ?? namespaceName}') + ..external = true + ..static = true)); + } + + // class refs + for (final nestable in nestableDeclarations) { + switch (nestable) { + case ClassDeclaration( + name: final className, + dartName: final classDartName, + constructors: final constructors, + typeParameters: final typeParams, + abstract: final abstract + ): + var constr = constructors + .where((c) => c.name == null || c.name == 'unnamed') + .firstOrNull; + + if (constructors.isEmpty && !abstract) { + constr = ConstructorDeclaration.defaultFor(nestable); + } + + // static call to class constructor + if (constr != null) { + options ??= DeclarationOptions(); + + final (requiredParams, optionalParams) = + emitParameters(constr.parameters, options); + + methods.add(Method((m) => m + ..name = classDartName ?? className + ..annotations + .addAll([generateJSAnnotation('$qualifiedName.$className')]) + ..types.addAll( + typeParams.map((t) => t.emit(options?.toTypeOptions()))) + ..requiredParameters.addAll(requiredParams) + ..optionalParameters.addAll(optionalParams) + ..returns = + refer('${completedDartName}_${classDartName ?? className}') + ..lambda = true + ..static = true + ..body = refer(nestable.completedDartName).call( + [ + ...requiredParams.map((p) => refer(p.name)), + if (optionalParams.isNotEmpty) + ...optionalParams.map((p) => refer(p.name)) + ], + {}, + typeParams + .map((t) => t.emit(options?.toTypeOptions())) + .toList()).code)); + } + break; + default: + break; + } + } + + // put them together... + return ExtensionType((eType) => eType + ..name = completedDartName + ..annotations.addAll([ + if (parent != null) + generateJSAnnotation(qualifiedName) + else if (dartName != null && dartName != name) + generateJSAnnotation(name) + ]) + ..implements.add(refer('JSObject', 'dart:js_interop')) + ..primaryConstructorName = '_' + ..representationDeclaration = RepresentationDeclaration((rep) => rep + ..name = '_' + ..declaredRepresentationType = refer('JSObject', 'dart:js_interop')) + ..fields.addAll(fields) + ..methods.addAll(methods)); + } +} + /// The declaration node for a TypeScript/JavaScript Class /// /// ```ts @@ -407,7 +574,7 @@ class ClassDeclaration extends TypeDeclaration { } @override - ID get id => ID(type: 'class', name: name); + ID get id => ID(type: 'class', name: qualifiedName); } /// The declaration node for a TypeScript [Interface]() @@ -418,22 +585,25 @@ class ClassDeclaration extends TypeDeclaration { /// } /// ``` class InterfaceDeclaration extends TypeDeclaration { + final ID _id; + @override - ID id; + ID get id => ID(type: _id.type, name: qualifiedName, index: _id.index); final List extendedTypes; InterfaceDeclaration( {required super.name, required super.exported, - required this.id, + required ID id, super.dartName, super.typeParameters, this.extendedTypes = const [], super.methods, super.properties, super.operators, - super.constructors}); + super.constructors}) + : _id = id; @override ExtensionType emit([covariant DeclarationOptions? options]) { @@ -558,20 +728,8 @@ class MethodDeclaration extends CallableDeclaration 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)); - } - } - } + final (requiredParams, optionalParams) = + emitParameters(parameters, options); assert(scope == DeclScope.public, 'Only public members can be emitted'); @@ -662,22 +820,10 @@ class ConstructorDeclaration implements MemberDeclaration { Constructor emit([covariant DeclarationOptions? options]) { options ??= DeclarationOptions(); - final requiredParams = []; - final optionalParams = []; - final isFactory = dartName != null && dartName != name; + final (requiredParams, optionalParams) = + emitParameters(parameters, options); - 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)); - } - } - } + final isFactory = dartName != null && dartName != name; return Constructor((c) => c ..external = true diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index f9140b73..acd3c824 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -112,3 +112,24 @@ Type getClassRepresentationType(ClassDeclaration cl) { return BuiltinType.primitiveType(primitiveType, isNullable: false); } } + +(List, List) emitParameters( + List parameters, + [DeclarationOptions? options]) { + 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)); + } + } + } + + return (requiredParams, optionalParams); +} diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 26b909ee..8c8a7c10 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -34,7 +34,9 @@ class ReferredType extends Type { @override Reference emit([TypeOptions? options]) { return TypeReference((t) => t - ..symbol = declaration.dartName ?? declaration.name + ..symbol = (declaration is NestableDeclaration) + ? (declaration as NestableDeclaration).completedDartName + : declaration.dartName ?? declaration.name ..types.addAll(typeParams.map((t) => t.emit(options))) ..isNullable = options?.nullable ..url = options?.url ?? url); diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart index 3cce7a7e..519e1445 100644 --- a/web_generator/lib/src/interop_gen/namer.dart +++ b/web_generator/lib/src/interop_gen/namer.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import '../banned_names.dart'; +import 'qualified_name.dart'; class ID { final String type; @@ -30,6 +31,8 @@ class ID { class UniqueNamer { final Set _usedNames; + Set get used => _usedNames; + UniqueNamer([ Iterable used = const [], ]) : _usedNames = used.toSet(); @@ -75,6 +78,18 @@ class UniqueNamer { ); } + /// Adds names from scoped declarations to [_usedNames] + void markUsedSet(ScopedUniqueNamer namer) { + for (final ID(name: name, type: type) in namer._usedIDs) { + if (['namespace', 'interface', 'class'].contains(type)) { + final qualifiedName = QualifiedName.raw(name); + // generate to completed name + final indexedName = qualifiedName.join('_'); + markUsed(indexedName); + } + } + } + static ID parse(String id) { String? index; String name; @@ -91,7 +106,7 @@ class UniqueNamer { } /// Adds a [name] to used names. - void markUsed(String name) { + void markUsed(String name, [String? type]) { _usedNames.add(name); } } @@ -103,6 +118,9 @@ class ScopedUniqueNamer implements UniqueNamer { @override Set get _usedNames => _usedIDs.map((i) => i.rename).toSet(); + @override + Set get used => _usedNames; + ScopedUniqueNamer( [Set? allowedEquals, Iterable used = const []]) : _usedIDs = used.map(UniqueNamer.parse).toSet(), @@ -137,7 +155,19 @@ class ScopedUniqueNamer implements UniqueNamer { } @override - void markUsed(String name) { - _usedIDs.add(UniqueNamer.parse(name)); + void markUsed(String name, [String? type]) { + ID id; + try { + id = UniqueNamer.parse(name); + } catch (e) { + id = ID(type: type!, name: name); + } + + _usedIDs.add(id); + } + + @override + void markUsedSet(ScopedUniqueNamer namer) { + _usedIDs.addAll(namer._usedIDs); } } diff --git a/web_generator/lib/src/interop_gen/qualified_name.dart b/web_generator/lib/src/interop_gen/qualified_name.dart new file mode 100644 index 00000000..66122b66 --- /dev/null +++ b/web_generator/lib/src/interop_gen/qualified_name.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +final class QualifiedNamePart extends LinkedListEntry { + final String part; + + QualifiedNamePart(this.part); + + @override + String toString() => part; +} + +/// A wrapper around a [LinkedList] suitable for converting a TS Qualified Name +/// into a more suitable representation for lookup, length deduction, etc +extension type QualifiedName(LinkedList _) + implements LinkedList { + QualifiedName.raw(String str) : _ = LinkedList() { + _.addAll(str.split('.').map(QualifiedNamePart.new)); + } + + String get asName => _.join('.'); +} + +(QualifiedName, String?) parseTSFullyQualifiedName(String name) { + final importRegex = RegExp(r'"([^"]+)"\.'); + if (importRegex.hasMatch(name)) { + final match = importRegex.firstMatch(name)!; + final import = match.group(1); + final [_, qualifiedName] = name.split(importRegex); + return (QualifiedName.raw(qualifiedName), import); + } else { + return (QualifiedName.raw(name), null); + } +} diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart index 9a6df011..62a0bca5 100644 --- a/web_generator/lib/src/interop_gen/transform.dart +++ b/web_generator/lib/src/interop_gen/transform.dart @@ -10,12 +10,14 @@ import 'package:dart_style/dart_style.dart'; import 'package:path/path.dart' as p; import '../ast/base.dart'; +import '../ast/declarations.dart'; import '../config.dart'; import '../js/helpers.dart'; import '../js/typescript.dart' as ts; import '../js/typescript.types.dart'; import 'namer.dart'; import 'parser.dart'; +import 'qualified_name.dart'; import 'transform/transformer.dart'; void _setGlobalOptions(Config config) { @@ -40,12 +42,15 @@ class TransformResult { return programDeclarationMap.map((file, declMap) { final emitter = DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true); - final specs = declMap.decls.values.map((d) { - return switch (d) { - final Declaration n => n.emit(), - final Type _ => null, - }; - }).whereType(); + final specs = declMap.values + .map((d) { + return switch (d) { + final Declaration n => n.emit(), + final Type _ => null, + }; + }) + .nonNulls + .whereType(); final lib = Library((l) { if (config.preamble case final preamble?) { l.comments.addAll(const LineSplitter().convert(preamble).map((l) { @@ -56,8 +61,13 @@ class TransformResult { })); } l - ..ignoreForFile.addAll( - ['constant_identifier_names', 'non_constant_identifier_names']) + ..ignoreForFile.addAll([ + 'constant_identifier_names', + 'non_constant_identifier_names', + if (declMap.values + .any((d) => d is NestableDeclaration && d.parent != null)) + 'camel_case_types', + ]) ..body.addAll(specs); }); return MapEntry( @@ -75,7 +85,22 @@ extension type NodeMap._(Map decls) implements Map { List findByName(String name) { return decls.entries .where((e) { - return UniqueNamer.parse(e.key).name == name; + final n = UniqueNamer.parse(e.key).name; + if (!n.contains('.')) return n == name; + + final qualifiedName = QualifiedName.raw(n); + return qualifiedName.last.part == name; + }) + .map((e) => e.value) + .toList(); + } + + List findByQualifiedName(QualifiedName qName) { + return decls.entries + .where((e) { + final name = UniqueNamer.parse(e.key).name; + final qualifiedName = QualifiedName.raw(name); + return qualifiedName.map((n) => n.part) == qName.map((n) => n.part); }) .map((e) => e.value) .toList(); @@ -117,15 +142,13 @@ class ProgramMap { final ts.TSTypeChecker typeChecker; /// The files in the given project - final List files; - - List get absoluteFiles => - files.map((f) => p.normalize(p.absolute(f))).toList(); + final p.PathSet files; final List filterDeclSet; - ProgramMap(this.program, this.files, {this.filterDeclSet = const []}) - : typeChecker = program.getTypeChecker(); + ProgramMap(this.program, List files, {this.filterDeclSet = const []}) + : typeChecker = program.getTypeChecker(), + files = p.PathSet.of(files); /// Find the node definition for a given declaration named [declName] /// or associated with a TypeScript node [node] from the map of files @@ -150,6 +173,7 @@ class ProgramMap { final exports = symbol.exports?.toDart ?? {}; final targetSymbol = exports[d.toJS]!; + transformer.transform(targetSymbol.getDeclarations()!.toDart.first); } else { transformer.transform(node); @@ -223,7 +247,7 @@ class ProgramMap { class TransformerManager { final ProgramMap programMap; - List get inputFiles => programMap.files; + p.PathSet get inputFiles => programMap.files; ts.TSProgram get program => programMap.program; @@ -243,7 +267,7 @@ class TransformerManager { // run through each file for (final file in inputFiles) { // transform - outputNodeMap[file] = programMap.getNodeMap(file); + outputNodeMap[file!] = programMap.getNodeMap(file); } return TransformResult._(outputNodeMap); diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index 90b755f6..d209f4d8 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:collection'; import 'dart:js_interop'; import 'package:path/path.dart' as p; import '../../ast/base.dart'; @@ -9,9 +10,12 @@ import '../../ast/builtin.dart'; import '../../ast/declarations.dart'; import '../../ast/helpers.dart'; import '../../ast/types.dart'; +import '../../js/annotations.dart'; +import '../../js/helpers.dart'; import '../../js/typescript.dart' as ts; import '../../js/typescript.types.dart'; import '../namer.dart'; +import '../qualified_name.dart'; import '../transform.dart'; class ExportReference { @@ -90,40 +94,65 @@ class Transformer { void transform(TSNode node) { if (nodes.contains(node)) return; + final decls = _transform(node); + + nodeMap.addAll({for (final d in decls) d.id.toString(): d}); + + nodes.add(node); + } + + List _transform(TSNode node, + {Set? exportSet, + UniqueNamer? namer, + NamespaceDeclaration? parent}) { switch (node.kind) { case TSSyntaxKind.ImportDeclaration || TSSyntaxKind.ImportSpecifier: // We do not parse import declarations by default // so that generated code only makes use of declarations we need. - break; + return []; case TSSyntaxKind.ExportSpecifier: - _parseExportSpecifier(node as TSExportSpecifier); + _parseExportSpecifier(node as TSExportSpecifier, exportSet: exportSet); + return []; case TSSyntaxKind.ExportDeclaration: - _parseExportDeclaration(node as TSExportDeclaration); + _parseExportDeclaration(node as TSExportDeclaration, + exportSet: exportSet); + return []; case TSSyntaxKind.VariableStatement: - final decs = _transformVariable(node as TSVariableStatement); - nodeMap.addAll({for (final d in decs) d.id.toString(): d}); + return _transformVariable(node as TSVariableStatement, namer: namer); + case TSSyntaxKind.VariableDeclaration: + return [ + _transformVariableDecl(node as TSVariableDeclaration, namer: namer) + ]; + case TSSyntaxKind.FunctionDeclaration: + return [ + _transformFunction(node as TSFunctionDeclaration, namer: namer) + ]; + case TSSyntaxKind.EnumDeclaration: + return [_transformEnum(node as TSEnumDeclaration, namer: namer)]; + case TSSyntaxKind.TypeAliasDeclaration: + return [ + _transformTypeAlias(node as TSTypeAliasDeclaration, namer: namer) + ]; + case TSSyntaxKind.ClassDeclaration || TSSyntaxKind.InterfaceDeclaration: + return [ + _transformClassOrInterface(node as TSObjectDeclaration, namer: namer) + ]; + case TSSyntaxKind.ImportEqualsDeclaration + when (node as TSImportEqualsDeclaration).moduleReference.kind != + TSSyntaxKind.ExternalModuleReference: + return [_transformImportEqualsDeclarationAsTypeAlias(node)]; + case TSSyntaxKind.ModuleDeclaration + when (node as TSModuleDeclaration).name.kind == + TSSyntaxKind.Identifier && + (node.name as TSIdentifier).text != 'global': + return [_transformNamespace(node, namer: namer, parent: parent)]; default: - final decl = switch (node.kind) { - TSSyntaxKind.VariableDeclaration => - _transformVariableDecl(node as TSVariableDeclaration), - TSSyntaxKind.FunctionDeclaration => - _transformFunction(node as TSFunctionDeclaration), - TSSyntaxKind.EnumDeclaration => - _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}') - }; - nodeMap.add(decl); + throw Exception('Unsupported Declaration Kind: ${node.kind}'); } - - nodes.add(node); } - void _parseExportSpecifier(TSExportSpecifier specifier) { + void _parseExportSpecifier(TSExportSpecifier specifier, + {Set? exportSet}) { final actualName = specifier.propertyName ?? specifier.name; final dartName = specifier.name; @@ -134,13 +163,15 @@ class Transformer { // export reference if (decl.isEmpty) _getTypeFromDeclaration(actualName, []); - exportSet.removeWhere((e) => e.name == actualName.text); - exportSet.add(ExportReference(actualName.text, as: dartName.text)); + (exportSet ?? this.exportSet).removeWhere((e) => e.name == actualName.text); + (exportSet ?? this.exportSet) + .add(ExportReference(actualName.text, as: dartName.text)); } /// Parses an export declaration and converts it into an [ExportReference] /// to be handled when returning results - void _parseExportDeclaration(TSExportDeclaration export) { + void _parseExportDeclaration(TSExportDeclaration export, + {Set? exportSet}) { // TODO(nikeokoronkwo): Support namespace exports if (export.exportClause?.kind == TSSyntaxKind.NamedExports) { // named exports @@ -153,15 +184,184 @@ class Transformer { // The exported name to use final dartName = exp.name.text; - exportSet.removeWhere((e) => e.name == actualName); - exportSet.add(ExportReference(actualName, as: dartName)); + (exportSet ?? this.exportSet).removeWhere((e) => e.name == actualName); + (exportSet ?? this.exportSet) + .add(ExportReference(actualName, as: dartName)); } } } + // TODO(): Support `import = require` declarations, https://github.com/dart-lang/web/issues/438 + TypeAliasDeclaration _transformImportEqualsDeclarationAsTypeAlias( + TSImportEqualsDeclaration typealias, + {UniqueNamer? namer}) { + namer ??= this.namer; + final name = typealias.name.text; + + // get modifiers + final modifiers = typealias.modifiers?.toDart ?? []; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + // As Identifier or Qualified Name + final type = typealias.moduleReference; + + namer.markUsed(name, 'typealias'); + + return TypeAliasDeclaration( + name: name, + type: _getTypeFromDeclaration(type, null), + exported: isExported); + } + + /// Transforms a TS Namespace (identified as a [TSModuleDeclaration] with + /// an identifier name that isn't "global") into a Dart Namespace + /// Representation. + NamespaceDeclaration _transformNamespace(TSModuleDeclaration namespace, + {UniqueNamer? namer, NamespaceDeclaration? parent}) { + namer ??= this.namer; + + final namespaceName = (namespace.name as TSIdentifier).text; + + // get modifiers + final modifiers = namespace.modifiers?.toDart ?? []; + final isExported = modifiers.any((m) { + return m.kind == TSSyntaxKind.ExportKeyword; + }); + + final currentNamespaces = parent != null + ? parent.namespaceDeclarations.where((n) => n.name == namespaceName) + : nodeMap.findByName(namespaceName).whereType(); + + final (name: dartName, :id) = currentNamespaces.isEmpty + ? namer.makeUnique(namespaceName, 'namespace') + : (name: null, id: null); + + final scopedNamer = ScopedUniqueNamer(); + + final outputNamespace = currentNamespaces.isNotEmpty + ? currentNamespaces.first + : NamespaceDeclaration( + name: namespaceName, + dartName: dartName, + id: id!, + exported: isExported, + topLevelDeclarations: {}, + namespaceDeclarations: {}, + nestableDeclarations: {}); + + // TODO: We can implement this in classes and interfaces. + // however, since namespaces and modules are a thing, + // let's keep that in mind + /// Updates the state of the given declaration, + /// allowing cross-references between types and declarations in the + /// namespace, including the namespace itself + void updateNSInParent() { + if (parent != null) { + if (currentNamespaces.isNotEmpty || + parent.namespaceDeclarations.any((n) => n.name == namespaceName)) { + parent.namespaceDeclarations.remove(currentNamespaces.first); + parent.namespaceDeclarations.add(outputNamespace); + } else { + outputNamespace.parent = parent; + parent.namespaceDeclarations.add(outputNamespace); + } + } else { + nodeMap.update(outputNamespace.id.toString(), (v) => outputNamespace, + ifAbsent: () => outputNamespace); + } + } + + // preload nodemap + updateNSInParent(); + + // to reduce probing, we can use exported decls instead + final symbol = typeChecker.getSymbolAtLocation(namespace.name); + final exports = symbol?.exports?.toDart; + + if (exports case final exportedMap?) { + for (final symbol in exportedMap.values) { + final decls = symbol.getDeclarations()?.toDart ?? []; + try { + final aliasedSymbol = typeChecker.getAliasedSymbol(symbol); + decls.addAll(aliasedSymbol.getDeclarations()?.toDart ?? []); + } catch (_) { + // throws error if no aliased symbol, so ignore + } + for (final decl in decls) { + // TODO: We could also ignore namespace decls with the same name, as + // a single instance should consider such non-necessary + if (outputNamespace.nodes.contains(decl)) continue; + final outputDecls = + _transform(decl, namer: scopedNamer, parent: outputNamespace); + switch (decl.kind) { + case TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration: + final outputDecl = outputDecls.first as TypeDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.EnumDeclaration: + final outputDecl = outputDecls.first as EnumDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + default: + outputNamespace.topLevelDeclarations.addAll(outputDecls); + } + outputNamespace.nodes.add(decl); + + // update namespace state + updateNSInParent(); + } + } + // fallback + } else { + if (namespace.body case final namespaceBody? + when namespaceBody.kind == TSSyntaxKind.ModuleBlock) { + for (final statement + in (namespaceBody as TSModuleBlock).statements.toDart) { + final outputDecls = _transform(statement, + namer: scopedNamer, parent: outputNamespace); + switch (statement.kind) { + case TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration: + final outputDecl = outputDecls.first as TypeDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.EnumDeclaration: + final outputDecl = outputDecls.first as EnumDeclaration; + outputDecl.parent = outputNamespace; + outputNamespace.nestableDeclarations.add(outputDecl); + default: + outputNamespace.topLevelDeclarations.addAll(outputDecls); + } + + // update namespace state + updateNSInParent(); + } + } else if (namespace.body case final namespaceBody?) { + // namespace import + _transformNamespace(namespaceBody as TSNamespaceDeclaration, + namer: scopedNamer, parent: outputNamespace); + } + } + + // final update on namespace state + updateNSInParent(); + + // index names + namer.markUsedSet(scopedNamer); + + // get the exported symbols from the namespace + return outputNamespace; + } + /// Transforms a TS Class or Interface declaration into a node representing /// a class or interface respectively. - TypeDeclaration _transformClassOrInterface(TSObjectDeclaration typeDecl) { + TypeDeclaration _transformClassOrInterface(TSObjectDeclaration typeDecl, + {UniqueNamer? namer}) { + namer ??= this.namer; + final name = typeDecl.name.text; final modifiers = typeDecl.modifiers?.toDart; @@ -307,7 +507,8 @@ class Transformer { if (property.type case final type? when ts.isTypeReferenceNode(type)) { // check if final referredType = type as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { propType = parent.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -350,7 +551,8 @@ class Transformer { if (method.type case final type? when ts.isTypeReferenceNode(type)) { // check if final referredType = type as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { methodType = parent.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -370,7 +572,8 @@ class Transformer { final paramRawType = t.type; if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { final referredType = ty as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { paramType = parent.asReferredType(ty.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -431,7 +634,8 @@ class Transformer { if (callSignature.type case final type? when ts.isTypeReferenceNode(type)) { // check if final referredType = type as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { methodType = parent.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -471,7 +675,8 @@ class Transformer { if (indexSignature.type case final type when ts.isTypeReferenceNode(type)) { // check if final referredType = type as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { indexerType = parent.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -521,7 +726,8 @@ class Transformer { if (getter.type case final type? when ts.isTypeReferenceNode(type)) { // check if final referredType = type as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { methodType = parent.asReferredType(type.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -569,7 +775,8 @@ class Transformer { final paramRawType = t.type; if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) { final referredType = ty as TSTypeReferenceNode; - if (referredType.typeName.text == parent.name) { + final referredTypeName = parseQualifiedName(referredType.typeName); + if (referredTypeName.asName == parent.name) { paramType = parent.asReferredType(ty.typeArguments?.toDart .map((t) => _transformType(t, typeArg: true)) .toList()); @@ -589,10 +796,12 @@ class Transformer { return methodDeclaration; } - FunctionDeclaration _transformFunction(TSFunctionDeclaration function) { + FunctionDeclaration _transformFunction(TSFunctionDeclaration function, + {UniqueNamer? namer}) { + namer ??= this.namer; final name = function.name.text; - final modifiers = function.modifiers.toDart; + final modifiers = function.modifiers?.toDart ?? []; final isExported = modifiers.any((m) { return m.kind == TSSyntaxKind.ExportKeyword; }); @@ -616,9 +825,10 @@ class Transformer { : BuiltinType.anyType); } - List _transformVariable(TSVariableStatement variable) { + List _transformVariable(TSVariableStatement variable, + {UniqueNamer? namer}) { // get the modifier of the declaration - final modifiers = variable.modifiers.toDart; + final modifiers = variable.modifiers?.toDart ?? []; final isExported = modifiers.any((m) { return m.kind == TSSyntaxKind.ExportKeyword; }); @@ -632,14 +842,16 @@ class Transformer { } return variable.declarationList.declarations.toDart.map((d) { - return _transformVariableDecl(d, modifier, isExported); + return _transformVariableDecl(d, + modifier: modifier, isExported: isExported, namer: namer); }).toList(); } VariableDeclaration _transformVariableDecl(TSVariableDeclaration d, - [VariableModifier? modifier, bool? isExported]) { + {VariableModifier? modifier, bool? isExported, UniqueNamer? namer}) { + namer ??= this.namer; final statement = d.parent.parent; - isExported ??= statement.modifiers.toDart.any((m) { + isExported ??= statement.modifiers?.toDart.any((m) { return m.kind == TSSyntaxKind.ExportKeyword; }); modifier ??= switch (statement.declarationList.flags) { @@ -649,15 +861,17 @@ class Transformer { _ => VariableModifier.$var }; - namer.markUsed(d.name.text); + namer.markUsed(d.name.text, 'var'); return VariableDeclaration( name: d.name.text, type: d.type == null ? BuiltinType.anyType : _transformType(d.type!), modifier: modifier, - exported: isExported); + exported: isExported ?? false); } - EnumDeclaration _transformEnum(TSEnumDeclaration enumeration) { + EnumDeclaration _transformEnum(TSEnumDeclaration enumeration, + {UniqueNamer? namer}) { + namer ??= this.namer; final modifiers = enumeration.modifiers?.toDart; final isExported = modifiers?.any((m) { return m.kind == TSSyntaxKind.ExportKeyword; @@ -726,6 +940,8 @@ class Transformer { } } + namer.markUsed(name, 'enum'); + return EnumDeclaration( name: name, baseType: BuiltinType.primitiveType(enumRepType ?? PrimitiveType.num), @@ -741,7 +957,9 @@ class Transformer { return stringLiteral.text; } - TypeAliasDeclaration _transformTypeAlias(TSTypeAliasDeclaration typealias) { + TypeAliasDeclaration _transformTypeAlias(TSTypeAliasDeclaration typealias, + {UniqueNamer? namer}) { + namer ??= this.namer; final name = typealias.name.text; final modifiers = typealias.modifiers?.toDart; @@ -754,6 +972,8 @@ class Transformer { final type = typealias.type; + namer.markUsed(name, 'typealias'); + return TypeAliasDeclaration( name: name, type: _transformType(type), @@ -817,7 +1037,7 @@ class Transformer { case TSSyntaxKind.TypeReference: final refType = type as TSTypeReferenceNode; - return _getTypeFromTypeRefNode(refType, typeArg: typeArg); + return _getTypeFromTypeNode(refType, typeArg: typeArg); case TSSyntaxKind.UnionType: final unionType = type as TSUnionTypeNode; // TODO: Unions @@ -958,256 +1178,301 @@ class Transformer { } } - /// Get the type of a type node [node] by gettings its type from - /// the node itself via the [ts.TSTypeChecker] + /// Given a [symbol] with declarations defined in the given file, this method + /// searches for the declaration recursively (either as a top level on the + /// [ts.TSSourceFile], or recursively inside a module or namespace). /// - /// This has similar options as [_getTypeFromDeclaration] - Type _getTypeFromTypeRefNode(TSTypeReferenceNode node, - {List? typeArguments, - bool typeArg = false, - bool isNotTypableDeclaration = false}) { - typeArguments ??= node.typeArguments?.toDart; - final name = node.typeName.text; + /// The method uses an iterable of [QualifiedNamePart], usually gotten from a + /// [QualifiedName] parsed from the identifier/qualified name used to associate + /// the given reference to recursively step through each part of the [name] + /// until all parts have been handled/parsed, and a single declaration is gotten + /// for the given [symbol]. If a declaration is not found at a given point, + /// the associated declaration with that part (usually as a parent of the + /// declaration gotten from the [symbol]) is transformed, and either added + /// to [nodeMap], or, if in recursion, added to its [parent] declaration. + /// + /// The referred type may accept [typeArguments], which are passed as well. + Type _searchForDeclRecursive( + Iterable name, + TSSymbol symbol, { + NamespaceDeclaration? parent, + List? typeArguments, + bool isNotTypableDeclaration = false, + bool typeArg = false, + }) { + // get name and map + final firstName = name.first.part; + + var map = parent != null + ? NodeMap([ + ...parent.nestableDeclarations, + ...parent.namespaceDeclarations + ].asMap().map((_, v) => MapEntry(v.id.toString(), v))) + : nodeMap; + + // search map + var declarationsMatching = map.findByName(firstName); - 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; + // if not referred type, then check here + // transform + final declarations = symbol.getDeclarations()?.toDart ?? []; + var firstDecl = declarations.first as TSNamedDeclaration; + + if (firstDecl.kind == TSSyntaxKind.ExportSpecifier) { + // in order to prevent recursion, we need to find the source of the + // export specifier + final aliasedSymbol = typeChecker.getAliasedSymbol(symbol); + final aliasedSymbolName = aliasedSymbol.name; + + exportSet.removeWhere((e) => e.name == aliasedSymbolName); + exportSet.add(ExportReference(aliasedSymbolName, as: firstName)); + return _getTypeFromSymbol( + aliasedSymbol, + typeChecker.getTypeOfSymbol(aliasedSymbol), + typeArguments, + typeArg, + isNotTypableDeclaration); } - var symbol = typeChecker.getSymbolAtLocation(node.typeName); - if (symbol == null) { - final type = typeChecker.getTypeFromTypeNode(node); - symbol = type?.aliasSymbol ?? type?.symbol; + while (firstDecl.name?.text != firstName && + firstDecl.parent.kind == TSSyntaxKind.ModuleBlock) { + firstDecl = (firstDecl.parent as TSModuleBlock).parent; } - - final (derivedType, newName) = _deriveTypeOrTransform( - symbol!, name, typeArguments, typeArg, isNotTypableDeclaration); - - if (derivedType != null) return derivedType; - - declarationsMatching = - nodeMap.findByName(newName != name ? newName : 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(), - ); - } - - /// 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 isNotTypableDeclaration = 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) - if (!isNotTypableDeclaration) { - final supportedType = BuiltinType.referred(name, - typeParams: (typeArguments ?? []) - .map((t) => getJSTypeAlternative(_transformType(t))) - .toList()); - if (supportedType case final resultType?) { - return resultType; + final namer = parent != null + ? ScopedUniqueNamer( + {}, + [ + ...parent.namespaceDeclarations, + ...parent.nestableDeclarations, + ...parent.topLevelDeclarations + ].map((d) => d.id.toString())) + : null; + // TODO: multi-decls + final transformedDecls = + _transform(firstDecl, namer: namer, parent: parent); + + if (parent != null) { + switch (firstDecl.kind) { + case TSSyntaxKind.ClassDeclaration || + TSSyntaxKind.InterfaceDeclaration: + final outputDecl = transformedDecls.first as TypeDeclaration; + outputDecl.parent = parent; + parent.nestableDeclarations.add(outputDecl); + case TSSyntaxKind.EnumDeclaration: + final outputDecl = transformedDecls.first as EnumDeclaration; + outputDecl.parent = parent; + parent.nestableDeclarations.add(outputDecl); + default: + parent.topLevelDeclarations.addAll(transformedDecls); } + parent.nodes.add(firstDecl); + } else { + nodeMap.addAll( + {for (final decl in transformedDecls) decl.id.toString(): decl}); + nodes.add(firstDecl); } - final symbol = typeChecker.getSymbolAtLocation(typeName); - if (symbol case final s?) { - final (derivedType, newName) = _deriveTypeOrTransform( - s, name, typeArguments, typeArg, isNotTypableDeclaration); + map = parent != null + ? NodeMap([ + ...parent.nestableDeclarations, + ...parent.namespaceDeclarations + ].asMap().map((_, v) => MapEntry(v.id.toString(), v))) + : nodeMap; - if (derivedType != null) return derivedType; - - declarationsMatching = - nodeMap.findByName(newName != name ? newName : name); - } else { - declarationsMatching = nodeMap.findByName(name); - } + declarationsMatching = map.findByName(firstName); } - // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? - final firstNode = declarationsMatching.whereType().first; + // get node finally + final decl = declarationsMatching.whereType().first; - 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) { + // are we done? + final rest = name.skip(1); + if (rest.isEmpty) { + // return decl + switch (decl) { case TypeAliasDeclaration(type: final t): case EnumDeclaration(baseType: final t): final jsType = getJSTypeAlternative(t); if (jsType != t && typeArg) return jsType; } - } - final asReferredType = firstNode.asReferredType( - (typeArguments ?? []) - .map((type) => _transformType(type, typeArg: true)) - .toList(), - ); + final asReferredType = decl.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; + } - if (asReferredType case ReferredDeclarationType(type: final type) - when type is BuiltinType) { - final jsType = getJSTypeAlternative(type); - if (jsType != type && typeArg) asReferredType.type = jsType; + return asReferredType; + } else { + // we go one more time + + // TODO: Typealias resolving? + switch (decl) { + // if decl is class/interface, check if we're referring to generic + case TypeDeclaration(typeParameters: final typeParams): + if (rest.singleOrNull?.part case final generic? + when typeParams.any((t) => t.name == generic)) { + final typeParam = typeParams.firstWhere((t) => t.name == generic); + return GenericType(name: typeParam.name, parent: decl); + } + break; + case final NamespaceDeclaration n: + final searchForDeclRecursive = _searchForDeclRecursive(rest, symbol, + typeArguments: typeArguments, typeArg: typeArg, parent: n); + if (parent == null) { + nodeMap.update(decl.id.toString(), (v) => n); + } + return searchForDeclRecursive; + // recursive + } } - return asReferredType; + throw Exception('Could not find type for given declaration'); } - /// Either derives the type referenced by the [symbol], or - /// transforms the declaration(s) associated with the [symbol]. + /// Get the type of a type node [node] by gettings its type from + /// the node itself via the [ts.TSTypeChecker] /// - /// Returns a record containing the type, if any, and the name of - /// the symbol, which, if not renamed by an export declaration, will - /// be the same as [name]. - (Type?, String) _deriveTypeOrTransform(TSSymbol symbol, - [String? name, - List? typeArguments, + /// This has similar options as [_getTypeFromDeclaration] + Type _getTypeFromTypeNode(TSTypeReferenceNode node, + {List? typeArguments, bool typeArg = false, - bool isNotTypableDeclaration = false]) { - name ??= symbol.name; - - final declarations = symbol.getDeclarations(); - if (declarations == null) { - throw Exception('Found no declaration matching $name'); + bool isNotTypableDeclaration = false}) { + typeArguments ??= node.typeArguments?.toDart; + final typeName = node.typeName; + + // get symbol + final type = typeChecker.getTypeFromTypeNode(node); + // from type: if symbol is null, or references an import + var symbol = typeChecker.getSymbolAtLocation(typeName); + if (symbol == null) { + symbol = type?.aliasSymbol ?? type?.symbol; + } else if (symbol.getDeclarations()?.toDart ?? [] case [final d] + when d.kind == TSSyntaxKind.ImportSpecifier || + d.kind == TSSyntaxKind.ImportEqualsDeclaration) { + // prefer using type node ref for such cases + // reduces import declaration handling + symbol = type?.aliasSymbol ?? type?.symbol; } - final declaration = declarations.toDart.first; - - if (declaration.kind == TSSyntaxKind.TypeParameter) { - return (GenericType(name: name), name); - } else if (declaration.kind == TSSyntaxKind.ImportSpecifier) { - // unravel import statement to get source file - final importSpecifier = declaration as TSImportSpecifer; - final importDecl = importSpecifier.parent.parent.parent; - var importUrl = importDecl.moduleSpecifier.text; - if (!importUrl.endsWith('ts')) importUrl = '$importUrl.d.ts'; + return _getTypeFromSymbol( + symbol, type, typeArguments, isNotTypableDeclaration, typeArg); + } - final importedFile = - p.normalize(p.absolute(p.join(p.dirname(file), importUrl))); + /// Given a [TSSymbol] for a given TS node or declaration, and its associated + /// [TSType], this method gets the necessary AST [Type] defined by the given + /// [symbol]. + /// + /// It uses the symbol's associated declarations, and its qualified name to + /// deduce the associated type being referred to by the symbol. + /// If the qualified name has no import file associated with it, it is either + /// a built-in type or an imported type. If it has an import file associated + /// with it and the file is this file, then the declaration is searched for + /// and transformed recursively via [_searchForDeclRecursive], else the + /// associated file is used to find and transform the associated declaration + /// through the [programMap]. + /// + /// The referred type may accept [typeArguments] which are passed to it once + /// the referred declaration is deduced. + Type _getTypeFromSymbol( + TSSymbol? symbol, + TSType? type, + List? typeArguments, + bool isNotTypableDeclaration, + bool typeArg) { + final declarations = symbol!.getDeclarations()?.toDart ?? []; - // get declaration in source file - final decl = programMap.getDeclarationRef( - importedFile, declaration, importSpecifier.name.text); + // get decl qualified name + final tsFullyQualifiedName = typeChecker.getFullyQualifiedName(symbol); - if (decl == null) { - throw Exception( - 'Imported File not included in compilation: $importedFile'); - } + // parse qualified name and import + final (fullyQualifiedName, nameImport) = + parseTSFullyQualifiedName(tsFullyQualifiedName); - final firstNode = - decl.firstWhere((d) => d is NamedDeclaration) as NamedDeclaration; + if (nameImport == null) { + // if import not there, most likely from an import - if (!isNotTypableDeclaration && typeArg) { - switch (firstNode) { - case TypeAliasDeclaration(type: final t): - case EnumDeclaration(baseType: final t): - final jsType = getJSTypeAlternative(t); - if (jsType != t) return (jsType, name); - } + if (type?.isTypeParameter() ?? false) { + // generic type + return GenericType(name: fullyQualifiedName.last.part); } - final asReferredType = firstNode.asReferredType( - (typeArguments ?? []) - .map((type) => _transformType(type, typeArg: true)) - .toList(), - programMap.absoluteFiles.contains(importedFile) - ? p.normalize(importUrl.replaceFirst('.d.ts', '.dart')) - : null); + // meaning others are imported + final firstName = fullyQualifiedName.last.part; - if (asReferredType case ReferredDeclarationType(type: final type) - when type is BuiltinType && typeArg) { - final jsType = getJSTypeAlternative(type); - if (jsType != type) asReferredType.type = jsType; + // check if primitive source + final supportedType = BuiltinType.referred(firstName, + typeParams: (typeArguments ?? []) + .map((t) => getJSTypeAlternative(_transformType(t))) + .toList()); + if (supportedType case final resultType?) { + return resultType; } - return (asReferredType, name); - } else if (declaration.kind == TSSyntaxKind.ExportSpecifier) { - // in order to prevent recursion, we need to find the source of the export - // specifier - final aliasedSymbol = typeChecker.getAliasedSymbol(symbol); - final aliasedSymbolName = aliasedSymbol.name; - - exportSet.removeWhere((e) => e.name == aliasedSymbolName); - exportSet.add(ExportReference(aliasedSymbolName, as: name)); - return _deriveTypeOrTransform(aliasedSymbol, aliasedSymbolName, - typeArguments, typeArg, isNotTypableDeclaration); - } + // now check if web type + // TODO: Use map on multiple declarations + final decl = declarations.first; + var declSource = decl.getSourceFile().fileName; - // check if this is from dom - final declarationSource = declaration.getSourceFile().fileName; - - if ((p.basename(declarationSource) == 'lib.dom.d.ts' || - declarationSource.contains('dom')) && - !isNotTypableDeclaration) { - // 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, + if ((p.basename(declSource) == 'lib.dom.d.ts' || + declSource.contains('dom')) && + !isNotTypableDeclaration) { + // dom declaration: supported by package:web + return PackageWebType.parse(firstName, typeParams: (typeArguments ?? []) .map(_transformType) .map(getJSTypeAlternative) - .toList()), - name - ); - } else if (declarationSource != file && - programMap.absoluteFiles.contains(declarationSource)) { - // get declaration from file source - // file path is absolute - // TODO: Handle star imports (`import * as Utils from "./utils"`) - final relativePath = p.relative(declarationSource, from: p.dirname(file)); - final referencedDeclarations = - programMap.getDeclarationRef(declarationSource, declaration, name); - - // TODO: In the case of overloading, should/shouldn't we handle more than one declaration? - final firstNode = - referencedDeclarations?.whereType().first; + .toList()); + } + + // TODO(nikeokoronkwo): Update the version of typescript we are using + // to 5.9 + var mustImport = false; + if (decl.kind == TSSyntaxKind.ImportSpecifier) { + // resolve import + final namedImport = decl as TSImportSpecifer; + final importDecl = namedImport.parent.parent.parent; + var importUrl = importDecl.moduleSpecifier.text; + if (!importUrl.endsWith('ts')) importUrl = '$importUrl.d.ts'; + + declSource = + p.normalize(p.absolute(p.join(p.dirname(file), importUrl))); + mustImport = true; + } + + // check if in sources for program + final NamedDeclaration? firstNode; + String? relativePath; + + if ((programMap.files.contains(declSource) && + !p.equals(declSource, file)) || + mustImport) { + if (programMap.files.contains(declSource) && + !p.equals(declSource, file)) { + relativePath = p.relative(declSource, from: p.dirname(file)); + } + final referencedDeclarations = programMap.getDeclarationRef( + declSource, decl, fullyQualifiedName.asName); + + firstNode = referencedDeclarations?.whereType().first; + } else { + var declarationsMatching = nodeMap.findByName(firstName); + if (declarationsMatching.isEmpty) { + transform(decl); + declarationsMatching = nodeMap.findByName(firstName); + } + + firstNode = + declarationsMatching.whereType().firstOrNull; + } + if (!isNotTypableDeclaration && typeArg) { // For Typealiases, we can either return the type itself // or the JS Alternative (if its underlying type isn't a JS type) @@ -1215,7 +1480,7 @@ class Transformer { case TypeAliasDeclaration(type: final t): case EnumDeclaration(baseType: final t): final jsType = getJSTypeAlternative(t); - if (jsType != t) return (jsType, name); + if (jsType != t) return jsType; } } @@ -1224,7 +1489,7 @@ class Transformer { (typeArguments ?? []) .map((type) => _transformType(type, typeArg: true)) .toList(), - relativePath.replaceFirst('.d.ts', '.dart')); + relativePath?.replaceFirst('.d.ts', '.dart')); if (outputType case ReferredDeclarationType(type: final type) when type is BuiltinType && typeArg) { @@ -1232,12 +1497,108 @@ class Transformer { if (jsType != type) outputType.type = jsType; } - return (outputType, name); + return outputType; + } + } else { + final filePathWithoutExtension = file.replaceFirst('.d.ts', ''); + if (p.equals(nameImport, filePathWithoutExtension)) { + // declared in this file + // if import there and this file, handle this file + + // generics are tricky when they are for the same decl, so + // let's just handle them before-hand + if (type?.isTypeParameter() ?? false) { + // generic type + return GenericType(name: fullyQualifiedName.last.part); + } + + // recursiveness + return _searchForDeclRecursive(fullyQualifiedName, symbol, + typeArguments: typeArguments, + typeArg: typeArg, + isNotTypableDeclaration: isNotTypableDeclaration); + } else { + // if import there and not this file, imported from specified file + final importUrl = + !nameImport.endsWith('.d.ts') ? '$nameImport.d.ts' : nameImport; + final relativePath = programMap.files.contains(importUrl) + ? p.relative(importUrl, from: p.dirname(file)) + : null; + final referencedDeclarations = declarations.map((decl) { + return programMap.getDeclarationRef( + importUrl, decl, fullyQualifiedName.asName); + }).reduce((prev, next) => + [if (prev != null) ...prev, if (next != null) ...next]); + + final firstNode = + referencedDeclarations?.whereType().first; + + if (!isNotTypableDeclaration && typeArg) { + // 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) return jsType; + } + } + + if (firstNode case final node?) { + final outputType = node.asReferredType( + (typeArguments ?? []) + .map((type) => _transformType(type, typeArg: true)) + .toList(), + relativePath?.replaceFirst('.d.ts', '.dart')); + + if (outputType case ReferredDeclarationType(type: final type) + when type is BuiltinType && typeArg) { + final jsType = getJSTypeAlternative(type); + if (jsType != type) outputType.type = jsType; + } + + return outputType; + } } } + throw Exception('Could not resolve type for node'); + } - transform(declaration); - return (null, name); + /// Get the type of a type node named [typeName] by referencing its + /// declaration. + /// + /// **NOTE**: If you have a [TSTypeReferenceNode], it is preferred to use + /// [_getTypeFromTypeNode] as it is more performant at getting the correct + /// type declarations from a given node. + /// + /// 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( + @UnionOf([TSIdentifier, TSQualifiedName]) TSNode typeName, + List? typeArguments, + {bool typeArg = false, + bool isNotTypableDeclaration = false}) { + // union assertion + assert(typeName.kind == TSSyntaxKind.Identifier || + typeName.kind == TSSyntaxKind.QualifiedName); + + final symbol = typeChecker.getSymbolAtLocation(typeName); + + return _getTypeFromSymbol(symbol, typeChecker.getTypeOfSymbol(symbol!), + typeArguments, isNotTypableDeclaration, typeArg); } /// Filters out the declarations generated from the [transform] function and @@ -1326,88 +1687,107 @@ class Transformer { }); } - final filteredDeclarations = NodeMap(); - - switch (decl) { - case final VariableDeclaration v: - if (v.type is! BuiltinType) filteredDeclarations.add(v.type); - break; - 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): + void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) { + switch (decl) { + case final VariableDeclaration v: + if (v.type is! BuiltinType) filteredDeclarations.add(v.type); + break; + 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 impl in extendedTypes.where((i) => i is! BuiltinType)) - impl.id.toString(): impl, + for (final param in con.parameters.map((p) => p.type)) + param.id.toString(): param }); - break; - } - // TODO: We can make (DeclarationAssociatedType) and use that - // rather than individual type names - case final HomogenousEnumType hu: - filteredDeclarations.add(hu.declaration); - break; - case final UnionType u: - filteredDeclarations.addAll({ - for (final t in u.types.where((t) => t is! BuiltinType)) - t.id.toString(): t - }); - break; - 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: - if (r.url == null) filteredDeclarations.add(r.declaration); - break; - case BuiltinType() || GenericType(): - break; - default: - print('WARN: The given node type ${decl.runtimeType.toString()} ' - 'is not supported for filtering. Skipping...'); - break; + } + 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; + } + case NamespaceDeclaration( + topLevelDeclarations: final topLevelDecls, + nestableDeclarations: final typeDecls, + namespaceDeclarations: final namespaceDecls, + ): + for (final tlDecl in [...typeDecls, ...namespaceDecls]) { + filteredDeclarations.add(tlDecl); + updateFilteredDeclsForDecl(tlDecl, filteredDeclarations); + } + for (final topLevelDecl in topLevelDecls) { + updateFilteredDeclsForDecl(topLevelDecl, filteredDeclarations); + } + break; + // TODO: We can make (DeclarationAssociatedType) and use that + // rather than individual type names + case final HomogenousEnumType hu: + filteredDeclarations.add(hu.declaration); + break; + case final UnionType u: + filteredDeclarations.addAll({ + for (final t in u.types.where((t) => t is! BuiltinType)) + t.id.toString(): t + }); + break; + 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: + if (r.url == null) filteredDeclarations.add(r.declaration); + break; + case BuiltinType() || GenericType(): + break; + default: + print('WARN: The given node type ${decl.runtimeType.toString()} ' + 'is not supported for filtering. Skipping...'); + break; + } } + final filteredDeclarations = NodeMap(); + + updateFilteredDeclsForDecl(decl, filteredDeclarations); + filteredDeclarations .removeWhere((k, v) => context?.containsKey(k) ?? false); @@ -1454,3 +1834,31 @@ class Transformer { return (isStatic: isStatic, isReadonly: isReadonly, scope: scope); } + +Iterable _parseQualifiedName(TSQualifiedName name) { + final list = []; + if (name.left.kind == TSSyntaxKind.Identifier) { + list.add(QualifiedNamePart((name.left as TSIdentifier).text)); + } else { + list.addAll(_parseQualifiedName(name.left as TSQualifiedName)); + } + + list.add(QualifiedNamePart(name.right.text)); + + return list; +} + +QualifiedName parseQualifiedNameFromTSQualifiedName(TSQualifiedName name) { + final list = LinkedList(); + list.addAll(_parseQualifiedName(name)); + return QualifiedName(list); +} + +QualifiedName parseQualifiedName( + @UnionOf([TSQualifiedName, TSIdentifier]) TSNode name) { + if (name.kind == TSSyntaxKind.Identifier) { + return QualifiedName.raw((name as TSIdentifier).text); + } else { + return parseQualifiedNameFromTSQualifiedName(name as TSQualifiedName); + } +} diff --git a/web_generator/lib/src/js/annotations.dart b/web_generator/lib/src/js/annotations.dart new file mode 100644 index 00000000..6a1f9f97 --- /dev/null +++ b/web_generator/lib/src/js/annotations.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An annotation representing that a given declaration is a union +/// of a given set of types +final class UnionOf { + final List types; + + const UnionOf(this.types); +} diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart index 40678b83..fd403b91 100644 --- a/web_generator/lib/src/js/typescript.dart +++ b/web_generator/lib/src/js/typescript.dart @@ -103,6 +103,8 @@ extension type TSTypeChecker._(JSObject _) implements JSObject { external TSSymbol? getSymbolAtLocation(TSNode node); external TSSymbol getAliasedSymbol(TSSymbol symbol); external TSType? getTypeFromTypeNode(TSTypeNode type); + external String getFullyQualifiedName(TSSymbol symbol); + external TSType getTypeOfSymbol(TSSymbol symbol); } // TODO: Can we make use of `FileReference`s diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index 8f86e899..d43e0a52 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -11,6 +11,7 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; +import 'annotations.dart'; import 'helpers.dart'; import 'typescript.dart'; @@ -26,11 +27,13 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262); static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278); static const TSSyntaxKind TypeAliasDeclaration = TSSyntaxKind._(265); + static const TSSyntaxKind ModuleDeclaration = TSSyntaxKind._(267); 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 ImportDeclaration = TSSyntaxKind._(272); + static const TSSyntaxKind ImportEqualsDeclaration = TSSyntaxKind._(271); static const TSSyntaxKind ImportSpecifier = TSSyntaxKind._(276); static const TSSyntaxKind Constructor = TSSyntaxKind._(176); @@ -90,6 +93,7 @@ extension type const TSSyntaxKind._(num _) { /// Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); + static const TSSyntaxKind QualifiedName = TSSyntaxKind._(166); static const TSSyntaxKind PropertyAccessExpression = TSSyntaxKind._(211); static const TSSyntaxKind ObjectBindingPattern = TSSyntaxKind._(206); static const TSSyntaxKind ArrayBindingPattern = TSSyntaxKind._(207); @@ -98,7 +102,11 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind ExpressionWithTypeArguments = TSSyntaxKind._(233); static const TSSyntaxKind NamespaceExport = TSSyntaxKind._(280); static const TSSyntaxKind NamedExports = TSSyntaxKind._(279); + static const TSSyntaxKind NamedImports = TSSyntaxKind._(275); static const TSSyntaxKind ExportSpecifier = TSSyntaxKind._(281); + static const TSSyntaxKind ModuleBlock = TSSyntaxKind._(268); + static const TSSyntaxKind ExternalModuleReference = TSSyntaxKind._(283); + static const TSSyntaxKind SourceFile = TSSyntaxKind._(308); } extension type const TSNodeFlags._(int _) implements int { @@ -156,7 +164,8 @@ extension type TSTypeReferenceNode._(JSObject _) implements TSTypeNode { @redeclare TSSyntaxKind get kind => TSSyntaxKind.TypeReference; - external TSIdentifier get typeName; + @UnionOf([TSIdentifier, TSQualifiedName]) + external TSNode get typeName; external TSNodeArray? get typeArguments; } @@ -206,19 +215,24 @@ extension type TSNumericLiteral._(JSObject _) implements TSLiteral { } @JS('StringLiteral') -extension type TSStringLiteral._(JSObject _) implements TSLiteral { - @redeclare - TSSyntaxKind get kind => TSSyntaxKind.StringLiteral; -} +extension type TSStringLiteral._(JSObject _) implements TSLiteral {} @JS('Statement') extension type TSStatement._(JSObject _) implements TSNode {} @JS('Identifier') -extension type TSIdentifier._(JSObject _) implements TSDeclaration { +extension type TSIdentifier._(JSObject _) + implements TSExpression, TSDeclaration { external String get text; } +@JS('QualifiedName') +extension type TSQualifiedName._(JSObject _) implements TSNode { + @UnionOf([TSIdentifier, TSQualifiedName]) + external TSNode get left; + external TSIdentifier get right; +} + @JS('NamedDeclaration') extension type TSNamedDeclaration._(JSObject _) implements TSNode { // TODO: Support other name specifiers @@ -310,10 +324,30 @@ extension type TSExportAssignment._(JSObject _) external TSExpression get expression; } +@JS('ImportEqualsDeclaration') +extension type TSImportEqualsDeclaration._(JSObject _) + implements TSDeclarationStatement { + @UnionOf([TSSourceFile, TSModuleDeclaration]) + @redeclare + external TSDeclaration get parent; + + external TSNodeArray? get modifiers; + external TSIdentifier get name; + external bool get isTypeOnly; + + @UnionOf([TSIdentifier, TSQualifiedName, TSExternalModuleReference]) + external TSNode get moduleReference; +} + +@JS('ExternalModuleReference') +extension type TSExternalModuleReference._(JSObject _) implements TSNode { + external TSExpression get expression; +} + @JS('VariableStatement') extension type TSVariableStatement._(JSObject _) implements TSStatement { external TSVariableDeclarationList get declarationList; - external TSNodeArray get modifiers; + external TSNodeArray? get modifiers; } @JS('VariableDeclarationList') @@ -351,7 +385,7 @@ extension type TSFunctionLikeDeclarationBase._(JSObject _) extension type TSFunctionDeclaration._(JSObject _) implements TSFunctionLikeDeclarationBase { external TSIdentifier get name; - external TSNodeArray get modifiers; + external TSNodeArray? get modifiers; } /// A common API for Classes and Interfaces @@ -534,6 +568,34 @@ extension type TSEnumMember._(JSObject _) implements TSDeclaration { external TSExpression? get initializer; } +@JS('ModuleDeclaration') +extension type TSModuleDeclaration._(JSObject _) + implements TSDeclarationStatement { + @UnionOf([TSSourceFile, TSModuleDeclaration]) + @redeclare + external TSDeclaration get parent; + + @UnionOf([TSIdentifier, TSStringLiteral]) + external TSExpression get name; + external TSNodeArray? get modifiers; + @UnionOf([TSModuleBlock, TSNamespaceDeclaration]) + external TSStatement? get body; +} + +@JS('NamespaceDeclaration') +extension type TSNamespaceDeclaration._(JSObject _) + implements TSModuleDeclaration { + @redeclare + external TSIdentifier get name; +} + +@JS('ModuleBlock') +extension type TSModuleBlock._(JSObject _) implements TSNode, TSStatement { + @redeclare + external TSModuleDeclaration get parent; + external TSNodeArray get statements; +} + @JS('NodeArray') extension type TSNodeArray._(JSArray _) implements JSArray {} @@ -551,4 +613,5 @@ typedef TSSymbolTable = JSMap; extension type TSType._(JSObject _) implements JSObject { external TSSymbol get symbol; external TSSymbol? get aliasSymbol; + external bool isTypeParameter(); } diff --git a/web_generator/test/integration/interop_gen/namespaces_expected.dart b/web_generator/test/integration/interop_gen/namespaces_expected.dart new file mode 100644 index 00000000..0194efb9 --- /dev/null +++ b/web_generator/test/integration/interop_gen/namespaces_expected.dart @@ -0,0 +1,310 @@ +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: 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 Core._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS() + external static void addLogs( + _i1.JSArray logs, [ + _i1.JSArray logs2, + _i1.JSArray logs3, + _i1.JSArray logs4, + ]); + @_i1.JS() + external static Core_IAppConfig updateConfigEndpoint([String? apiEndpoint]); + @_i1.JS() + external static String get APP_NAME; + @_i1.JS() + external static String get APP_VERSION; + @_i1.JS('Core.Internal') + external static Core_Internal get Internal; + @_i1.JS('Core.LogEntry') + static Core_LogEntry LogEntry( + String timestamp, + String message, + ) => + Core_LogEntry( + timestamp, + message, + ); +} +extension type Security._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS() + external static double get TOKEN_LIFETIME_SECONDS; + @_i1.JS('Security.AuthService') + static Security_AuthService AuthService() => Security_AuthService(); +} +extension type Data._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS('Data.Models') + external static Data_Models get Models; + @_i1.JS('Data.UserRepository') + static Data_UserRepository UserRepository() => Data_UserRepository(); +} +extension type EnterpriseApp._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS() + external static String get APP_VERSION; + @_i1.JS('EnterpriseApp.Models') + external static EnterpriseApp_Models get Models; + @_i1.JS('EnterpriseApp.Utilities') + external static EnterpriseApp_Utilities get Utilities; + @_i1.JS('EnterpriseApp.DataServices') + external static EnterpriseApp_DataServices get DataServices; + @_i1.JS('EnterpriseApp.UI') + external static EnterpriseApp_UI get UI; +} +@_i1.JS() +external Data_Models_User get user1; +typedef UserService = EnterpriseApp_DataServices_UserService; +@_i1.JS() +external UserService get userService; +typedef ProductService = EnterpriseApp_DataServices_ProductService; +@_i1.JS() +external _i1.JSArray get allUsers; +@_i1.JS('Core.LogEntry') +extension type Core_LogEntry._(_i1.JSObject _) implements _i1.JSObject { + external Core_LogEntry( + String timestamp, + String message, + ); + + external String timestamp; + + external String message; +} +@_i1.JS('Core.IAppConfig') +extension type Core_IAppConfig._(_i1.JSObject _) implements _i1.JSObject { + external String apiEndpoint; + + external bool authRequired; +} +@_i1.JS('Core.Internal') +extension type Core_Internal._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS() + external static String get internalName; + @_i1.JS() + external static bool get devMode; +} +@_i1.JS('Security.IAuthToken') +extension type Security_IAuthToken._(_i1.JSObject _) implements _i1.JSObject { + external String token; + + external double expiresIn; + + external double userId; +} +@_i1.JS('Security.AuthService') +extension type Security_AuthService._(_i1.JSObject _) implements _i1.JSObject { + external Security_AuthService(); + + external Security_IAuthToken login( + String username, + String password, + ); +} +@_i1.JS('Data.IRepository') +extension type Data_IRepository._(_i1.JSObject _) + implements _i1.JSObject { + external T findById(num id); + external _i1.JSArray findAll(); + external void save(T entity); +} +@_i1.JS('Data.UserRepository') +extension type Data_UserRepository._(_i1.JSObject _) + implements Data_IRepository { + external Data_UserRepository(); + + @_i2.redeclare + external Data_Models_User findById(num id); + @_i2.redeclare + external _i1.JSArray findAll(); + @_i2.redeclare + external void save(Data_Models_User user); +} +@_i1.JS('Data.Models') +extension type Data_Models._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS('Data.Models.User') + static Data_Models_User User( + num id, + String name, + String email, + ) => + Data_Models_User( + id, + name, + email, + ); +} +@_i1.JS('Data.Models.IUser') +extension type Data_Models_IUser._(_i1.JSObject _) implements _i1.JSObject { + external double id; + + external String name; + + external String email; +} +@_i1.JS('Data.Models.User') +extension type Data_Models_User._(_i1.JSObject _) implements Data_Models_IUser { + external Data_Models_User( + num id, + String name, + String email, + ); + + external double id; + + external String name; + + external String email; +} +@_i1.JS('EnterpriseApp.Models') +extension type EnterpriseApp_Models._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS('EnterpriseApp.Models.User') + static EnterpriseApp_Models_User User( + num id, + String name, + String email, + ) => + EnterpriseApp_Models_User( + id, + name, + email, + ); + + @_i1.JS('EnterpriseApp.Models.Product') + static EnterpriseApp_Models_Product Product( + String sku, + String title, + num price, + ) => + EnterpriseApp_Models_Product( + sku, + title, + price, + ); +} +@_i1.JS('EnterpriseApp.Models.IUser') +extension type EnterpriseApp_Models_IUser._(_i1.JSObject _) + implements _i1.JSObject { + external double id; + + external String name; + + external String email; +} +@_i1.JS('EnterpriseApp.Models.User') +extension type EnterpriseApp_Models_User._(_i1.JSObject _) + implements EnterpriseApp_Models_IUser { + external EnterpriseApp_Models_User( + num id, + String name, + String email, + ); + + external double id; + + external String name; + + external String email; + + external String getDisplayName(); + external void linkUser(Data_Models_IUser data); + external Security_IAuthToken createAuthToken(); +} +@_i1.JS('EnterpriseApp.Models.IProduct') +extension type EnterpriseApp_Models_IProduct._(_i1.JSObject _) + implements _i1.JSObject { + external String sku; + + external String title; + + external double price; +} +@_i1.JS('EnterpriseApp.Models.Product') +extension type EnterpriseApp_Models_Product._(_i1.JSObject _) + implements EnterpriseApp_Models_IProduct { + external EnterpriseApp_Models_Product( + String sku, + String title, + num price, + ); + + external String sku; + + external String title; + + external double price; +} +@_i1.JS('EnterpriseApp.Utilities') +extension type EnterpriseApp_Utilities._(_i1.JSObject _) + implements _i1.JSObject { + @_i1.JS() + external static String formatCurrency( + num amount, [ + String? currency, + ]); + @_i1.JS() + external static bool isValidEmail(String email); +} +@_i1.JS('EnterpriseApp.DataServices') +extension type EnterpriseApp_DataServices._(_i1.JSObject _) + implements _i1.JSObject { + @_i1.JS('EnterpriseApp.DataServices.UserService') + static EnterpriseApp_DataServices_UserService UserService() => + EnterpriseApp_DataServices_UserService(); + + @_i1.JS('EnterpriseApp.DataServices.ProductService') + static EnterpriseApp_DataServices_ProductService ProductService() => + EnterpriseApp_DataServices_ProductService(); +} +@_i1.JS('EnterpriseApp.DataServices.IDataService') +extension type EnterpriseApp_DataServices_IDataService._( + _i1.JSObject _) implements _i1.JSObject { + external _i1.JSArray getAll(); + external T getById(String id); + external void save(T item); +} +@_i1.JS('EnterpriseApp.DataServices.UserService') +extension type EnterpriseApp_DataServices_UserService._(_i1.JSObject _) + implements + EnterpriseApp_DataServices_IDataService { + external EnterpriseApp_DataServices_UserService(); + + @_i2.redeclare + external _i1.JSArray getAll(); + @_i2.redeclare + external EnterpriseApp_Models_User getById(String id); + @_i2.redeclare + external void save(EnterpriseApp_Models_User user); +} +@_i1.JS('EnterpriseApp.DataServices.ProductService') +extension type EnterpriseApp_DataServices_ProductService._(_i1.JSObject _) + implements + EnterpriseApp_DataServices_IDataService { + external EnterpriseApp_DataServices_ProductService(); + + @_i2.redeclare + external EnterpriseApp_Models_Product getById(String id); + @_i2.redeclare + external void save(EnterpriseApp_Models_Product item); + external void add(EnterpriseApp_Models_Product product); + @_i1.JS('get') + external EnterpriseApp_Models_Product get$(num id); + @_i2.redeclare + external _i1.JSArray getAll(); +} +@_i1.JS('EnterpriseApp.UI') +extension type EnterpriseApp_UI._(_i1.JSObject _) implements _i1.JSObject { + @_i1.JS('EnterpriseApp.UI.Components') + external static EnterpriseApp_UI_Components get Components; +} +@_i1.JS('EnterpriseApp.UI.Components') +extension type EnterpriseApp_UI_Components._(_i1.JSObject _) + implements _i1.JSObject { + @_i1.JS() + external static void renderUserList( + _i1.JSArray users); +} diff --git a/web_generator/test/integration/interop_gen/namespaces_input.d.ts b/web_generator/test/integration/interop_gen/namespaces_input.d.ts new file mode 100644 index 00000000..7b1d3b80 --- /dev/null +++ b/web_generator/test/integration/interop_gen/namespaces_input.d.ts @@ -0,0 +1,153 @@ +export declare namespace Core { + const APP_NAME: string; + const APP_VERSION: string; + /** + * Represents the core application configuration. + * This interface is used across multiple services and modules. + */ + interface IAppConfig { + apiEndpoint: string; + authRequired: boolean; + } + class LogEntry { + timestamp: string; + message: string; + constructor(timestamp: string, message: string); + } + function addLogs(...logs: LogEntry[]): void; + function updateConfigEndpoint(apiEndpoint?: string): IAppConfig; +} +export declare namespace Core.Internal { + const internalName: string; + const devMode: boolean; +} +export declare namespace Security { + const TOKEN_LIFETIME_SECONDS: number; + interface IAuthToken { + token: string; + expiresIn: number; + userId: number; + } + /** + * A service for handling user authentication. + * Demonstrates using a type from another namespace (Core.LogEntry). + */ + class AuthService { + private logs; + login(username: string, password: string): IAuthToken; + } +} +export declare namespace Data { + /** + * A generic repository pattern interface. + * T can be a class from another namespace, like Models.User. + */ + interface IRepository { + findById(id: number): T; + findAll(): T[]; + save(entity: T): void; + } + namespace Models { + interface IUser { + id: number; + name: string; + email: string; + } + class User implements IUser { + id: number; + name: string; + email: string; + constructor(id: number, name: string, email: string); + } + } + /** + * An implementation of the IRepository for User entities. + * Demonstrates using a nested namespace alias. + */ + import UserModel = Data.Models.User; + class UserRepository implements IRepository { + private users; + findById(id: number): UserModel; + findAll(): UserModel[]; + save(user: UserModel): void; + } +} +export declare namespace EnterpriseApp { + const APP_VERSION: string; + namespace Models { + interface IUser { + id: number; + name: string; + email: string; + } + class User implements IUser { + id: number; + name: string; + email: string; + constructor(id: number, name: string, email: string); + getDisplayName(): string; + linkUser(data: Data.Models.IUser): void; + createAuthToken(): Security.IAuthToken; + } + interface IProduct { + sku: string; + title: string; + price: number; + } + class Product implements IProduct { + sku: string; + title: string; + price: number; + constructor(sku: string, title: string, price: number); + } + } + namespace Utilities { + /** + * Formats a number as currency. + * @param amount The number to format. + * @param currency The currency symbol. + * @returns A formatted string. + */ + function formatCurrency(amount: number, currency?: string): string; + /** + * Validates an email address. + * @param email The email string to validate. + * @returns True if the email is valid, false otherwise. + */ + function isValidEmail(email: string): boolean; + } + namespace DataServices { + interface IDataService { + getAll(): T[]; + getById(id: string): T; + save(item: T): void; + } + class UserService implements IDataService { + private users; + getAll(): Models.User[]; + getById(id: string): Models.User; + save(user: Models.User): void; + } + class ProductService implements IDataService { + getById(id: string): Models.Product; + save(item: Models.Product): void; + private products; + add(product: Models.Product): void; + get(id: number): Models.Product; + getAll(): Models.Product[]; + } + } + namespace UI { + namespace Components { + function renderUserList(users: Models.User[]): void; + } + } +} +export declare const user1: Data.Models.User; +declare const user2: Data.Models.User; +declare const product1: EnterpriseApp.Models.Product; +export import UserService = EnterpriseApp.DataServices.UserService; +export declare const userService: UserService; +export import ProductService = EnterpriseApp.DataServices.ProductService; +declare const productService: ProductService; +export declare const allUsers: Data.Models.User[]; diff --git a/web_generator/test/integration/interop_gen/project/config.yaml b/web_generator/test/integration/interop_gen/project/config.yaml index 4439924d..3bb57fd2 100644 --- a/web_generator/test/integration/interop_gen/project/config.yaml +++ b/web_generator/test/integration/interop_gen/project/config.yaml @@ -4,4 +4,5 @@ input: - input/a.d.ts - input/b.d.ts - input/c.d.ts + - input/f.d.ts output: ../../../../.dart_tool/interop_gen_project diff --git a/web_generator/test/integration/interop_gen/project/input/f.d.ts b/web_generator/test/integration/interop_gen/project/input/f.d.ts new file mode 100644 index 00000000..e8b8f03c --- /dev/null +++ b/web_generator/test/integration/interop_gen/project/input/f.d.ts @@ -0,0 +1,9 @@ +import * as e from "./e"; + +export declare const rootUser: e.Product; +export declare const rootConfig: e.Configuration; +export function setConfiguration(newConfig: e.Configuration): void; +export function buyProductForUser(author: e.User, product: e.Product, quantity?: number): number; +export function createNewProducts(author: e.User, ...products: e.Product[]): void; +export function getProductsMadeByUser(author: e.User): e.Product[]; +export function getTotalPriceOfProducts(...products: e.Product[]): number; diff --git a/web_generator/test/integration/interop_gen/project/output/f.dart b/web_generator/test/integration/interop_gen/project/output/f.dart new file mode 100644 index 00000000..fbc368fa --- /dev/null +++ b/web_generator/test/integration/interop_gen/project/output/f.dart @@ -0,0 +1,69 @@ +// 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 void setConfiguration(Configuration newConfig); +@_i1.JS() +external double buyProductForUser( + User author, + Product product, [ + num? quantity, +]); +@_i1.JS() +external void createNewProducts( + User author, + _i1.JSArray products, [ + _i1.JSArray products2, + _i1.JSArray products3, + _i1.JSArray products4, +]); +@_i1.JS() +external _i1.JSArray getProductsMadeByUser(User author); +@_i1.JS() +external double getTotalPriceOfProducts( + _i1.JSArray products, [ + _i1.JSArray products2, + _i1.JSArray products3, + _i1.JSArray products4, +]); +@_i1.JS() +external Product get rootUser; +@_i1.JS() +external Configuration get rootConfig; +extension type Configuration._(_i1.JSObject _) implements _i1.JSObject { + external Configuration( + String version, + String apiUrl, + ); + + external String get version; + external String get apiUrl; +} +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 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; +} diff --git a/web_generator/test/integration/interop_gen/typealias_expected.dart b/web_generator/test/integration/interop_gen/typealias_expected.dart index 39cd7cee..f5aa6ce7 100644 --- a/web_generator/test/integration/interop_gen/typealias_expected.dart +++ b/web_generator/test/integration/interop_gen/typealias_expected.dart @@ -1,4 +1,5 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names +// ignore_for_file: camel_case_types, constant_identifier_names +// ignore_for_file: non_constant_identifier_names // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:js_interop' as _i1; @@ -24,6 +25,7 @@ typedef Box = _i1.JSArray<_i1.JSArray>; typedef Logger = LoggerType; typedef Direction = AnonymousUnion; typedef Method = AnonymousUnion$1; +typedef Planet = Space_Planet; @_i1.JS() external LoggerContainer<_i1.JSNumber> get loggerContainers; @_i1.JS() @@ -73,4 +75,8 @@ extension type const AnonymousUnion$1._(String _) { static const AnonymousUnion$1 OPTIONS = AnonymousUnion$1._('OPTIONS'); } +@_i1.JS('Space.Planet') +extension type Space_Planet._(_i1.JSObject _) implements _i1.JSObject { + external double radius; +} typedef LoggerContainer = _i1.JSArray; diff --git a/web_generator/test/integration/interop_gen/typealias_input.d.ts b/web_generator/test/integration/interop_gen/typealias_input.d.ts index 27fccba2..52e2b7ae 100644 --- a/web_generator/test/integration/interop_gen/typealias_input.d.ts +++ b/web_generator/test/integration/interop_gen/typealias_input.d.ts @@ -5,6 +5,12 @@ declare enum LoggerType { File = 3, Other = 4 } +declare namespace Space { + interface Planet { + radius: number; + } + const earth: Planet; +} export type Username = string; export type Age = number; export type IsActive = boolean; @@ -17,6 +23,7 @@ export type PrismFromShape2D = Array; export type Logger = LoggerType; export type Direction = "N" | "S" | "E" | "W"; export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; +export type Planet = Space.Planet; type LoggerContainer = N[]; export declare const loggerContainers: LoggerContainer; export declare let myLogger: Logger; diff --git a/web_generator/test/integration/interop_gen/types_input.d.ts b/web_generator/test/integration/interop_gen/types_input.d.ts index 19dbb515..e28e3846 100644 --- a/web_generator/test/integration/interop_gen/types_input.d.ts +++ b/web_generator/test/integration/interop_gen/types_input.d.ts @@ -23,4 +23,4 @@ export declare const myint32Array: Int32Array; export declare const myUint8Array: Uint8Array; export declare const myUint16Array: Uint16Array; export declare const myUint32Array: Uint32Array; -export declare const myUint8ClampedArray: Uint8ClampedArray; +export declare const myUint8ClampedArray: Uint8ClampedArray; \ No newline at end of file diff --git a/web_generator/test/qualified_name_test.dart b/web_generator/test/qualified_name_test.dart new file mode 100644 index 00000000..caa7e0f9 --- /dev/null +++ b/web_generator/test/qualified_name_test.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; +import 'package:web_generator/src/interop_gen/qualified_name.dart'; + +final normalStringExamples = { + 'A': ['A'], + 'A.B': ['A', 'B'], + 'A.B.C': ['A', 'B', 'C'], + 'URL': ['URL'], + 'Promise': ['Promise'], + 'EpahsImpl.TMeta': ['EpahsImpl', 'TMeta'], + 'EnterpriseApp.DataServices.IDataService.T': [ + 'EnterpriseApp', + 'DataServices', + 'IDataService', + 'T' + ] +}; + +final fullyQualifiedStringExamples = { + 'A': { + 'qualified': ['A'], + 'import': null + }, + 'HTMLElement': { + 'qualified': ['HTMLElement'], + 'import': null + }, + '"../../test/integration/interop_gen/web_types_input".EventManipulationFunc': + { + 'qualified': ['EventManipulationFunc'], + 'import': '../../test/integration/interop_gen/web_types_input' + }, + '"dummy".Shape': { + 'qualified': ['Shape'], + 'import': 'dummy' + }, + '"../../test/integration/interop_gen/web_types_input".ElementStamp.T': { + 'qualified': ['ElementStamp', 'T'], + 'import': '../../test/integration/interop_gen/web_types_input' + }, + '"../../test/integration/interop_gen/ts_typing_input".ComposedType.T': { + 'qualified': ['ComposedType', 'T'], + 'import': '../../test/integration/interop_gen/ts_typing_input' + }, + '"../../test/integration/interop_gen/ts_typing_input".MyEnum': { + 'qualified': ['MyEnum'], + 'import': '../../test/integration/interop_gen/ts_typing_input' + }, + '"integration/interop_gen/classes_input".Geometry.Coordinates.Point2D': { + 'qualified': ['Geometry', 'Coordinates', 'Point2D'], + 'import': 'integration/interop_gen/classes_input' + }, + '"node:console".Console.Mode': { + 'qualified': ['Console', 'Mode'], + 'import': 'node:console' + } +}; + +void main() { + group('Qualified Name Testing', () { + group('Parse From Normal String', () { + for (final MapEntry(key: k, value: v) in normalStringExamples.entries) { + test(k, () { + final qualifiedName = QualifiedName.raw(k); + expect(qualifiedName.length, equals(v.length)); + expect(qualifiedName.first.part, equals(v.first)); + expect(qualifiedName.map((q) => q.part), equals(v)); + }); + } + }); + + group('Parse From Fully Qualified', () { + for (final MapEntry(key: k, value: v) + in fullyQualifiedStringExamples.entries) { + test(k, () { + final (qualifiedName, import) = parseTSFullyQualifiedName(k); + final expectedQName = v['qualified'] as List; + final expectedImport = v['import'] as String?; + + expect(import, equals(expectedImport)); + + expect(qualifiedName.length, equals(expectedQName.length)); + expect(qualifiedName.first.part, equals(expectedQName.first)); + expect(qualifiedName.map((q) => q.part), equals(expectedQName)); + }); + } + }); + }); +}