Skip to content

Add Variable Declaration Support #382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions web_generator/lib/src/ast.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked briefly about having the IDL use this AST as well, but I'm okay either if you want to do this now as you're adding nodes or later when we have the necessary AST for the IDL generation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add this later when I'm working with nodes similar to those in the IDL. Since we didn't find any instances of global declarations, the next AST declaration would be interfaces

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 }
69 changes: 69 additions & 0 deletions web_generator/lib/src/banned_names.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,75 @@
// 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.

/// Dart reserved keywords, used for resolving conflict with a name.
///
/// Source: https://dart.dev/guides/language/language-tour#keywords.
const keywords = {
'abstract',
'as',
'assert',
'async',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'covariant',
'default',
'deferred',
'do',
'dynamic',
'else',
'enum',
'export',
'extends',
'extension',
'external',
'factory',
'false',
'final',
'finally',
'for',
'Function',
'get',
'hide',
'if',
'implements',
'import',
'in',
'interface',
'is',
'late',
'library',
'mixin',
'new',
'null',
'on',
'operator',
'part',
'required',
'rethrow',
'return',
'set',
'show',
'static',
'super',
'switch',
'sync',
'this',
'throw',
'true',
'try',
'typedef',
'var',
'void',
'while',
'with',
'yield',
};

const bannedNames = <String>{
'assert',
'break',
Expand Down
4 changes: 2 additions & 2 deletions web_generator/lib/src/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:path/path.dart' as p;

final bindingsGeneratorPath = p.fromUri(Platform.script.resolve('../lib/src'));

Future<void> compileDartMain({String? langVersion}) async {
Future<void> compileDartMain({String? langVersion, String? dir}) async {
await runProc(
Platform.executable,
[
Expand All @@ -27,7 +27,7 @@ Future<void> compileDartMain({String? langVersion}) async {
'-o',
'dart_main.js',
],
workingDirectory: bindingsGeneratorPath,
workingDirectory: dir ?? bindingsGeneratorPath,
);
}

Expand Down
24 changes: 16 additions & 8 deletions web_generator/lib/src/dart_main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import 'dart:js_interop';
import 'package:args/args.dart';
import 'package:code_builder/code_builder.dart' as code;
import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';

import 'dts/parser.dart';
import 'dts/transform.dart';
import 'generate_bindings.dart';
import 'interop_gen/parser.dart';
import 'interop_gen/transform.dart';
import 'js/filesystem_api.dart';
import 'util.dart';

Expand Down Expand Up @@ -44,7 +45,7 @@ void main(List<String> args) async {
}
}

// TODO(nikeokoronkwo): Add support for configuration
// TODO(https://github.com/dart-lang/web/issues/376): Add support for configuration
Future<void> generateJSInteropBindings({
required Iterable<String> inputs,
required String output,
Expand All @@ -54,13 +55,20 @@ Future<void> generateJSInteropBindings({
final jsDeclarations = parseDeclarationFiles(inputs);

// transform declarations
final dartDeclarations = transformDeclarations(jsDeclarations);
final dartDeclarations = transform(jsDeclarations);

// generate
final generatedCode = dartDeclarations.generate();

// write code to file
fs.writeFileSync(output.toJS, generatedCode.toJS);
final generatedCodeMap = dartDeclarations.generate();

// write code to file(s)
if (inputs.length == 1) {
final singleEntry = generatedCodeMap.entries.single;
fs.writeFileSync(output.toJS, singleEntry.value.toJS);
} else {
for (final entry in generatedCodeMap.entries) {
fs.writeFileSync(p.join(output, entry.key).toJS, entry.value.toJS);
}
}
}

Future<void> generateIDLBindings({
Expand Down
6 changes: 0 additions & 6 deletions web_generator/lib/src/dts/parser.dart

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
// 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.

class DeclarationMap {
String generate() {
throw UnimplementedError();
}
}
import 'package:code_builder/code_builder.dart';

DeclarationMap transformDeclarations(Object? jsDeclarations) {
throw UnimplementedError();
Expression generateJSAnnotation([String? name]) {
return refer('JS', 'dart:js_interop')
.call([if (name != null) literalString(name)]);
}
37 changes: 37 additions & 0 deletions web_generator/lib/src/interop_gen/namer.dart
Original file line number Diff line number Diff line change
@@ -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.

class ID {
final String type;
final String name;
final int? index;

const ID({required this.type, required this.name, this.index});

bool get isUnnamed => name == 'unnamed';

@override
String toString() => '$type#$name${index != null ? '#$index' : ''}';
}

class UniqueNamer {
final Set<String> _usedNames;

UniqueNamer([Iterable<String> used = const <String>[]])
: _usedNames = used.toSet();

static ID parse(String id) {
String? index;
final [type, name, ...ids] = id.split('#');
if (ids.isEmpty) index = ids.single;

return ID(
type: type, name: name, index: index == null ? null : int.parse(index));
}

/// Adds a [name] to used names.
void markUsed(String name) {
_usedNames.add(name);
}
}
21 changes: 21 additions & 0 deletions web_generator/lib/src/interop_gen/parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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:js_interop';

import '../js/typescript.dart' as ts;

class ParserResult {
ts.TSProgram program;
Iterable<String> files;

ParserResult({required this.program, required this.files});
}

ParserResult parseDeclarationFiles(Iterable<String> files) {
final program = ts.createProgram(files.jsify() as JSArray<JSString>,
ts.TSCompilerOptions(declaration: true));

return ParserResult(program: program, files: files);
}
Loading