blob: d30d88b72a542ec1fbe8c2cfe14db331f97a364e [file]
// Copyright (c) 2026, 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 'banned_names.dart';
import 'bcd.dart';
import 'doc_provider.dart';
import 'js/webidl_api.dart' as idl;
import 'translator.dart';
import 'type_aliases.dart';
import 'type_union.dart';
import 'util.dart';
/// 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,
'callback' ||
'callback interface' => RawType('JSFunction', rawType.nullable),
'enum' => RawType('JSString', rawType.nullable),
_ => 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;
RawType? computeTypeParamUnion(RawType? typeParam1, RawType? typeParam2) =>
typeParam1 != null && typeParam2 != null
? _computeRawTypeUnion(typeParam1, typeParam2)
: null;
if (type1 == type2) {
return RawType(
type1,
nullable1 || nullable2,
computeTypeParamUnion(typeParam1, typeParam2),
);
}
if (type1 == 'JSUndefined') return RawType(type2, true, typeParam2);
if (type2 == 'JSUndefined') return RawType(type1, true, typeParam1);
if (type1 == 'JSInteger' || type1 == 'JSDouble') rawType1.type = 'JSNumber';
if (type2 == 'JSInteger' || type2 == 'JSDouble') rawType2.type = 'JSNumber';
final unionableType1 = _getJSTypeEquivalent(rawType1) ?? rawType1;
final unionableType2 = _getJSTypeEquivalent(rawType2) ?? rawType2;
return RawType(
computeJsTypeUnion(unionableType1.type, unionableType2.type) ?? 'JSAny',
unionableType1.nullable || unionableType2.nullable,
computeTypeParamUnion(
unionableType1.typeParameter,
unionableType2.typeParameter,
),
);
}
/// 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) {
return _getJSTypeEquivalent(desugaredType) ?? desugaredType;
}
throw Exception('Unhandled type $type with node type: $nodeType');
}
}
return null;
}
/// Returns a [RawType] for the given [idl.IDLType].
RawType _getRawType(idl.IDLType idlType) {
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;
}
if (type == 'WindowProxy') type = 'Window';
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)) {
type = _getJSTypeEquivalent(RawType(type, false))!.type;
}
return RawType(alias ?? type, nullable, typeParameter);
}
class Attribute extends Property {
final bool isStatic;
final bool isReadOnly;
Attribute(
super.name,
super.idlType,
super.mdnProperty, {
required this.isStatic,
required this.isReadOnly,
});
}
class Constant extends Property {
final String valueType;
final JSAny value;
Constant(super.name, super.idlType, this.valueType, this.value);
}
class Field extends Property {
final bool isRequired;
Field(
super.name,
super.idlType,
super.mdnProperty, {
required this.isRequired,
});
}
class MemberName {
final String name;
final String jsOverride;
factory MemberName(String name, [String jsOverride = '']) {
final rename = dartRename(name);
if (rename != name && jsOverride.isEmpty) jsOverride = name;
return MemberName._(rename, jsOverride);
}
MemberName._(this.name, this.jsOverride);
}
class OverridableConstructor extends OverridableMember {
OverridableConstructor(idl.Constructor constructor)
: super(constructor.arguments);
void update(idl.Constructor that) => _processParameters(that.arguments);
}
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) {
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) {
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();
factory OverridableOperation(
idl.Operation operation,
MemberName memberName,
MdnProperty? mdnProperty,
) => OverridableOperation._(
memberName,
operation.special,
_getRawType(operation.idlType),
mdnProperty,
operation.arguments,
);
OverridableOperation._(
this._name,
this.special,
this.returnType,
this.mdnProperty,
super.parameters,
);
bool get isStatic => special == 'static';
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);
}
MemberName _generateName() {
_finalized = true;
final dartName = _name.name;
if (dartName == returnType.type ||
parameters.any((parameter) => dartName == parameter.type.type)) {
underscoreName();
}
return _name;
}
}
class Parameter {
final Set<String> _names;
final RawType type;
bool isOptional;
bool isVariadic;
late final String name = _generateName();
factory Parameter(idl.Argument argument) => Parameter._(
{argument.name},
_getRawType(argument.idlType),
argument.optional,
argument.variadic,
);
Parameter._(this._names, this.type, this.isOptional, this.isVariadic);
void update(idl.Argument argument) {
final thatName = argument.name;
_names.add(thatName);
type.update(argument.idlType);
if (argument.optional) {
isOptional = true;
}
if (argument.variadic) {
isVariadic = true;
}
}
String _generateName() {
final namesList = _names.toList();
namesList.sort();
return namesList
.sublist(0, 1)
.followedBy(namesList.sublist(1).map(capitalize))
.join('Or');
}
}
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;
factory PartialInterfacelike(
idl.Interfacelike interfacelike,
MdnInterface? mdnInterface,
) {
final partialInterfacelike = PartialInterfacelike._(
interfacelike.name,
interfacelike.type,
interfacelike.inheritance,
mdnInterface,
);
partialInterfacelike._processMembers(interfacelike.members);
return partialInterfacelike;
}
PartialInterfacelike._(
this.name,
this.type,
String? inheritance,
this.mdnInterface,
) {
_setInheritance(inheritance);
}
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);
}
bool _hasHTMLConstructorAttribute(idl.Constructor constructor) => constructor
.extAttrs
.toDart
.any((extAttr) => extAttr.name == 'HTMLConstructor');
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;
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;
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;
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:
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;
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':
case 'async_iterable':
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;
if (type != 'interface' && type != 'namespace') return true;
final interfaceBcd = Translator.instance!.browserCompatData
.retrieveInterfaceFor(name)!;
final bcd = interfaceBcd.retrievePropertyFor(
memberName,
isStatic: isStatic || type == 'namespace',
);
final shouldGenerate = bcd?.shouldGenerate;
if (shouldGenerate != null) return shouldGenerate;
if (!isStatic && BrowserCompatData.isEventHandlerSupported(memberName)) {
return true;
}
return false;
}
}
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) {
final dartName = name.name;
final jsName = name.jsOverride.isEmpty ? dartName : name.jsOverride;
this.name = dartName == type.type
? MemberName('${dartName}_', jsName)
: name;
}
}
/// 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]) {
if (type == 'JSUndefined') nullable = true;
}
@override
String toString() =>
'RawType(type: $type, nullable: $nullable, '
'typeParameter: $typeParameter)';
void update(idl.IDLType idlType) {
final union = _computeRawTypeUnion(this, _getRawType(idlType));
type = union.type;
nullable = union.nullable;
typeParameter = union.typeParameter;
}
}