[interop] Add Support for Function Declarations (#392)
* Interop Gen: Support Function Declarations
Fixes #389
* code resolution
* changed primitive types from `enum` to `static const` types
* minor changes
* formatting
* resolved code issues, added test cases, and refactored primitive type handling
* Comment Resolution:
- enum switch expression refactoring
- other fixes
* completed comment resolution
diff --git a/web_generator/lib/src/ast.dart b/web_generator/lib/src/ast.dart
deleted file mode 100644
index 14a43a6..0000000
--- a/web_generator/lib/src/ast.dart
+++ /dev/null
@@ -1,169 +0,0 @@
-// 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:code_builder/code_builder.dart';
-
-import 'interop_gen/generate.dart';
-import 'interop_gen/namer.dart';
-
-sealed class Node {
- abstract final String? name;
- abstract final ID id;
- final String? dartName;
-
- Node() : dartName = null;
-}
-
-abstract class Declaration extends Node {
- @override
- abstract final String name;
-
- Spec emit();
-}
-
-abstract class NamedDeclaration extends Declaration {
- ReferredType asReferredType([List<Type>? typeArgs]) =>
- ReferredType(name: name, declaration: this, typeParams: typeArgs ?? []);
-}
-
-abstract interface class ExportableDeclaration extends Declaration {
- /// Whether this declaration is exported.
- bool get exported;
-}
-
-abstract class Type extends Node {
- Reference emit();
-}
-
-enum PrimitiveType implements Type {
- string('string'),
- any('any'),
- object('object'),
- number('number'),
- boolean('boolean'),
- undefined('undefined'),
- unknown('unknown');
-
- const PrimitiveType(this.name);
-
- @override
- final String name;
-
- @override
- ID get id => ID(type: 'type', name: name);
-
- // TODO(https://github.com/dart-lang/web/pull/386): Configuration options: double and num
- @override
- Reference emit() {
- return switch (this) {
- PrimitiveType.string => refer('String'),
- PrimitiveType.any => refer('JSAny', 'dart:js_interop'),
- PrimitiveType.object => refer('JSObject', 'dart:js_interop'),
- PrimitiveType.number => refer('int'),
- PrimitiveType.boolean => refer('bool'),
- PrimitiveType.undefined => TypeReference((t) => t
- ..symbol = 'JSAny'
- ..url = 'dart:js_interop'
- ..isNullable = true),
- PrimitiveType.unknown => TypeReference((t) => t
- ..symbol = 'JSAny'
- ..url = 'dart:js_interop'
- ..isNullable = true)
- };
- }
-
- @override
- String? get dartName => null;
-}
-
-// TODO(): Refactor name - not all types can be referred to
-// (only specific types) Instead change this
-// to represent `typeof` declarations.
-// TODO(): Create a shared type for such types that
-// can be referred to (i.e namespace, interface, class)
-// as a type `ReferrableDeclaration`.
-class ReferredType<T extends Declaration> extends Type {
- @override
- String name;
-
- @override
- ID get id => ID(type: 'type', name: name);
-
- T declaration;
-
- List<Type> typeParams;
-
- ReferredType(
- {required this.name,
- required this.declaration,
- this.typeParams = const []});
-
- @override
- Reference emit() {
- // TODO: implement emit
- throw UnimplementedError();
- }
-}
-
-// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`)
-class UnionType extends Type {
- List<Type> types;
-
- UnionType({required this.types});
-
- @override
- ID get id => ID(type: 'type', name: types.map((t) => t.id).join('|'));
-
- @override
- Reference emit() {
- throw UnimplementedError();
- }
-
- @override
- String? get name => null;
-}
-
-class VariableDeclaration extends NamedDeclaration
- implements ExportableDeclaration {
- /// The variable modifier, as represented in TypeScript
- VariableModifier modifier;
-
- @override
- String name;
-
- Type type;
-
- @override
- bool exported;
-
- VariableDeclaration(
- {required this.name,
- required this.type,
- required this.modifier,
- required this.exported});
-
- @override
- ID get id => ID(type: 'var', name: name);
-
- @override
- Spec emit() {
- if (modifier == VariableModifier.$const) {
- return Method((m) => m
- ..name = name
- ..type = MethodType.getter
- ..annotations.add(generateJSAnnotation())
- ..external = true
- ..returns = type.emit());
- } else {
- // getter and setter -> single variable
- return Field((f) => f
- ..external = true
- ..name = name
- ..type = type.emit()
- ..annotations.add(generateJSAnnotation()));
- }
- }
-}
-
-enum VariableModifier { let, $const, $var }
diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart
new file mode 100644
index 0000000..6ebc13c
--- /dev/null
+++ b/web_generator/lib/src/ast/base.dart
@@ -0,0 +1,76 @@
+// 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:code_builder/code_builder.dart';
+
+import '../interop_gen/namer.dart';
+import 'types.dart';
+
+class GlobalOptions {
+ static int variardicArgsCount = 4;
+ static bool shouldEmitJsTypes = false;
+}
+
+class Options {}
+
+// TODO(nikeokoronkwo): Remove this once we address isNullable
+class DeclarationOptions extends Options {
+ DeclarationOptions();
+
+ TypeOptions toTypeOptions({bool nullable = false}) =>
+ TypeOptions(nullable: nullable);
+}
+
+class TypeOptions extends Options {
+ bool nullable;
+
+ TypeOptions({this.nullable = false});
+}
+
+class ASTOptions {
+ bool parameter;
+ bool emitJSTypes;
+ int variardicArgsCount;
+
+ ASTOptions(
+ {this.parameter = false,
+ this.variardicArgsCount = 4,
+ this.emitJSTypes = false});
+}
+
+sealed class Node {
+ abstract final String? name;
+ abstract final ID id;
+ String? get dartName;
+
+ Spec emit([Options? options]);
+
+ Node();
+}
+
+abstract class Declaration extends Node {
+ @override
+ abstract final String name;
+
+ @override
+ Spec emit([covariant DeclarationOptions? options]);
+}
+
+abstract class NamedDeclaration extends Declaration {
+ ReferredType asReferredType([List<Type>? typeArgs]) =>
+ ReferredType(name: name, declaration: this, typeParams: typeArgs ?? []);
+}
+
+abstract interface class ExportableDeclaration extends Declaration {
+ /// Whether this declaration is exported.
+ bool get exported;
+}
+
+abstract class Type extends Node {
+ @override
+ String? dartName;
+
+ @override
+ Reference emit([covariant TypeOptions? options]);
+}
diff --git a/web_generator/lib/src/ast/builtin.dart b/web_generator/lib/src/ast/builtin.dart
new file mode 100644
index 0000000..563572c
--- /dev/null
+++ b/web_generator/lib/src/ast/builtin.dart
@@ -0,0 +1,116 @@
+// 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.
+
+// ignore_for_file: non_constant_identifier_names
+
+import 'package:code_builder/code_builder.dart';
+
+import '../interop_gen/namer.dart';
+import 'base.dart';
+
+/// A built in type supported by `dart:js_interop` or by this library
+/// (with generated declarations)
+class BuiltinType extends Type {
+ @override
+ final String name;
+
+ final List<Type> typeParams;
+
+ /// Whether the given type is present in "dart:js_interop"
+ final bool fromDartJSInterop;
+
+ // TODO(nikeokoronkwo): Types in general should have an `isNullable`
+ // property on them to indicate nullability for Dart generated code.
+ final bool? isNullable;
+
+ BuiltinType(
+ {required this.name,
+ this.typeParams = const [],
+ this.fromDartJSInterop = false,
+ this.isNullable});
+
+ @override
+ ID get id => ID(type: 'type', name: name);
+
+ @override
+ String? get dartName => null;
+
+ @override
+ Reference emit([TypeOptions? options]) {
+ options ??= TypeOptions();
+
+ return TypeReference((t) => t
+ ..symbol = name
+ ..types.addAll(typeParams
+ // if there is only one type param, and it is void, ignore
+ .where((p) => typeParams.length != 1 || p != $voidType)
+ .map((p) => p.emit(TypeOptions())))
+ ..url = fromDartJSInterop ? 'dart:js_interop' : null
+ ..isNullable = isNullable ?? options!.nullable);
+ }
+
+ static final BuiltinType $voidType = BuiltinType(name: 'void');
+ static final BuiltinType anyType =
+ BuiltinType(name: 'JSAny', fromDartJSInterop: true, isNullable: true);
+
+ static BuiltinType primitiveType(PrimitiveType typeIdentifier,
+ {bool? shouldEmitJsType,
+ bool? isNullable,
+ List<Type> typeParams = const []}) {
+ shouldEmitJsType ??= GlobalOptions.shouldEmitJsTypes;
+ return switch (typeIdentifier) {
+ PrimitiveType.int ||
+ PrimitiveType.num ||
+ PrimitiveType.double when shouldEmitJsType =>
+ BuiltinType(
+ name: 'JSNumber', fromDartJSInterop: true, isNullable: isNullable),
+ PrimitiveType.int => BuiltinType(name: 'int', isNullable: isNullable),
+ PrimitiveType.num => BuiltinType(name: 'num', isNullable: isNullable),
+ PrimitiveType.double =>
+ BuiltinType(name: 'double', isNullable: isNullable),
+ PrimitiveType.boolean => shouldEmitJsType
+ ? BuiltinType(
+ name: 'JSBoolean',
+ fromDartJSInterop: true,
+ isNullable: isNullable)
+ : BuiltinType(name: 'bool', isNullable: isNullable),
+ PrimitiveType.string => shouldEmitJsType
+ ? BuiltinType(
+ name: 'JSString', fromDartJSInterop: true, isNullable: isNullable)
+ : BuiltinType(name: 'String', isNullable: isNullable),
+ PrimitiveType.$void || PrimitiveType.undefined => $voidType,
+ PrimitiveType.any || PrimitiveType.unknown => anyType,
+ PrimitiveType.object => BuiltinType(
+ name: 'JSObject', fromDartJSInterop: true, isNullable: isNullable),
+ PrimitiveType.array => BuiltinType(
+ name: 'JSArray',
+ typeParams: [typeParams.single],
+ fromDartJSInterop: true,
+ isNullable: isNullable),
+ PrimitiveType.promise => BuiltinType(
+ name: 'JSPromise',
+ typeParams: [typeParams.single],
+ fromDartJSInterop: true,
+ isNullable: isNullable),
+ PrimitiveType.function => BuiltinType(
+ name: 'JSFunction', fromDartJSInterop: true, isNullable: isNullable),
+ };
+ }
+}
+
+enum PrimitiveType {
+ int,
+ num,
+ double,
+ boolean,
+ string,
+ $void,
+ any,
+ object,
+ unknown,
+ undefined,
+ array,
+ promise,
+ function
+}
diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart
new file mode 100644
index 0000000..1b02774
--- /dev/null
+++ b/web_generator/lib/src/ast/declarations.dart
@@ -0,0 +1,140 @@
+// 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:code_builder/code_builder.dart';
+
+import '../interop_gen/namer.dart';
+import 'base.dart';
+import 'helpers.dart';
+import 'types.dart';
+
+class VariableDeclaration extends NamedDeclaration
+ implements ExportableDeclaration {
+ /// The variable modifier, as represented in TypeScript
+ VariableModifier modifier;
+
+ @override
+ String name;
+
+ Type type;
+
+ @override
+ bool exported;
+
+ VariableDeclaration(
+ {required this.name,
+ required this.type,
+ required this.modifier,
+ required this.exported});
+
+ @override
+ ID get id => ID(type: 'var', name: name);
+
+ @override
+ Spec emit([DeclarationOptions? options]) {
+ if (modifier == VariableModifier.$const) {
+ return Method((m) => m
+ ..name = name
+ ..type = MethodType.getter
+ ..annotations.add(generateJSAnnotation())
+ ..external = true
+ ..returns = type.emit());
+ } else {
+ // getter and setter -> single variable
+ return Field((f) => f
+ ..external = true
+ ..name = name
+ ..type = type.emit()
+ ..annotations.add(generateJSAnnotation()));
+ }
+ }
+
+ @override
+ String? get dartName => null;
+}
+
+enum VariableModifier { let, $const, $var }
+
+class FunctionDeclaration extends NamedDeclaration
+ implements ExportableDeclaration {
+ @override
+ final String name;
+
+ @override
+ final String? dartName;
+
+ final List<ParameterDeclaration> parameters;
+
+ final List<GenericType> typeParameters;
+
+ final Type returnType;
+
+ @override
+ bool exported;
+
+ @override
+ ID id;
+
+ FunctionDeclaration(
+ {required this.name,
+ required this.id,
+ this.dartName,
+ this.parameters = const [],
+ this.typeParameters = const [],
+ required this.exported,
+ required this.returnType});
+
+ @override
+ Spec emit([DeclarationOptions? options]) {
+ options ??= DeclarationOptions();
+
+ final requiredParams = <Parameter>[];
+ final optionalParams = <Parameter>[];
+ for (final p in parameters) {
+ if (p.variardic) {
+ optionalParams.addAll(spreadParam(p, GlobalOptions.variardicArgsCount));
+ requiredParams.add(p.emit(options));
+ } else {
+ if (p.optional) {
+ optionalParams.add(p.emit(options));
+ } else {
+ requiredParams.add(p.emit(options));
+ }
+ }
+ }
+
+ return Method((m) => m
+ ..external = true
+ ..name = dartName ?? name
+ ..annotations.add(generateJSAnnotation(
+ dartName == null || dartName == name ? null : name))
+ ..types
+ .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
+ ..returns = returnType.emit()
+ ..requiredParameters.addAll(requiredParams)
+ ..optionalParameters.addAll(optionalParams));
+ }
+}
+
+class ParameterDeclaration {
+ final String name;
+
+ final bool optional;
+
+ final Type type;
+
+ final bool variardic;
+
+ ParameterDeclaration(
+ {required this.name,
+ this.optional = false,
+ required this.type,
+ this.variardic = false});
+
+ Parameter emit([DeclarationOptions? options]) {
+ return Parameter((p) => p
+ ..name = name
+ ..type = type.emit(TypeOptions(nullable: optional)));
+ }
+}
diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart
new file mode 100644
index 0000000..e987ccd
--- /dev/null
+++ b/web_generator/lib/src/ast/helpers.dart
@@ -0,0 +1,56 @@
+// 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:code_builder/code_builder.dart';
+
+import 'base.dart';
+import 'builtin.dart';
+import 'declarations.dart';
+
+BuiltinType? getSupportedType(String name, [List<Type> typeParams = const []]) {
+ final type = switch (name) {
+ 'Array' => PrimitiveType.array,
+ 'Promise' => PrimitiveType.promise,
+ _ => null
+ };
+
+ if (type == null) return null;
+
+ return BuiltinType.primitiveType(type, typeParams: [
+ getJSTypeAlternative(typeParams.singleOrNull ?? BuiltinType.anyType)
+ ]);
+}
+
+Type getJSTypeAlternative(Type type) {
+ if (type is BuiltinType) {
+ if (type.fromDartJSInterop) return type;
+
+ final primitiveType = switch (type.name) {
+ 'num' => PrimitiveType.num,
+ 'int' => PrimitiveType.int,
+ 'double' => PrimitiveType.double,
+ 'String' => PrimitiveType.string,
+ 'bool' => PrimitiveType.boolean,
+ _ => null
+ };
+
+ if (primitiveType == null) return BuiltinType.anyType;
+
+ return BuiltinType.primitiveType(primitiveType, shouldEmitJsType: true);
+ }
+ return type;
+}
+
+Expression generateJSAnnotation([String? name]) {
+ return refer('JS', 'dart:js_interop')
+ .call([if (name != null) literalString(name)]);
+}
+
+List<Parameter> spreadParam(ParameterDeclaration p, int count) {
+ return List.generate(count - 1, (i) {
+ final paramNumber = i + 2;
+ final paramName = '${p.name}$paramNumber';
+ return ParameterDeclaration(name: paramName, type: p.type).emit();
+ });
+}
diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart
new file mode 100644
index 0000000..e3d789b
--- /dev/null
+++ b/web_generator/lib/src/ast/types.dart
@@ -0,0 +1,70 @@
+// 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:code_builder/code_builder.dart';
+import '../interop_gen/namer.dart';
+import 'base.dart';
+
+class ReferredType<T extends Declaration> extends Type {
+ @override
+ String name;
+
+ @override
+ ID get id => ID(type: 'type', name: name);
+
+ T declaration;
+
+ List<Type> typeParams;
+
+ ReferredType(
+ {required this.name,
+ required this.declaration,
+ this.typeParams = const []});
+
+ @override
+ Reference emit([TypeOptions? options]) {
+ // TODO: implement emit
+ throw UnimplementedError();
+ }
+}
+
+// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`)
+class UnionType extends Type {
+ List<Type> types;
+
+ UnionType({required this.types});
+
+ @override
+ ID get id => ID(type: 'type', name: types.map((t) => t.id).join('|'));
+
+ @override
+ Reference emit([TypeOptions? options]) {
+ throw UnimplementedError('TODO: Implement UnionType.emit');
+ }
+
+ @override
+ String? get name => null;
+}
+
+/// The base class for a type generic (like 'T')
+class GenericType extends Type {
+ @override
+ final String name;
+
+ final Type? constraint;
+
+ final Declaration? parent;
+
+ GenericType({required this.name, this.constraint, this.parent});
+
+ @override
+ Reference emit([TypeOptions? options]) => TypeReference((t) => t
+ ..symbol = name
+ ..bound = constraint?.emit()
+ ..isNullable = options?.nullable);
+
+ @override
+ ID get id =>
+ ID(type: 'generic-type', name: '$name@${parent?.id ?? "(anonymous)"}');
+}
diff --git a/web_generator/lib/src/interop_gen/generate.dart b/web_generator/lib/src/interop_gen/generate.dart
deleted file mode 100644
index 54ce2d3..0000000
--- a/web_generator/lib/src/interop_gen/generate.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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:code_builder/code_builder.dart';
-
-Expression generateJSAnnotation([String? name]) {
- return refer('JS', 'dart:js_interop')
- .call([if (name != null) literalString(name)]);
-}
diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart
index 3725c11..dd07115 100644
--- a/web_generator/lib/src/interop_gen/namer.dart
+++ b/web_generator/lib/src/interop_gen/namer.dart
@@ -2,6 +2,8 @@
// 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 '../banned_names.dart';
+
class ID {
final String type;
final String name;
@@ -21,10 +23,44 @@
UniqueNamer([Iterable<String> used = const <String>[]])
: _usedNames = used.toSet();
+ /// Creates a unique name and ID for a given declaration to prevent
+ /// name collisions in Dart applications
+ ///
+ /// (Dart does not support operator overloading)
+ ({ID id, String name}) makeUnique(String name, String type) {
+ // nested structures (and anonymous structures) may not have a name
+ if (name.isEmpty) {
+ name = 'unnamed';
+ }
+
+ var newName = name;
+ if (keywords.contains(newName)) {
+ newName = '$newName\$';
+ }
+
+ var i = 0;
+ while (_usedNames.contains(newName)) {
+ ++i;
+ newName = '$name\$$i';
+ }
+
+ markUsed(newName);
+ return (
+ id: ID(type: type, name: name, index: i == 0 ? null : i),
+ name: newName
+ );
+ }
+
static ID parse(String id) {
String? index;
- final [type, name, ...ids] = id.split('#');
- if (ids.isEmpty) index = ids.single;
+ String name;
+ final [type, ...parts] = id.split('#');
+ if (parts.isEmpty) {
+ throw Exception('Invalid ID: $id');
+ } else {
+ name = parts[0];
+ if (parts.length > 1) index = parts[1];
+ }
return ID(
type: type, name: name, index: index == null ? null : int.parse(index));
diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart
index f6c0a78..663786b 100644
--- a/web_generator/lib/src/interop_gen/transform.dart
+++ b/web_generator/lib/src/interop_gen/transform.dart
@@ -7,7 +7,7 @@
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
-import '../ast.dart';
+import '../ast/base.dart';
import '../js/typescript.dart' as ts;
import '../js/typescript.types.dart';
import 'namer.dart';
@@ -29,9 +29,9 @@
final specs = declMap.decls.values.map((d) {
return switch (d) {
final Declaration n => n.emit(),
- final Type t => t.emit(),
+ final Type _ => null,
};
- });
+ }).whereType<Spec>();
final lib = Library((l) => l..body.addAll(specs));
return MapEntry(file, formatter.format('${lib.accept(emitter)}'));
});
diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart
index 0da0e79..404c299 100644
--- a/web_generator/lib/src/interop_gen/transform/transformer.dart
+++ b/web_generator/lib/src/interop_gen/transform/transformer.dart
@@ -3,7 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:js_interop';
-import '../../ast.dart';
+import '../../ast/base.dart';
+import '../../ast/builtin.dart';
+import '../../ast/declarations.dart';
+import '../../ast/helpers.dart';
+import '../../ast/types.dart';
import '../../js/typescript.dart' as ts;
import '../../js/typescript.types.dart';
import '../namer.dart';
@@ -41,6 +45,8 @@
nodeMap.addAll({for (final d in decs) d.id.toString(): d});
default:
final Declaration decl = switch (node.kind) {
+ TSSyntaxKind.FunctionDeclaration =>
+ _transformFunction(node as TSFunctionDeclaration),
_ => throw Exception('Unsupported Declaration Kind: ${node.kind}')
};
// ignore: dead_code This line will not be dead in future decl additions
@@ -50,6 +56,180 @@
nodes.add(node);
}
+ List<Declaration> _transformVariable(TSVariableStatement variable) {
+ // get the modifier of the declaration
+ final modifiers = variable.modifiers.toDart;
+ final isExported = modifiers.any((m) {
+ return m.kind == TSSyntaxKind.ExportKeyword;
+ });
+
+ var modifier = VariableModifier.$var;
+
+ if ((variable.declarationList.flags & TSNodeFlags.Const) != 0) {
+ modifier = VariableModifier.$const;
+ } else if ((variable.declarationList.flags & TSNodeFlags.Let) != 0) {
+ modifier = VariableModifier.let;
+ }
+
+ return variable.declarationList.declarations.toDart.map((d) {
+ namer.markUsed(d.name.text);
+ return VariableDeclaration(
+ name: d.name.text,
+ type: d.type == null ? BuiltinType.anyType : _transformType(d.type!),
+ modifier: modifier,
+ exported: isExported);
+ }).toList();
+ }
+
+ TSNode? _getDeclarationByName(TSIdentifier name) {
+ final symbol = typeChecker.getSymbolAtLocation(name);
+
+ final declarations = symbol?.getDeclarations();
+ // TODO(https://github.com/dart-lang/web/issues/387): Some declarations may not be defined on file,
+ // and may be from an import statement
+ // We should be able to handle these
+ return declarations?.toDart.first;
+ }
+
+ FunctionDeclaration _transformFunction(TSFunctionDeclaration function) {
+ final name = function.name.text;
+
+ final modifiers = function.modifiers.toDart;
+ final isExported = modifiers.any((m) {
+ return m.kind == TSSyntaxKind.ExportKeyword;
+ });
+
+ final params = function.parameters.toDart;
+
+ final typeParams = function.typeParameters?.toDart;
+
+ final (id: id, name: uniqueName) = namer.makeUnique(name, 'fun');
+
+ return FunctionDeclaration(
+ name: name,
+ id: id,
+ dartName: uniqueName,
+ exported: isExported,
+ parameters: params.map(_transformParameter).toList(),
+ typeParameters:
+ typeParams?.map(_transformTypeParamDeclaration).toList() ?? [],
+ returnType: function.type != null
+ ? _transformType(function.type!)
+ : BuiltinType.anyType);
+ }
+
+ ParameterDeclaration _transformParameter(TSParameterDeclaration parameter) {
+ final type = parameter.type != null
+ ? _transformType(parameter.type!, parameter: true)
+ : BuiltinType.anyType;
+ final isOptional = parameter.questionToken != null;
+ final isVariardic = parameter.dotDotDotToken != null;
+
+ // what kind of parameter is this
+ switch (parameter.name.kind) {
+ case TSSyntaxKind.Identifier:
+ return ParameterDeclaration(
+ name: (parameter.name as TSIdentifier).text,
+ type: type,
+ variardic: isVariardic,
+ optional: isOptional);
+ default:
+ // TODO: Support Destructured Object Parameters
+ // and Destructured Array Parameters
+ throw Exception('Unsupported Parameter Name kind ${parameter.kind}');
+ }
+ }
+
+ GenericType _transformTypeParamDeclaration(
+ TSTypeParameterDeclaration typeParam) {
+ return GenericType(
+ name: typeParam.name.text,
+ constraint: typeParam.constraint == null
+ ? BuiltinType.anyType
+ : _transformType(typeParam.constraint!));
+ }
+
+ /// Parses the type
+ ///
+ /// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`)
+ /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types
+ Type _transformType(TSTypeNode type, {bool parameter = false}) {
+ if (type.kind == TSSyntaxKind.UnionType) {
+ final unionType = type as TSUnionTypeNode;
+ return UnionType(
+ types: unionType.types.toDart.map<Type>(_transformType).toList());
+ }
+
+ if (type.kind == TSSyntaxKind.TypeReference) {
+ final refType = type as TSTypeReferenceNode;
+
+ final name = refType.typeName.text;
+ final typeArguments = refType.typeArguments?.toDart;
+
+ var declarationsMatching = nodeMap.findByName(name);
+
+ if (declarationsMatching.isEmpty) {
+ // check if builtin
+ // TODO(https://github.com/dart-lang/web/issues/380): A better name
+ // for this, and adding support for "supported declarations"
+ // (also a better name for that)
+ final supportedType = getSupportedType(
+ name, (typeArguments ?? []).map(_transformType).toList());
+ if (supportedType != null) {
+ return supportedType;
+ }
+
+ // TODO: In the case of overloading, should/shouldn't we handle more than one declaration?
+ final declaration = _getDeclarationByName(refType.typeName);
+
+ if (declaration == null) {
+ throw Exception('Found no declaration matching $name');
+ }
+
+ if (declaration.kind == TSSyntaxKind.TypeParameter) {
+ return GenericType(name: name);
+ }
+
+ transform(declaration);
+
+ declarationsMatching = nodeMap.findByName(name);
+ }
+
+ // TODO: In the case of overloading, should/shouldn't we handle more than one declaration?
+ final firstNode =
+ declarationsMatching.whereType<NamedDeclaration>().first;
+
+ return firstNode.asReferredType(
+ (typeArguments ?? []).map(_transformType).toList(),
+ );
+ }
+
+ if (type.kind == TSSyntaxKind.ArrayType) {
+ return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [
+ getJSTypeAlternative(
+ _transformType((type as TSArrayTypeNode).elementType))
+ ]);
+ }
+
+ // check for primitive type via its kind
+ final primitiveType = switch (type.kind) {
+ TSSyntaxKind.ArrayType => PrimitiveType.array,
+ TSSyntaxKind.StringKeyword => PrimitiveType.string,
+ TSSyntaxKind.AnyKeyword => PrimitiveType.any,
+ TSSyntaxKind.ObjectKeyword => PrimitiveType.object,
+ TSSyntaxKind.NumberKeyword =>
+ (parameter ? PrimitiveType.num : PrimitiveType.double),
+ TSSyntaxKind.UndefinedKeyword => PrimitiveType.undefined,
+ TSSyntaxKind.UnknownKeyword => PrimitiveType.unknown,
+ TSSyntaxKind.BooleanKeyword => PrimitiveType.boolean,
+ TSSyntaxKind.VoidKeyword => PrimitiveType.$void,
+ _ => throw UnsupportedError(
+ 'The given type with kind ${type.kind} is not supported yet')
+ };
+
+ return BuiltinType.primitiveType(primitiveType);
+ }
+
NodeMap filter() {
final filteredDeclarations = NodeMap();
@@ -66,7 +246,7 @@
filteredDeclarations.add(e);
}
break;
- case final PrimitiveType _:
+ case final BuiltinType _:
// primitive types are generated by default
break;
case Type():
@@ -94,15 +274,29 @@
switch (decl) {
case final VariableDeclaration v:
- if (v.type is! PrimitiveType) filteredDeclarations.add(v.type);
+ if (v.type is! BuiltinType) filteredDeclarations.add(v.type);
+ break;
+ case final FunctionDeclaration f:
+ if (f.returnType is! BuiltinType) {
+ filteredDeclarations.add(f.returnType);
+ }
+ filteredDeclarations.addAll({
+ for (final node in f.parameters.map((p) => p.type))
+ node.id.toString(): node
+ });
+ filteredDeclarations.addAll({
+ for (final node
+ in f.typeParameters.map((p) => p.constraint).whereType<Type>())
+ node.id.toString(): node
+ });
break;
case final UnionType u:
filteredDeclarations.addAll({
- for (final t in u.types.where((t) => t is! PrimitiveType))
+ for (final t in u.types.where((t) => t is! BuiltinType))
t.id.toString(): t
});
break;
- case final PrimitiveType _:
+ case final BuiltinType _:
// primitive types are generated by default
break;
default:
@@ -121,95 +315,4 @@
return filteredDeclarations;
}
-
- List<Declaration> _transformVariable(TSVariableStatement variable) {
- // get the modifier of the declaration
- final modifiers = variable.modifiers.toDart;
- final isExported = modifiers.any((m) {
- return m.kind == TSSyntaxKind.ExportKeyword;
- });
-
- var modifier = VariableModifier.$var;
-
- if ((variable.declarationList.flags & TSNodeFlags.Const) != 0) {
- modifier = VariableModifier.$const;
- } else if ((variable.declarationList.flags & TSNodeFlags.Let) != 0) {
- modifier = VariableModifier.let;
- }
-
- return variable.declarationList.declarations.toDart.map((d) {
- namer.markUsed(d.name.text);
- return VariableDeclaration(
- name: d.name.text,
- type: d.type == null ? PrimitiveType.any : _transformType(d.type!),
- modifier: modifier,
- exported: isExported);
- }).toList();
- }
-
- TSNode? _getDeclarationByName(TSIdentifier name) {
- final symbol = typeChecker.getSymbolAtLocation(name);
-
- final declarations = symbol?.getDeclarations();
- // TODO(https://github.com/dart-lang/web/issues/387): Some declarations may not be defined on file,
- // and may be from an import statement
- // We should be able to handle these
- return declarations?.toDart.first;
- }
-
- /// Parses the type
- ///
- /// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`)
- /// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types
- Type _transformType(TSTypeNode type) {
- if (type.kind == TSSyntaxKind.UnionType) {
- final unionType = type as TSUnionTypeNode;
- // parse union type
- return UnionType(
- types: unionType.types.toDart.map<Type>(_transformType).toList());
- }
-
- if (type.kind == TSSyntaxKind.TypeReference) {
- // reference type
- final refType = type as TSTypeReferenceNode;
-
- final name = refType.typeName.text;
- final typeArguments = refType.typeArguments?.toDart;
-
- var declarationsMatching = nodeMap.findByName(name);
- if (declarationsMatching.isEmpty) {
- // TODO: In the case of overloading, should/shouldn't we handle more than one declaration?
- final declaration = _getDeclarationByName(refType.typeName);
-
- if (declaration == null) {
- throw Exception('Found no declaration matching $name');
- }
-
- transform(declaration);
-
- declarationsMatching = nodeMap.findByName(name);
- }
-
- // TODO: In the case of overloading, should/shouldn't we handle more than one declaration?
- final firstNode =
- declarationsMatching.whereType<NamedDeclaration>().first;
-
- return firstNode.asReferredType(
- (typeArguments ?? []).map(_transformType).toList(),
- );
- }
-
- // check for its kind
- return switch (type.kind) {
- TSSyntaxKind.StringKeyword => PrimitiveType.string,
- TSSyntaxKind.AnyKeyword => PrimitiveType.any,
- TSSyntaxKind.ObjectKeyword => PrimitiveType.object,
- TSSyntaxKind.NumberKeyword => PrimitiveType.number,
- TSSyntaxKind.UndefinedKeyword => PrimitiveType.undefined,
- TSSyntaxKind.UnknownKeyword => PrimitiveType.unknown,
- TSSyntaxKind.BooleanKeyword => PrimitiveType.boolean,
- _ => throw UnsupportedError(
- 'The given type with kind ${type.kind} is not supported yet')
- };
- }
}
diff --git a/web_generator/lib/src/js/typescript.dart b/web_generator/lib/src/js/typescript.dart
index bc9a73d..f098057 100644
--- a/web_generator/lib/src/js/typescript.dart
+++ b/web_generator/lib/src/js/typescript.dart
@@ -1,3 +1,7 @@
+// 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.
+
@JS('ts')
library;
diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart
index e6d31d3..ed8855d 100644
--- a/web_generator/lib/src/js/typescript.types.dart
+++ b/web_generator/lib/src/js/typescript.types.dart
@@ -1,3 +1,7 @@
+// 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.
+
// ignore_for_file: constant_identifier_names
@JS('ts')
@@ -7,6 +11,8 @@
import 'package:meta/meta.dart';
+import 'typescript.dart';
+
extension type const TSSyntaxKind._(num _) {
/// To be ignored
static const TSSyntaxKind EndOfFileToken = TSSyntaxKind._(1);
@@ -18,6 +24,7 @@
static const TSSyntaxKind InterfaceDeclaration = TSSyntaxKind._(264);
static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262);
static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278);
+ static const TSSyntaxKind Parameter = TSSyntaxKind._(169);
/// keywords
static const TSSyntaxKind ExportKeyword = TSSyntaxKind._(95);
@@ -34,13 +41,17 @@
static const TSSyntaxKind UndefinedKeyword = TSSyntaxKind._(157);
static const TSSyntaxKind SetKeyword = TSSyntaxKind._(153);
static const TSSyntaxKind UnknownKeyword = TSSyntaxKind._(159);
+ static const TSSyntaxKind VoidKeyword = TSSyntaxKind._(116);
// types
static const TSSyntaxKind UnionType = TSSyntaxKind._(192);
static const TSSyntaxKind TypeReference = TSSyntaxKind._(183);
+ static const TSSyntaxKind ArrayType = TSSyntaxKind._(188);
/// Other
static const TSSyntaxKind Identifier = TSSyntaxKind._(80);
+ static const TSSyntaxKind ObjectBindingPattern = TSSyntaxKind._(206);
+ static const TSSyntaxKind ArrayBindingPattern = TSSyntaxKind._(207);
static const TSSyntaxKind TypeParameter = TSSyntaxKind._(168);
static const TSSyntaxKind HeritageClause = TSSyntaxKind._(298);
static const TSSyntaxKind ExpressionWithTypeArguments = TSSyntaxKind._(233);
@@ -57,11 +68,20 @@
external TSSyntaxKind get kind;
external TSNode get parent;
external TSNodeFlags get flags;
+ external String getText([TSSourceFile? sourceFile]);
+ external String getFullText([TSSourceFile? sourceFile]);
}
@JS('TypeNode')
extension type TSTypeNode._(JSObject _) implements TSNode {}
+@JS('ArrayTypeNode')
+extension type TSArrayTypeNode._(JSObject _) implements TSTypeNode {
+ @redeclare
+ TSSyntaxKind get kind => TSSyntaxKind.ArrayType;
+ external TSTypeNode get elementType;
+}
+
@JS('UnionTypeNode')
extension type TSUnionTypeNode._(JSObject _) implements TSTypeNode {
@redeclare
@@ -89,21 +109,47 @@
external String get text;
}
-@JS('VariableDeclaration')
+@JS('VariableStatement')
extension type TSVariableStatement._(JSObject _) implements TSStatement {
external TSVariableDeclarationList get declarationList;
external TSNodeArray<TSNode> get modifiers;
}
+@JS('VariableDeclarationList')
+extension type TSVariableDeclarationList._(JSObject _) implements TSNode {
+ external TSNodeArray<TSVariableDeclaration> get declarations;
+}
+
@JS('VariableDeclaration')
extension type TSVariableDeclaration._(JSObject _) implements TSDeclaration {
external TSIdentifier get name;
external TSTypeNode? get type;
}
-@JS('VariableDeclarationList')
-extension type TSVariableDeclarationList._(JSObject _) implements TSNode {
- external TSNodeArray<TSVariableDeclaration> get declarations;
+@JS('FunctionDeclaration')
+extension type TSFunctionDeclaration._(JSObject _) implements TSDeclaration {
+ external TSIdentifier get name;
+ external TSTypeNode? get type;
+ external TSNode? get asteriskToken;
+ external TSNodeArray<TSParameterDeclaration> get parameters;
+ external TSNodeArray<TSTypeParameterDeclaration>? get typeParameters;
+ external TSNodeArray<TSNode> get modifiers;
+}
+
+@JS('ParameterDeclaration')
+extension type TSParameterDeclaration._(JSObject _) implements TSDeclaration {
+ external TSNode get name;
+ external TSTypeNode? get type;
+ external TSNodeArray<TSNode>? get modifiers;
+ external TSNode? get questionToken;
+ external TSNode? get dotDotDotToken;
+}
+
+@JS('TypeParameterDeclaration')
+extension type TSTypeParameterDeclaration._(JSObject _)
+ implements TSDeclaration {
+ external TSIdentifier get name;
+ external TSTypeNode? get constraint;
}
@JS('NodeArray')
diff --git a/web_generator/lib/src/js/typescript_extensions.dart b/web_generator/lib/src/js/typescript_extensions.dart
deleted file mode 100644
index 603d62d..0000000
--- a/web_generator/lib/src/js/typescript_extensions.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'typescript.types.dart';
-
-extension Names on TSSyntaxKind {
- String get name {
- return switch (this) {
- TSSyntaxKind.DeclareKeyword => 'declare',
- TSSyntaxKind.ExportKeyword => 'export',
- TSSyntaxKind.ExtendsKeyword => 'extends',
- TSSyntaxKind.ImplementsKeyword => 'implements',
- TSSyntaxKind.VariableDeclaration => 'variable',
- _ => throw UnsupportedError('The keyword is not supported at the moment')
- };
- }
-}
diff --git a/web_generator/test/integration/interop_gen/functions_expected.dart b/web_generator/test/integration/interop_gen/functions_expected.dart
new file mode 100644
index 0000000..4c8c2f8
--- /dev/null
+++ b/web_generator/test/integration/interop_gen/functions_expected.dart
@@ -0,0 +1,47 @@
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:js_interop' as _i1;
+
+@_i1.JS()
+external String greetUser(String name);
+@_i1.JS()
+external void logMessages(
+ _i1.JSArray<_i1.JSString> messages, [
+ _i1.JSArray<_i1.JSString> messages2,
+ _i1.JSArray<_i1.JSString> messages3,
+ _i1.JSArray<_i1.JSString> messages4,
+]);
+@_i1.JS()
+external _i1.JSPromise<U> delay<U extends _i1.JSAny?>(num ms, [U? returnValue]);
+@_i1.JS()
+external _i1.JSArray<_i1.JSNumber> toArray(num a);
+@_i1.JS()
+external double square(num a);
+@_i1.JS()
+external double pow(num a);
+@_i1.JS('pow')
+external double pow$1(num a, num power);
+@_i1.JS('toArray')
+external _i1.JSArray<_i1.JSString> toArray$1(String a);
+@_i1.JS()
+external _i1.JSObject createUser(String name, [num? age, String? role]);
+@_i1.JS()
+external T firstElement<T extends _i1.JSAny?>(_i1.JSArray<T> arr);
+@_i1.JS()
+external void throwError(String msg);
+@_i1.JS()
+external _i1.JSArray<T> wrapInArray<T extends _i1.JSAny?>(T value);
+@_i1.JS()
+external T identity<T extends _i1.JSAny?>(T value);
+@_i1.JS()
+external void someFunction<A extends _i1.JSAny?>(_i1.JSArray<A> arr);
+@_i1.JS('someFunction')
+external B someFunction$1<A extends _i1.JSAny?, B extends _i1.JSAny?>(
+ _i1.JSArray<A> arr,
+);
+@_i1.JS()
+external T logTuple<T extends _i1.JSArray<_i1.JSAny?>>(
+ T args, [
+ T args2,
+ T args3,
+ T args4,
+]);
diff --git a/web_generator/test/integration/interop_gen/functions_input.d.ts b/web_generator/test/integration/interop_gen/functions_input.d.ts
new file mode 100644
index 0000000..6fc8665
--- /dev/null
+++ b/web_generator/test/integration/interop_gen/functions_input.d.ts
@@ -0,0 +1,16 @@
+export declare function greetUser(name: string): string;
+export declare function logMessages(...messages: string[]): void;
+export declare function delay<U>(ms: number, returnValue?: U): Promise<U>;
+export declare function toArray(a: number): number[];
+export declare function square(a: number): number;
+export declare function pow(a: number): number;
+export declare function pow(a: number, power: number): number;
+export declare function toArray(a: string): string[];
+export declare function createUser(name: string, age?: number, role?: string): object;
+export declare function firstElement<T>(arr: T[]): T;
+export declare function throwError(msg: string): void;
+export declare function wrapInArray<T>(value: T): T[];
+export declare function identity<T = string>(value: T): T;
+export declare function someFunction<A>(arr: A[]): undefined;
+export declare function someFunction<A, B>(arr: A[]): B;
+export declare function logTuple<T extends any[]>(...args: T): T;
diff --git a/web_generator/test/integration/interop_gen/variables_expected.dart b/web_generator/test/integration/interop_gen/variables_expected.dart
index 2609c67..1df172a 100644
--- a/web_generator/test/integration/interop_gen/variables_expected.dart
+++ b/web_generator/test/integration/interop_gen/variables_expected.dart
@@ -2,11 +2,11 @@
import 'dart:js_interop' as _i1;
@_i1.JS()
-external int counter;
+external double counter;
@_i1.JS()
external String get appName;
@_i1.JS()
-external int globalCounter;
+external double globalCounter;
@_i1.JS()
external _i1.JSObject globalObject;
@_i1.JS()
@@ -14,18 +14,22 @@
@_i1.JS()
external String username;
@_i1.JS()
-external int get foo;
+external double get foo;
@_i1.JS()
-external int get bar;
+external double get bar;
@_i1.JS()
-external int free;
+external double free;
@_i1.JS()
-external int dom;
+external double dom;
@_i1.JS()
external String fred;
@_i1.JS()
external String doctor;
@_i1.JS()
-external _i1.JSAny something;
+external _i1.JSAny? something;
@_i1.JS()
external _i1.JSAny? get maybeValue;
+@_i1.JS()
+external _i1.JSArray<_i1.JSString> get names;
+@_i1.JS()
+external _i1.JSArray<_i1.JSString> get newNames;
diff --git a/web_generator/test/integration/interop_gen/variables_input.d.ts b/web_generator/test/integration/interop_gen/variables_input.d.ts
index 933449b..1bc7d05 100644
--- a/web_generator/test/integration/interop_gen/variables_input.d.ts
+++ b/web_generator/test/integration/interop_gen/variables_input.d.ts
@@ -9,3 +9,5 @@
export declare let fred: string, doctor: string;
export declare let something: any;
export declare const maybeValue: unknown;
+export declare const names: string[];
+export declare const newNames: Array<string>;