blob: ef58c04a1f5b775494c08b38d1d30a630b4b8ce8 [file] [log] [blame] [edit]
// Copyright (c) 2023, 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 'package:code_builder/code_builder.dart' as code;
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'banned_names.dart';
import 'bcd.dart';
import 'doc_provider.dart';
import 'formatting.dart';
import 'singletons.dart';
import 'type_aliases.dart';
import 'type_union.dart';
import 'util.dart';
import 'webidl_api.dart' as idl;
import 'webref_elements_api.dart';
typedef TranslationResult = Map<String, code.Library>;
class _Library {
final String name;
final String url;
// Contains both IDL `interface`s and `namespace`s.
final List<idl.Interfacelike> interfacelikes = [];
final List<idl.Interfacelike> interfaceMixins = [];
final List<idl.Typedef> typedefs = [];
final List<idl.Enum> enums = [];
final List<idl.Callback> callbacks = [];
final List<idl.Interfacelike> callbackInterfaces = [];
_Library(this.name, this.url);
void _addNamed<T extends idl.Named>(idl.Node node, List<T> list) {
final named = node as T;
final name = named.name;
final translator = Translator.instance!;
assert(!translator._typeToLibrary.containsKey(name));
translator._typeToLibrary[name] = this;
assert(!translator._typeToDeclaration.containsKey(name));
translator._typeToDeclaration[name] = node;
list.add(named);
}
void add(idl.Node node) {
final type = node.type;
final translator = Translator.instance!;
// TODO(srujzs): We may want an enum here, but that would be slower due to
// a string lookup in the set of enums.
switch (type) {
case 'interface mixin':
case 'interface':
case 'namespace':
case 'dictionary':
// If we have a not partial interfacelike, then we will emit it in this
// library. However, in order to collect any possible cross-library
// partial interfaces, we track interfacelikes on the translator as
// well.
final isMixin = type == 'interface mixin';
final interfaceList = isMixin ? interfaceMixins : interfacelikes;
final interfacelike = node as idl.Interfacelike;
if (!node.partial) {
_addNamed<idl.Interfacelike>(node, interfaceList);
} else {
translator._typeToPartials
.putIfAbsent(interfacelike.name, () => [])
.add(interfacelike);
}
break;
case 'typedef':
_addNamed<idl.Typedef>(node, typedefs);
break;
case 'includes':
final includes = node as idl.Includes;
translator._includes
.putIfAbsent(includes.target, () => [])
.add(includes.includes);
break;
case 'enum':
_addNamed<idl.Enum>(node, enums);
break;
case 'callback interface':
_addNamed<idl.Interfacelike>(node, callbackInterfaces);
break;
case 'callback':
final callback = node as idl.Callback;
/// TODO(joshualitt): Maybe handle this case a bit more elegantly?
if (callback.name == 'Function') {
return;
}
_addNamed<idl.Callback>(callback, callbacks);
break;
case 'eof':
break;
default:
throw Exception('Unexpected node type $type');
}
}
}
/// If [rawType] corresponds to an IDL type that we declare as a typedef,
/// desugars the typedef, accounting for nullability along the way.
///
/// Otherwise, returns null.
_RawType? _desugarTypedef(_RawType rawType) {
final decl = Translator.instance!._typeToDeclaration[rawType.type];
return switch (decl?.type) {
'typedef' => _getRawType((decl as idl.Typedef).idlType)
..nullable |= rawType.nullable,
// TODO(srujzs): If we ever add a generic JS function type, we should
// maybe leverage that here so we have stronger type-checking of
// callbacks.
'callback' ||
'callback interface' =>
_RawType('JSFunction', rawType.nullable),
// TODO(srujzs): Enums in the WebIDL are just strings, but we could make
// them easier to work with on the Dart side.
'enum' => _RawType('JSString', rawType.nullable),
_ => null
};
}
/// Given a [rawType], return its JS type-equivalent type if it's a type that is
/// declared in the IDL.
///
/// Otherwise, return null.
_RawType? _getJSTypeEquivalent(_RawType rawType) {
final type = rawType.type;
final nullable = rawType.nullable;
final decl = Translator.instance!._typeToDeclaration[type];
if (decl != null) {
final nodeType = decl.type;
switch (nodeType) {
case 'interface':
case 'dictionary':
return _RawType('JSObject', nullable);
default:
final desugaredType = _desugarTypedef(rawType);
if (desugaredType != null) {
// The output of `_desugarTypedef` is always an IDL decl type or a JS
// type, so either get the equivalent in the former case or return the
// JS type directly.
return _getJSTypeEquivalent(desugaredType) ?? desugaredType;
}
throw Exception('Unhandled type $type with node type: $nodeType');
}
}
return null;
}
_RawType _computeRawTypeUnion(_RawType rawType1, _RawType rawType2) {
final type1 = rawType1.type;
final type2 = rawType2.type;
final nullable1 = rawType1.nullable;
final nullable2 = rawType2.nullable;
final typeParam1 = rawType1.typeParameter;
final typeParam2 = rawType2.typeParameter;
// If either type parameter is null, then the resulting union can never be a
// generic type, so return null.
_RawType? computeTypeParamUnion(_RawType? typeParam1, _RawType? typeParam2) =>
typeParam1 != null && typeParam2 != null
? _computeRawTypeUnion(typeParam1, typeParam2)
: null;
// Equality.
if (type1 == type2) {
return _RawType(type1, nullable1 || nullable2,
computeTypeParamUnion(typeParam1, typeParam2));
}
// This sentinel is only for nullability.
if (type1 == 'JSUndefined') return _RawType(type2, true, typeParam2);
if (type2 == 'JSUndefined') return _RawType(type1, true, typeParam1);
// If the two types are not equal, we can just use `JSNumber` as the union can
// never be `JSInteger` or `JSDouble` anyways.
if (type1 == 'JSInteger' || type1 == 'JSDouble') rawType1.type = 'JSNumber';
if (type2 == 'JSInteger' || type2 == 'JSDouble') rawType2.type = 'JSNumber';
// In the case of unions, we should try and get a JS type-able type to get a
// better LUB.
final unionableType1 = _getJSTypeEquivalent(rawType1) ?? rawType1;
final unionableType2 = _getJSTypeEquivalent(rawType2) ?? rawType2;
// We choose `JSAny` if they're not both JS types.
return _RawType(
computeJsTypeUnion(unionableType1.type, unionableType2.type) ?? 'JSAny',
unionableType1.nullable || unionableType2.nullable,
computeTypeParamUnion(
unionableType1.typeParameter, unionableType2.typeParameter));
}
/// Returns a [_RawType] for the given [idl.IDLType].
_RawType _getRawType(idl.IDLType idlType) {
// For union types, we take the possible union of all the types using a LUB.
if (idlType.union) {
final types = (idlType.idlType as JSArray<idl.IDLType>).toDart;
final unionType = _getRawType(types[0]);
for (var i = 1; i < types.length; i++) {
unionType.update(types[i]);
}
return unionType..nullable |= idlType.nullable;
}
String type;
var nullable = idlType.nullable;
_RawType? typeParameter;
if (idlType.generic.isNotEmpty) {
final types = (idlType.idlType as JSArray<idl.IDLType>).toDart;
if (types.length == 1) {
typeParameter = _getRawType(types[0]);
} else if (types.length > 1) {
assert(types.length == 2);
assert(idlType.generic == 'record');
}
type = idlType.generic;
} else {
type = (idlType.idlType as JSString).toDart;
}
// Handles types that don't exist in the set of IDL type declarations. They
// are either some special values or JS builtin types.
// `WindowProxy` doesn't exist as an interface in the IDL. For our purposes,
// `Window` is the appropriate interface.
if (type == 'WindowProxy') type = 'Window';
// `any` is marked non-nullable in the IDL, but since it is a union of
// `undefined`, it can be nullable for our purposes.
if (type == 'any') nullable = true;
final translator = Translator.instance!;
final decl = translator._typeToDeclaration[type];
final alias = idlOrBuiltinToJsTypeAliases[type];
assert(decl != null || alias != null);
if (alias == null && !translator.markTypeAsUsed(type)) {
// If the type is an IDL type that is never generated, use its JS type
// equivalent.
type = _getJSTypeEquivalent(_RawType(type, false))!.type;
}
return _RawType(alias ?? type, nullable, typeParameter);
}
/// A class representing either a type that corresponds to an IDL declaration or
/// a `dart:js_interop` JS types (including sentinels).
///
/// This should not include IDL types for which there isn't a declaration e.g.
/// `any` or a JS built-in type e.g. `ArrayBuffer`.
class _RawType {
String type;
bool nullable;
_RawType? typeParameter;
_RawType(this.type, this.nullable, [this.typeParameter]) {
// While the IDL does not define `undefined` as nullable, it is treated as
// null in interop.
if (type == 'JSUndefined') nullable = true;
}
void update(idl.IDLType idlType) {
final union = _computeRawTypeUnion(this, _getRawType(idlType));
type = union.type;
nullable = union.nullable;
typeParameter = union.typeParameter;
}
@override
String toString() => '_RawType(type: $type, nullable: $nullable, '
'typeParameter: $typeParameter)';
}
class _Parameter {
final Set<String> _names;
final _RawType type;
bool isOptional;
late final String name = _generateName();
_Parameter._(this._names, this.type, this.isOptional);
factory _Parameter(idl.Argument argument) => _Parameter._(
{argument.name}, _getRawType(argument.idlType), argument.optional);
String _generateName() {
final namesList = _names.toList();
namesList.sort();
return namesList
.sublist(0, 1)
.followedBy(namesList.sublist(1).map(capitalize))
.join('Or');
}
void update(idl.Argument argument) {
final thatName = argument.name;
_names.add(thatName);
type.update(argument.idlType);
if (argument.optional) {
isOptional = true;
}
}
}
sealed class _Property {
late final _MemberName name;
final _RawType type;
final MdnProperty? mdnProperty;
// TODO(srujzs): Remove ignore after
// https://github.com/dart-lang/sdk/issues/55720 is resolved.
// ignore: unused_element_parameter
_Property(_MemberName name, idl.IDLType idlType, [this.mdnProperty])
: type = _getRawType(idlType) {
// Rename the property if there's a collision with the type name.
final dartName = name.name;
final jsName = name.jsOverride.isEmpty ? dartName : name.jsOverride;
this.name =
dartName == type.type ? _MemberName('${dartName}_', jsName) : name;
}
}
class _Attribute extends _Property {
final bool isStatic;
final bool isReadOnly;
_Attribute(super.name, super.idlType, super.mdnProperty,
{required this.isStatic, required this.isReadOnly});
}
class _Field extends _Property {
final bool isRequired;
_Field(super.name, super.idlType, super.mdnProperty,
{required this.isRequired});
}
class _Constant extends _Property {
final String valueType;
final JSAny value;
_Constant(super.name, super.idlType, this.valueType, this.value);
}
abstract class _OverridableMember {
final List<_Parameter> parameters = [];
_OverridableMember(JSArray<idl.Argument> rawParameters) {
for (var i = 0; i < rawParameters.length; i++) {
parameters.add(_Parameter(rawParameters[i]));
}
}
void _processParameters(JSArray<idl.Argument> thoseParameters) {
// Assume if we have extra arguments beyond what was provided in some other
// method, that these are all optional.
final thatLength = thoseParameters.length;
for (var i = thatLength; i < parameters.length; i++) {
parameters[i].isOptional = true;
}
for (var i = 0; i < thatLength; i++) {
final argument = thoseParameters[i];
if (i >= parameters.length) {
// We assume these parameters must be optional, regardless of what the
// IDL says.
parameters.add(_Parameter(argument)..isOptional = true);
} else {
parameters[i].update(argument);
}
}
}
}
class _OverridableOperation extends _OverridableMember {
bool _finalized = false;
_MemberName _name;
final String special;
final _RawType returnType;
final MdnProperty? mdnProperty;
late final _MemberName name = _generateName();
_OverridableOperation._(this._name, this.special, this.returnType,
this.mdnProperty, super.parameters);
factory _OverridableOperation(idl.Operation operation, _MemberName memberName,
MdnProperty? mdnProperty) =>
_OverridableOperation._(memberName, operation.special,
_getRawType(operation.idlType), mdnProperty, operation.arguments);
bool get isStatic => special == 'static';
_MemberName _generateName() {
// The name is determined after all updates are done, so finalize the
// operation.
_finalized = true;
// Rename the member if the name collides with a return or parameter type.
final dartName = _name.name;
if (dartName == returnType.type ||
parameters.any((parameter) => dartName == parameter.type.type)) {
underscoreName();
}
return _name;
}
void underscoreName() {
final jsName = _name.jsOverride.isEmpty ? _name.name : _name.jsOverride;
_name = _MemberName('${_name.name}_', jsName);
}
void update(idl.Operation that) {
assert(
!_finalized,
'Call to _OverridableOperation.update was made after the operation was '
'finalized.');
final jsOverride = _name.jsOverride;
final thisName = jsOverride.isNotEmpty ? jsOverride : _name.name;
assert((that.name.isEmpty || thisName == that.name) &&
special == that.special);
returnType.update(that.idlType);
_processParameters(that.arguments);
}
}
class _OverridableConstructor extends _OverridableMember {
_OverridableConstructor(idl.Constructor constructor)
: super(constructor.arguments);
void update(idl.Constructor that) => _processParameters(that.arguments);
}
class _PartialInterfacelike {
final String name;
final String type;
String? inheritance;
final Map<String, _OverridableOperation> operations = {};
final Map<String, _OverridableOperation> staticOperations = {};
final List<_Property> properties = [];
final List<_Property> extensionProperties = [];
final MdnInterface? mdnInterface;
_OverridableConstructor? constructor;
_PartialInterfacelike._(
this.name, this.type, String? inheritance, this.mdnInterface) {
_setInheritance(inheritance);
}
factory _PartialInterfacelike(
idl.Interfacelike interfacelike, MdnInterface? mdnInterface) {
final partialInterfacelike = _PartialInterfacelike._(interfacelike.name,
interfacelike.type, interfacelike.inheritance, mdnInterface);
partialInterfacelike._processMembers(interfacelike.members);
return partialInterfacelike;
}
void _processMembers(JSArray<idl.Member> nodeMembers) {
for (var i = 0; i < nodeMembers.length; i++) {
final member = nodeMembers[i];
final type = member.type;
switch (type) {
case 'constructor':
if (!_shouldGenerateMember(name)) break;
final idlConstructor = member as idl.Constructor;
if (_hasHTMLConstructorAttribute(idlConstructor)) break;
if (constructor == null) {
constructor = _OverridableConstructor(idlConstructor);
} else {
constructor!.update(idlConstructor);
}
break;
case 'const':
final constant = member as idl.Constant;
// Note that constants do not have browser compatibility data, so we
// always emit.
properties.add(_Constant(_MemberName(constant.name), constant.idlType,
constant.value.type, constant.value.value));
break;
case 'attribute':
final attribute = member as idl.Attribute;
final isStatic = attribute.special == 'static';
final attributeName = attribute.name;
if (!_shouldGenerateMember(attributeName, isStatic: isStatic)) break;
// `SVGElement.className` returns an `SVGAnimatedString`, but its
// corresponding setter `Element.className` takes a `String`. As these
// two types are incompatible, we need to move this member to an
// extension instead. As it shares the same name as the getter
// `Element.className`, users will need to apply the extension
// explicitly.
final isExtensionMember =
name == 'SVGElement' && attributeName == 'className';
final memberList =
isExtensionMember ? extensionProperties : properties;
memberList.add(_Attribute(
_MemberName(attributeName),
attribute.idlType,
mdnInterface?.propertyFor(attributeName, isStatic: isStatic),
isStatic: isStatic,
isReadOnly: attribute.readonly));
break;
case 'operation':
final operation = member as idl.Operation;
final special = operation.special;
var operationName = operation.name;
// Some special operations may not have any MDN data and may be given
// a name that is irrelevant to the IDL, so avoid querying in that
// case and always emit.
var shouldQueryMDN = true;
switch (special) {
case 'getter':
if (operationName.isEmpty) {
operationName = 'operator []';
shouldQueryMDN = false;
}
break;
case 'setter':
if (operationName.isEmpty) {
operationName = 'operator []=';
shouldQueryMDN = false;
}
break;
case 'static':
break;
default:
// TODO(srujzs): Should we handle other special operations,
// unnamed or otherwise? For now, don't emit the unnamed ones and
// do nothing special for the named ones.
if (operationName.isEmpty) continue;
}
final isStatic = operation.special == 'static';
if (shouldQueryMDN &&
!_shouldGenerateMember(operationName, isStatic: isStatic)) {
break;
}
final docs = shouldQueryMDN
? mdnInterface?.propertyFor(operationName, isStatic: isStatic)
: null;
// Static member may have the same name as instance members in the
// IDL, but not in Dart. Rename the static member if so.
if (isStatic) {
if (staticOperations.containsKey(operationName)) {
staticOperations[operationName]!.update(operation);
} else {
staticOperations[operationName] = _OverridableOperation(
operation, _MemberName(operationName), docs);
if (operations.containsKey(operationName)) {
staticOperations[operationName]!.underscoreName();
}
}
} else {
if (operations.containsKey(operationName)) {
operations[operationName]!.update(operation);
} else {
staticOperations[operationName]?.underscoreName();
operations[operationName] = _OverridableOperation(
operation, _MemberName(operationName), docs);
}
}
break;
case 'field':
final field = member as idl.Field;
final fieldName = field.name;
if (!_shouldGenerateMember(fieldName)) break;
properties.add(_Field(_MemberName(fieldName), field.idlType,
mdnInterface?.propertyFor(fieldName, isStatic: false),
isRequired: field.required));
break;
case 'maplike':
case 'setlike':
case 'iterable':
// TODO(srujzs): Generate members for these types.
break;
default:
throw Exception('Unrecognized member type $type');
}
}
}
/// Given the [declaredInheritance] by the IDL, find the closest supertype
/// that is actually generated, and set the inheritance equal to that type.
void _setInheritance(String? declaredInheritance) {
if (declaredInheritance == null) return;
final translator = Translator.instance!;
while (declaredInheritance != null) {
if (translator.markTypeAsUsed(declaredInheritance)) {
inheritance = declaredInheritance;
break;
} else {
declaredInheritance = (translator
._typeToDeclaration[declaredInheritance] as idl.Interfacelike)
.inheritance;
}
}
}
/// Given a [memberName] and whether it [isStatic], return whether it is a
/// member that should be emitted according to the compat data.
bool _shouldGenerateMember(String memberName, {bool isStatic = false}) {
if (Translator.instance!.browserCompatData.generateAll) return true;
// Compat data only exists for interfaces and namespaces. Mixins and
// dictionaries should always generate their members.
if (type != 'interface' && type != 'namespace') return true;
final interfaceBcd =
Translator.instance!.browserCompatData.retrieveInterfaceFor(name)!;
final bcd = interfaceBcd.retrievePropertyFor(memberName,
// Compat data treats namespace members as static, but the IDL does not.
isStatic: isStatic || type == 'namespace');
final shouldGenerate = bcd?.shouldGenerate;
if (shouldGenerate != null) return shouldGenerate;
// Events can bubble up to the window, document, or other elements. In the
// case where we have no compatibility data, we assume that an event can
// bubble up to this interface and support the event handler.
if (!isStatic && BrowserCompatData.isEventHandlerSupported(memberName)) {
return true;
}
// TODO(srujzs): Sometimes compatibility data can be up or down the type
// hierarchy, so it may be worth checking supertypes and subtypes. In
// practice, it doesn't seem to make a difference in the output.
return false;
}
void update(idl.Interfacelike interfacelike) {
assert((name == interfacelike.name && type == interfacelike.type) ||
interfacelike.type == 'interface mixin');
assert(interfacelike.inheritance == null || inheritance == null,
'An interface should only be defined once.');
_setInheritance(interfacelike.inheritance);
_processMembers(interfacelike.members);
}
// Constructors with the attribute `HTMLConstructor` are intended for custom
// element behavior, and are not useful otherwise, so avoid emitting them.
// https://html.spec.whatwg.org/#html-element-constructors
bool _hasHTMLConstructorAttribute(idl.Constructor constructor) =>
constructor.extAttrs.toDart
.any((extAttr) => extAttr.name == 'HTMLConstructor');
}
class _MemberName {
final String name;
final String jsOverride;
_MemberName._(this.name, this.jsOverride);
factory _MemberName(String name, [String jsOverride = '']) {
final rename = dartRename(name);
if (rename != name && jsOverride.isEmpty) jsOverride = name;
return _MemberName._(rename, jsOverride);
}
}
class Translator {
final String packageRoot;
final String _librarySubDir;
final List<String> _cssStyleDeclarations;
final Map<String, Set<String>> _elementTagMap;
final _libraries = <String, _Library>{};
final _typeToDeclaration = <String, idl.Node>{};
final _typeToPartials = <String, List<idl.Interfacelike>>{};
final _typeToLibrary = <String, _Library>{};
final _interfacelikes = <String, _PartialInterfacelike>{};
final _includes = <String, List<String>>{};
final _usedTypes = <idl.Node>{};
late String _currentlyTranslatingUrl;
late DocProvider docProvider;
late BrowserCompatData browserCompatData;
/// Singleton so that various helper methods can access info about the AST.
static Translator? instance;
Translator(this.packageRoot, this._librarySubDir, this._cssStyleDeclarations,
this._elementTagMap,
{required bool generateAll}) {
instance = this;
docProvider = DocProvider.create();
browserCompatData = BrowserCompatData.read(generateAll: generateAll);
}
void _addOrUpdateInterfaceLike(idl.Interfacelike interfacelike) {
final name = interfacelike.name;
if (_interfacelikes.containsKey(name)) {
_interfacelikes[name]!.update(interfacelike);
} else {
_interfacelikes[name] = _PartialInterfacelike(
interfacelike,
docProvider.interfaceFor(name),
);
}
}
/// Add interfaces and namespaces so we can have a unified interface
/// representation.
///
/// Note that this is done after the initial pass on the AST. This is because
/// this step resolves unions and therefore can't be done until we record all
/// types.
///
/// This method only adds the interfaces and namespaces that the browser
/// compat data claims should be generated. It also only adds dictionaries if
/// [BrowserCompatData.generateAll] is true and are otherwise handled by
/// [markTypeAsUsed] because they don't have any compat data and are emitted
/// only if used.
void addInterfacesAndNamespaces() {
for (final library in _libraries.values) {
for (final interfacelike in library.interfacelikes) {
final name = interfacelike.name;
switch (interfacelike.type) {
case 'interface':
case 'namespace':
markTypeAsUsed(name);
break;
case 'dictionary':
if (Translator.instance!.browserCompatData.generateAll) {
markTypeAsUsed(name);
}
break;
default:
throw Exception(
'Unexpected interfacelike type ${interfacelike.type}');
}
}
}
}
/// Given an [interfacelikeName], combines its interfacelike declaration, its
/// partial interfacelikes, and any mixins it includes in that order.
///
/// Mixins are applied by applying the mixin interface first and then its
/// partial interfaces.
void _combineInterfacelikes(String interfacelikeName) {
final decl = _typeToDeclaration[interfacelikeName]! as idl.Interfacelike;
for (final interfacelike in [
decl,
...?_typeToPartials[interfacelikeName]
]) {
_addOrUpdateInterfaceLike(interfacelike);
}
final mixins = _includes[interfacelikeName];
if (mixins == null) return;
for (final mixin in mixins) {
for (final interfacelike in [
_typeToDeclaration[mixin] as idl.Interfacelike,
...?_typeToPartials[mixin]
]) {
_interfacelikes[interfacelikeName]!.update(interfacelike);
}
}
}
/// Given a [type] that corresponds to an IDL type, marks it as a used type,
/// processes the type if needed, and marks any types its declaration uses.
///
/// If the type is an interface, this function only marks it used if the
/// browser compat data says it should be.
///
/// If the type is a dictionary, this function always marks it as used.
///
/// If the type is a type that is treated like a typedef, marks the type it is
/// aliased to as used.
///
/// Returns whether the type has been or will be marked as used.
bool markTypeAsUsed(String type) {
final decl = _typeToDeclaration[type];
if (decl == null) return false;
if (_usedTypes.contains(decl)) return true;
switch (decl.type) {
case 'dictionary':
final name = (decl as idl.Interfacelike).name;
_usedTypes.add(decl);
_combineInterfacelikes(name);
return true;
case 'typedef':
_usedTypes.add(decl);
final desugaredType = _desugarTypedef(_RawType(type, false))!.type;
markTypeAsUsed(desugaredType);
return true;
case 'enum':
case 'callback interface':
case 'callback':
_usedTypes.add(decl);
return true;
case 'interface':
// Interfaces and namespaces can only be marked as used depending on
// their compat data.
final name = (decl as idl.Interfacelike).name;
if (browserCompatData.shouldGenerateInterface(name)) {
_usedTypes.add(decl);
_combineInterfacelikes(name);
return true;
}
return false;
case 'namespace':
// Browser compat data doesn't document namespaces that only contain
// constants.
// https://github.com/mdn/browser-compat-data/blob/main/docs/data-guidelines/api.md#namespaces
final namespace = decl as idl.Interfacelike;
final name = namespace.name;
if (browserCompatData.shouldGenerateInterface(name) ||
namespace.members.toDart
.every((member) => member.type == 'const')) {
_usedTypes.add(decl);
_combineInterfacelikes(name);
return true;
}
return false;
case 'interface mixin':
// Mixins should never appear as types.
default:
throw Exception(
'Unexpected node type to be marked as used: ${decl.type}');
}
}
void collect(String shortName, JSArray<idl.Node> ast) {
final libraryPath = '$_librarySubDir/${shortName.kebabToSnake}.dart';
assert(!_libraries.containsKey(libraryPath));
final library = _Library(shortName, '$packageRoot/$libraryPath');
for (var i = 0; i < ast.length; i++) {
library.add(ast[i]);
}
_libraries[libraryPath] = library;
}
code.TypeDef _typedef(String name, _RawType rawType) => code.TypeDef((b) => b
..name = name
// Any typedefs that need to be handled differently when used in a return
// type context will be handled in `_typeReference` separately.
..definition = _typeReference(rawType));
code.Method _topLevelGetter(_RawType type, String getterName) =>
code.Method((b) => b
..annotations.addAll(_jsOverride('', alwaysEmit: true))
..external = true
..returns = _typeReference(type, returnType: true)
..name = getterName
..type = code.MethodType.getter);
/// Given a raw type, convert it to the Dart type that will be emitted by the
/// translator.
///
/// If [returnType] is true, [type] is assumed to be used as a return type of
/// some member.
///
/// If [onlyEmitInteropTypes] is true, we don't convert to Dart primitives but
/// rather only emit a valid interop type. This is used for type arguments as
/// they are bound to `JSAny?`.
code.TypeReference _typeReference(_RawType type,
{bool returnType = false, bool onlyEmitInteropTypes = false}) {
var dartType = type.type;
var nullable = type.nullable;
var typeParameter = type.typeParameter;
if (onlyEmitInteropTypes) {
// [type] is already an interop type, but we need to handle two cases:
// 1. Types that we declare as typedefs. In the case where they are
// aliased to a type that we would declare as a Dart primitive, we need to
// use the JS type equivalent and not the typedef name.
// 2. Sentinels in our type aliases that aren't actually JS types.
// TODO(srujzs): Some of these typedefs definitions may end up being
// unused as they were ever only used in a generic. Should we delete them
// or do they provide value to users? If we do delete them, a good way of
// detecting if they're unused is making `_usedTypes` a ref counter.
final rawType = _desugarTypedef(type);
if (rawType != null &&
jsTypeToDartPrimitiveAliases.containsKey(rawType.type)) {
dartType = rawType.type;
nullable = rawType.nullable;
typeParameter = rawType.typeParameter;
}
dartType = switch (dartType) {
'JSInteger' => 'JSNumber',
'JSDouble' => 'JSNumber',
// When the result is `undefined`, we use `JSAny?`. We explicitly
// declare `JSUndefined` `_RawType`s to be nullable, so no need to set
// nullable.
'JSUndefined' => 'JSAny',
_ => dartType,
};
} else {
if (returnType) {
// To avoid users downcasting `num`, which works differently based on
// the platform, we return `double` if it's a double type.
// TODO(srujzs): Some of these typedefs definitions may end up being
// unused as they were ever only used in a return type. Should we delete
// them or do they provide value to users? If we do delete them, a good
// way of detecting if they're unused is making `_usedTypes` a ref
// counter.
final rawType = _desugarTypedef(type);
final underlyingType = rawType?.type ?? dartType;
if (underlyingType == 'JSDouble') dartType = 'double';
}
dartType = jsTypeToDartPrimitiveAliases[dartType] ?? dartType;
if (dartType == 'void') nullable = false;
}
final typeArguments = <code.TypeReference>[];
if (typeParameter != null &&
(dartType == 'JSArray' || dartType == 'JSPromise')) {
typeArguments
.add(_typeReference(typeParameter, onlyEmitInteropTypes: true));
}
final url = _urlForType(dartType);
return code.TypeReference((b) => b
..symbol = dartType
..isNullable = nullable
..types.addAll(typeArguments)
..url = url);
}
// Given a [dartType] that is part of a reference, returns the url that needs
// to be imported to use it, if any.
String? _urlForType(String dartType) {
// Unfortunately, `code_builder` doesn't know the url of the library we are
// emitting, so we have to remove it here to avoid importing ourselves.
var url = _typeToLibrary[dartType]?.url;
// JS types and core types don't have urls.
if (url == null) {
if (dartType.startsWith('JS')) {
url = 'dart:js_interop';
}
// Else is a core type, so no import required.
} else if (url == _currentlyTranslatingUrl) {
url = null;
} else if (p.dirname(url) == p.dirname(_currentlyTranslatingUrl)) {
url = p.basename(url);
}
return url;
}
T _overridableMember<T>(
_OverridableMember member,
T Function(List<code.Parameter> requiredParameters,
List<code.Parameter> optionalParameters)
generator) {
final requiredParameters = <code.Parameter>[];
final optionalParameters = <code.Parameter>[];
for (final rawParameter in member.parameters) {
final parameter = code.Parameter((b) => b
..name = dartRename(rawParameter.name)
..type = _typeReference(rawParameter.type));
if (rawParameter.isOptional) {
optionalParameters.add(parameter);
} else {
requiredParameters.add(parameter);
}
}
return generator(requiredParameters, optionalParameters);
}
code.Constructor _constructor(_OverridableConstructor constructor) =>
_overridableMember<code.Constructor>(
constructor,
(requiredParameters, optionalParameters) => code.Constructor((b) => b
..external = true
// TODO(srujzs): Should we generate generative or factory
// constructors? With `@staticInterop`, factories were needed, but
// extension types have no such limitation.
..factory = true
..requiredParameters.addAll(requiredParameters)
..optionalParameters.addAll(optionalParameters)));
// TODO(srujzs): We don't need constructors for many dictionaries as they're
// only ever returned from APIs instead of passed to them. However,
// determining whether they are is quite difficult and requires tracking not
// only where this type is used but where any typedefs of this type are used.
// The IDL also doesn't tell us if a dictionary needs a constructor or not, so
// for now, always emit one.
code.Constructor _objectLiteral(
String jsName, String representationFieldName) {
// Dictionaries that inherit other dictionaries should provide a constructor
// that can take in their supertypes' members as well.
final namedParameters = <code.Parameter>[];
String? dictionaryName = jsName;
while (dictionaryName != null) {
final interfacelike = _interfacelikes[dictionaryName]!;
final parameters = <code.Parameter>[];
for (final property in interfacelike.properties) {
// We currently only lower dictionaries to object literals, and
// dictionaries can only have 'field' members.
final field = property as _Field;
final isRequired = field.isRequired;
final parameter = code.Parameter((b) => b
..name = field.name.name
..type = _typeReference(field.type)
..required = isRequired
..named = true);
parameters.add(parameter);
}
// Supertype members should be first.
namedParameters.insertAll(0, parameters);
dictionaryName = interfacelike.inheritance;
}
if (namedParameters.isEmpty) {
return code.Constructor((b) => b
..initializers.add(code
.refer(representationFieldName)
.assign(code.refer('JSObject', _urlForType('JSObject')).call([]))
.code));
} else {
return code.Constructor((b) => b
..optionalParameters.addAll(namedParameters)
..external = true
// TODO(srujzs): Should we generate generative or factory constructors?
// With `@staticInterop`, factories were needed, but extension types
// have no such limitation.
..factory = true);
}
}
// Generates an `@JS` annotation if the given [jsOverride] is not empty or if
// [alwaysEmit] is true.
//
// The value of the annotation is either omitted or [jsOverride] if it isn't
// empty.
List<code.Expression> _jsOverride(String jsOverride,
{bool alwaysEmit = false}) =>
[
if (jsOverride.isNotEmpty || alwaysEmit)
code.refer('JS', 'dart:js_interop').call([
if (jsOverride.isNotEmpty) code.literalString(jsOverride),
]),
];
code.Method _operation(_OverridableOperation operation) {
final memberName = operation.name;
// The IDL may return the value that is set. Dart doesn't let us use any
// type besides `void` for `[]=`, so we ignore the return value.
final returnType = memberName.name == 'operator []='
? code.TypeReference((b) => b..symbol = 'void')
: _typeReference(operation.returnType, returnType: true);
return _overridableMember<code.Method>(
operation,
(requiredParameters, optionalParameters) => code.Method((b) => b
..annotations.addAll(_jsOverride(memberName.jsOverride))
..external = true
..static = operation.isStatic
..returns = returnType
..name = memberName.name
..docs.addAll(operation.mdnProperty?.formattedDocs ?? [])
..requiredParameters.addAll(requiredParameters)
..optionalParameters.addAll(optionalParameters)),
);
}
List<code.Method> _getterSetter({
required _MemberName memberName,
required code.Reference Function() getGetterType,
required code.Reference Function() getSetterType,
required bool isStatic,
required bool readOnly,
required MdnInterface? mdnInterface,
}) {
final name = memberName.name;
final docs =
mdnInterface?.propertyFor(name, isStatic: isStatic)?.formattedDocs ??
[];
return [
code.Method(
(b) => b
..annotations.addAll(_jsOverride(memberName.jsOverride))
..external = true
..static = isStatic
..returns = getGetterType()
..type = code.MethodType.getter
..name = name
..docs.addAll(docs),
),
if (!readOnly)
code.Method(
(b) => b
..annotations.addAll(_jsOverride(memberName.jsOverride))
..external = true
..static = isStatic
..type = code.MethodType.setter
..name = name
..requiredParameters.add(
code.Parameter(
(b) => b
..type = getSetterType()
..name = 'value',
),
),
),
];
}
List<code.Method> _attribute(
_Attribute attribute, MdnInterface? mdnInterface) {
return _getterSetter(
memberName: attribute.name,
getGetterType: () => _typeReference(attribute.type, returnType: true),
getSetterType: () => _typeReference(attribute.type),
readOnly: attribute.isReadOnly,
isStatic: attribute.isStatic,
mdnInterface: mdnInterface,
);
}
(List<code.Field>, List<code.Method>) _constant(_Constant constant) {
// If it's a value type that we can emit directly in Dart as a constant,
// emit this as a field so users can `switch` over it. Value types taken
// from: https://github.com/w3c/webidl2.js/blob/main/README.md#default-and-const-values
final body = switch (constant.valueType) {
'string' => code.literalString((constant.value as JSString).toDart),
'boolean' => code.literalBool(
(constant.value as JSString).toDart.toLowerCase() == 'true'),
'number' =>
code.literalNum(num.parse((constant.value as JSString).toDart)),
'null' => code.literalNull,
_ => null,
};
if (body != null) {
return (
[
code.Field(
(b) => b
..external = false
..static = true
..modifier = code.FieldModifier.constant
..type = _typeReference(constant.type, returnType: true)
..assignment = body.code
..name = constant.name.name,
)
],
[]
);
}
return (
[],
[
code.Method(
(b) => b
..annotations.addAll(_jsOverride(constant.name.jsOverride))
..external = true
..static = true
..returns = _typeReference(constant.type, returnType: true)
..type = code.MethodType.getter
..name = constant.name.name,
)
]
);
}
List<code.Method> _field(_Field field, MdnInterface? mdnInterface) {
return _getterSetter(
memberName: field.name,
getGetterType: () => _typeReference(field.type, returnType: true),
getSetterType: () => _typeReference(field.type),
readOnly: false,
isStatic: false,
mdnInterface: mdnInterface,
);
}
(List<code.Field>, List<code.Method>) _property(
_Property member, MdnInterface? mdnInterface) =>
switch (member) {
_Attribute() => ([], _attribute(member, mdnInterface)),
_Field() => ([], _field(member, mdnInterface)),
_Constant() => _constant(member),
};
(List<code.Field>, List<code.Method>) _properties(
List<_Property> properties, MdnInterface? mdnInterface) =>
properties.fold(([], []), (specs, property) {
final (fields, methods) = _property(property, mdnInterface);
return (specs.$1..addAll(fields), specs.$2..addAll(methods));
});
List<code.Method> _operations(List<_OverridableOperation> operations) =>
[for (final operation in operations) _operation(operation)];
List<code.Method> _cssStyleDeclarationProperties() {
return [
for (final style in _cssStyleDeclarations)
..._getterSetter(
memberName: _MemberName(style),
getGetterType: () =>
_typeReference(_RawType('JSString', false), returnType: true),
getSetterType: () => _typeReference(_RawType('JSString', false)),
isStatic: false,
readOnly: false,
mdnInterface: null,
),
];
}
// If [jsName] is an element type, creates a constructor for each tag that the
// element interface corresponds to using either `createElement` or
// `createElementNS`.
List<code.Constructor> _elementConstructors(
String jsName, String dartClassName, String representationFieldName) {
final elementConstructors = <code.Constructor>[];
final tags = _elementTagMap[jsName];
if (tags != null) {
final uri = uriForElement(jsName);
assert(tags.isNotEmpty);
final createElementMethod =
uri != null ? 'createElementNS' : 'createElement';
for (final tag in tags) {
final article = singularArticleForElement(dartClassName);
elementConstructors.add(code.Constructor((b) => b
..docs.addAll([
formatDocs(
"Creates $article [$dartClassName] using the tag '$tag'.",
80,
// Extension type members start with an indentation of 2
// chars.
2)
.join('\n')
])
// If there are multiple tags, use a named constructor.
..name = tags.length == 1 ? null : dartRename(tag)
..initializers.addAll([
code
.refer(representationFieldName)
.assign(code
.refer('document', _urlForType('Document'))
.property(createElementMethod)
.call([
// TODO(srujzs): Should we make these URIs a constant and
// refer to the constant instead? Downside is that it requires
// another manual hack to generate them.
if (uri != null) code.literalString(uri),
code.literalString(tag)
]))
.code
])));
}
}
return elementConstructors;
}
code.Extension _extension({
required _RawType type,
required List<_Property> extensionProperties,
}) {
final properties = _properties(extensionProperties, null);
return code.Extension(
(b) => b
..name = '${type.type.snakeToPascal}Extension'
..on = _typeReference(type)
..fields.addAll(properties.$1)
..methods.addAll(properties.$2),
);
}
code.ExtensionType _extensionType({
required String jsName,
required String dartClassName,
required List<idl.ExtendedAttribute> extendedAttributes,
required MdnInterface? mdnInterface,
required BCDInterfaceStatus? interfaceStatus,
required List<String> implements,
required _OverridableConstructor? constructor,
required List<_OverridableOperation> operations,
required List<_OverridableOperation> staticOperations,
required List<_Property> properties,
required bool isObjectLiteral,
}) {
final docs = mdnInterface == null ? <String>[] : mdnInterface.formattedDocs;
final jsObject = _typeReference(_RawType('JSObject', false));
const representationFieldName = '_';
final legacyNameSpace = extendedAttributes
.firstWhereOrNull(
(extendedAttribute) => extendedAttribute.name == 'LegacyNamespace',
)
?.rhs
.value;
final instancePropertyMethods = <code.Method>[];
final staticPropertyMethods = <code.Method>[];
final propertySpecs = _properties(properties, mdnInterface);
for (final property in propertySpecs.$2) {
(property.static ? staticPropertyMethods : instancePropertyMethods)
.add(property);
}
return code.ExtensionType((b) => b
..docs.addAll(docs)
..annotations.addAll(
_jsOverride(
legacyNameSpace != null
? '$legacyNameSpace.$jsName'
: (isObjectLiteral || jsName == dartClassName ? '' : jsName),
),
)
..name = dartClassName
..primaryConstructorName = '_'
..representationDeclaration = code.RepresentationDeclaration((b) => b
..name = representationFieldName
..declaredRepresentationType = jsObject)
..implements.addAll(implements
.map((interface) => _typeReference(_RawType(interface, false)))
.followedBy([jsObject]))
..constructors.addAll((isObjectLiteral
? [_objectLiteral(jsName, representationFieldName)]
: constructor != null
? [_constructor(constructor)]
: <code.Constructor>[])
.followedBy(_elementConstructors(
jsName, dartClassName, representationFieldName)))
..fields.addAll(propertySpecs.$1)
..methods.addAll(_operations(staticOperations)
.followedBy(staticPropertyMethods)
.followedBy(_operations(operations))
.followedBy(instancePropertyMethods)
.followedBy(dartClassName == 'CSSStyleDeclaration'
? _cssStyleDeclarationProperties()
: [])));
}
List<code.Spec> _interfacelike(idl.Interfacelike idlInterfacelike) {
final name = idlInterfacelike.name;
final interfacelike = _interfacelikes[name]!;
final jsName = interfacelike.name;
final type = interfacelike.type;
final isNamespace = type == 'namespace';
final isDictionary = type == 'dictionary';
final extendedAttributes = idlInterfacelike.extAttrs.toDart;
final mdnInterface = docProvider.interfaceFor(jsName);
// Namespaces have lowercase names. We also translate them to
// private classes, and make their first character uppercase in the process.
final dartClassName = isNamespace ? '\$${capitalize(jsName)}' : jsName;
final interfaceStatus = browserCompatData.retrieveInterfaceFor(name);
// We create a getter for namespaces with the expected name. We also create
// getters for a few pre-defined singleton classes.
final getterName = isNamespace ? jsName : singletons[jsName];
final operations = interfacelike.operations.values.toList();
final staticOperations = interfacelike.staticOperations.values.toList();
final properties = interfacelike.properties;
final extensionProperties = interfacelike.extensionProperties;
final implements = [
if (interfacelike.inheritance != null) interfacelike.inheritance!
];
final rawType = _RawType(dartClassName, false);
return [
if (getterName != null) _topLevelGetter(rawType, getterName),
_extensionType(
jsName: jsName,
dartClassName: dartClassName,
extendedAttributes: extendedAttributes,
mdnInterface: mdnInterface,
interfaceStatus: interfaceStatus,
implements: implements,
constructor: interfacelike.constructor,
operations: operations,
staticOperations: staticOperations,
properties: properties,
isObjectLiteral: isDictionary,
),
if (extensionProperties.isNotEmpty)
_extension(type: rawType, extensionProperties: extensionProperties)
];
}
code.Library _library(_Library library) => code.Library((b) => b
..comments.addAll([
...licenseHeader,
'',
...mozLicenseHeader,
])
// TODO(https://github.com/dart-lang/sdk/issues/56450): Remove this once
// this bug has been resolved.
..ignoreForFile.addAll([
'unintended_html_in_doc_comment',
])
..generatedByComment = generatedFileDisclaimer
// TODO(srujzs): This is to address the issue around extension type object
// literal constructors in https://github.com/dart-lang/sdk/issues/54801.
// Once this package moves to an SDK version that contains a fix for that,
// this can be removed.
..annotations.addAll(_jsOverride('', alwaysEmit: true))
..body.addAll([
for (final typedef in library.typedefs.where(_usedTypes.contains))
_typedef(typedef.name, _desugarTypedef(_RawType(typedef.name, false))!),
for (final callback in library.callbacks.where(_usedTypes.contains))
_typedef(
callback.name, _desugarTypedef(_RawType(callback.name, false))!),
for (final callbackInterface
in library.callbackInterfaces.where(_usedTypes.contains))
_typedef(callbackInterface.name,
_desugarTypedef(_RawType(callbackInterface.name, false))!),
for (final enum_ in library.enums.where(_usedTypes.contains))
_typedef(enum_.name, _desugarTypedef(_RawType(enum_.name, false))!),
for (final interfacelike
in library.interfacelikes.where(_usedTypes.contains))
..._interfacelike(interfacelike),
]));
code.Library generateRootImport(Iterable<String> files) =>
code.Library((b) => b
..comments.addAll(licenseHeader)
..directives.addAll(files.map(code.Directive.export)));
TranslationResult translate() {
// Create a root import that exports all of the other libraries.
final dartLibraries = <String, code.Library>{};
// Translate each IDL library into a Dart library.
for (var entry in _libraries.entries) {
_currentlyTranslatingUrl = entry.value.url;
final dartLibrary = _library(entry.value);
if (dartLibrary.body.isEmpty && dartLibrary.directives.isEmpty) {
print(' not generating empty library: ${entry.value.url}');
} else {
dartLibraries[entry.key] = dartLibrary;
}
}
dartLibraries['dom.dart'] = generateRootImport(dartLibraries.keys);
return dartLibraries;
}
}