[dart:js_interop] Add literal constructors for inline classes

Adds @ObjectLiteral annotation to denote object literal constructors,
and implements it in all the backends. For dart2js, this involves
modifying the SSA and for dart2wasm, we create a one-per-shape
forwarding procedure to a specialized JS method that returns the
literal. This also modifies @anonymous semantics in dart2wasm to
be consistent with the other backends.

CoreLibraryReviewExempt: Backend-specific, just adding annotation.
Change-Id: I4d7a9ea9ed097f4f378709b40f8bd74f02e26b23
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/283922
Commit-Queue: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Joshua Litt <joshualitt@google.com>
This commit is contained in:
Srujan Gaddam 2023-02-23 01:14:39 +00:00 committed by Commit Queue
parent 4cd0ca7693
commit 1f6d4ae1a8
14 changed files with 340 additions and 85 deletions

View file

@ -318,12 +318,16 @@ class JsInteropChecks extends RecursiveVisitor {
}
// Check JS Interop positional and named parameters.
var isAnonymousFactory = _classHasAnonymousAnnotation && node.isFactory;
if (isAnonymousFactory) {
// ignore: unnecessary_null_comparison
if (node.function != null &&
node.function.positionalParameters.isNotEmpty) {
var firstPositionalParam = node.function.positionalParameters[0];
var isObjectLiteralFactory =
_classHasAnonymousAnnotation && node.isFactory ||
node.isInlineClassMember && hasObjectLiteralAnnotation(node);
if (isObjectLiteralFactory) {
var positionalParams = node.function.positionalParameters;
if (node.isInlineClassMember) {
positionalParams = positionalParams.skip(1).toList();
}
if (node.function.positionalParameters.isNotEmpty) {
var firstPositionalParam = positionalParams[0];
_diagnosticsReporter.report(
messageJsInteropAnonymousFactoryPositionalParameters,
firstPositionalParam.fileOffset,

View file

@ -40,6 +40,11 @@ bool hasJSExportAnnotation(Annotatable a) =>
bool hasNativeAnnotation(Annotatable a) =>
a.annotations.any(_isNativeAnnotation);
/// Returns true iff the node has an `@ObjectLiteral(...)` annotation from
/// `dart:js_interop`.
bool hasObjectLiteralAnnotation(Annotatable a) =>
a.annotations.any(_isObjectLiteralAnnotation);
/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
@ -100,6 +105,7 @@ String getJSExportName(Annotatable a) {
final _packageJs = Uri.parse('package:js/js.dart');
final _internalJs = Uri.parse('dart:_js_annotations');
final _jsHelper = Uri.parse('dart:_js_helper');
final _jsInterop = Uri.parse('dart:js_interop');
/// Returns true if [value] is the interop annotation whose class is
/// [annotationClassName] from `package:js` or from `dart:_js_annotations`.
@ -141,6 +147,15 @@ bool _isNativeAnnotation(Expression value) {
c.enclosingLibrary.importUri == _jsHelper;
}
/// Returns true if [value] is the `ObjectLiteral` annotation from
/// `dart:js_interop`.
bool _isObjectLiteralAnnotation(Expression value) {
var c = annotationClass(value);
return c != null &&
c.name == 'ObjectLiteral' &&
c.enclosingLibrary.importUri == _jsInterop;
}
/// Returns the class of the instance referred to by metadata annotation [node].
///
/// For example:

View file

@ -14,6 +14,7 @@ import '../js_interop.dart'
hasInternalJSInteropAnnotation,
hasJSInteropAnnotation,
hasNativeAnnotation,
hasObjectLiteralAnnotation,
hasStaticInteropAnnotation,
hasTrustTypesAnnotation;
@ -174,8 +175,9 @@ class JsUtilOptimizer extends Transformer {
if (node.isInlineClassMember) {
var kind =
_inlineExtensionIndex.getInlineDescriptor(node.reference)?.kind;
return kind == InlineClassMemberKind.Constructor ||
kind == InlineClassMemberKind.Factory;
return (kind == InlineClassMemberKind.Constructor ||
kind == InlineClassMemberKind.Factory) &&
!hasObjectLiteralAnnotation(node);
} else {
return node.kind == ProcedureKind.Factory &&
node.enclosingClass != null &&

View file

@ -265,6 +265,9 @@ class Uris {
static final Uri dart__js_annotations =
Uri(scheme: 'dart', path: '_js_annotations');
/// The URI for 'dart:js_interop'.
static final Uri dart__js_interop = Uri(scheme: 'dart', path: 'js_interop');
/// The URI for 'package:meta/dart2js.dart'.
static final Uri package_meta_dart2js =
Uri(scheme: 'package', path: 'meta/dart2js.dart');

View file

@ -18,6 +18,7 @@ class IrAnnotationData {
final Map<ir.Class, String> _jsInteropClassNames = {};
final Set<ir.Class> _anonymousJsInteropClasses = {};
final Map<ir.Member, String> _jsInteropMemberNames = {};
final Set<ir.Member> _jsInteropObjectLiterals = {};
final Map<ir.Member, List<PragmaAnnotationData>> _memberPragmaAnnotations =
{};
@ -52,6 +53,10 @@ class IrAnnotationData {
bool isAnonymousJsInteropClass(ir.Class node) =>
_anonymousJsInteropClasses.contains(node);
// Returns `true` if [node] is annotated with `@ObjectLiteral`.
bool isJsInteropObjectLiteral(ir.Member node) =>
_jsInteropObjectLiterals.contains(node);
// Returns the text from the `@JS(<text>)` annotation of [node], if any.
String? getJsInteropMemberName(ir.Member node) => _jsInteropMemberNames[node];
@ -74,11 +79,15 @@ class IrAnnotationData {
});
}
void forEachJsInteropMember(void Function(ir.Member, String?) f) {
void forEachJsInteropMember(
void Function(ir.Member, String?,
{required bool isJsInteropObjectLiteral})
f) {
_jsInteropLibraryNames.forEach((ir.Library library, _) {
for (ir.Member member in library.members) {
if (member.isExternal) {
f(member, _jsInteropMemberNames[member] ?? member.name.text);
f(member, _jsInteropMemberNames[member] ?? member.name.text,
isJsInteropObjectLiteral: isJsInteropObjectLiteral(member));
}
}
});
@ -89,7 +98,7 @@ class IrAnnotationData {
if (member.isExternal) {
name ??= member.name.text;
}
f(member, name);
f(member, name, isJsInteropObjectLiteral: false);
}
});
}
@ -142,6 +151,12 @@ IrAnnotationData processAnnotations(ModularCore modularCore) {
data._jsInteropMemberNames[member] = jsName;
}
bool isJsInteropObjectLiteralMember =
_isJsInteropObjectLiteral(constant);
if (isJsInteropObjectLiteralMember) {
data._jsInteropObjectLiterals.add(member);
}
bool isNativeMember = _isNativeMember(constant);
if (isNativeMember) {
data._nativeMembers.add(member);
@ -318,6 +333,12 @@ bool _isAnonymousJsInterop(ir.Constant constant) {
Uris.dart__js_annotations);
}
bool _isJsInteropObjectLiteral(ir.Constant constant) {
return constant is ir.InstanceConstant &&
constant.classNode.name == 'ObjectLiteral' &&
constant.classNode.enclosingLibrary.importUri == Uris.dart__js_interop;
}
class PragmaAnnotationData {
// TODO(johnniwinther): Support non 'dart2js:' pragma names if necessary.
final String suffix;

View file

@ -35,6 +35,9 @@ class NativeBasicDataBuilder {
/// The JavaScript members implemented via typed JavaScript interop.
final Map<MemberEntity, String> _jsInteropMembers = {};
/// The JavaScript interop members annotated with `@ObjectLiteral`.
final Set<MemberEntity> _jsInteropObjectLiterals = {};
/// Sets the native tag info for [cls].
///
/// The tag info string contains comma-separated 'words' which are either
@ -99,7 +102,8 @@ class NativeBasicDataBuilder {
/// Marks [element] as an explicit part of js interop and sets the explicit js
/// interop [name] for the member [element].
void markAsJsInteropMember(MemberEntity element, String name) {
void markAsJsInteropMember(MemberEntity element, String name,
{required bool isJsInteropObjectLiteral}) {
assert(
!_closed,
failedAt(
@ -107,6 +111,7 @@ class NativeBasicDataBuilder {
"NativeBasicDataBuilder is closed. "
"Trying to mark $element as a js-interop member."));
_jsInteropMembers[element] = name;
if (isJsInteropObjectLiteral) _jsInteropObjectLiterals.add(element);
}
/// Creates the [NativeBasicData] object for the data collected in this
@ -120,7 +125,8 @@ class NativeBasicDataBuilder {
_jsInteropLibraries,
_jsInteropClasses,
_anonymousJsInteropClasses,
_jsInteropMembers);
_jsInteropMembers,
_jsInteropObjectLiterals);
}
void reopenForTesting() {
@ -156,6 +162,9 @@ class NativeBasicData {
/// The JavaScript members implemented via typed JavaScript interop.
final Map<MemberEntity, String?> _jsInteropMembers;
/// JavaScript interop constructors annotated with `@ObjectLiteral`.
final Set<MemberEntity> _jsInteropObjectLiterals;
NativeBasicData(
this._env,
this._isAllowInteropUsed,
@ -163,7 +172,8 @@ class NativeBasicData {
this._jsInteropLibraries,
this._jsInteropClasses,
this._anonymousJsInteropClasses,
this._jsInteropMembers);
this._jsInteropMembers,
this._jsInteropObjectLiterals);
factory NativeBasicData.fromIr(
KernelToElementMap map, IrAnnotationData data) {
@ -173,6 +183,7 @@ class NativeBasicData {
Map<ClassEntity, String> jsInteropClasses = {};
Set<ClassEntity> anonymousJsInteropClasses = {};
Map<MemberEntity, String?> jsInteropMembers = {};
Set<MemberEntity> jsInteropObjectLiterals = {};
data.forEachNativeClass((ir.Class node, String text) {
nativeClassTagInfo[map.getClass(node)] = NativeClassTag(text);
@ -189,17 +200,27 @@ class NativeBasicData {
anonymousJsInteropClasses.add(cls);
}
});
data.forEachJsInteropMember((ir.Member node, String? name) {
data.forEachJsInteropMember((ir.Member node, String? name,
{required bool isJsInteropObjectLiteral}) {
// TODO(49428): Are there other members that we should ignore here?
// There are non-external and unannotated members because the source code
// doesn't contain them. (e.g. default constructor) Does it make sense to
// consider these valid JS members?
if (memberIsIgnorable(node)) return;
jsInteropMembers[map.getMember(node)] = name;
if (isJsInteropObjectLiteral)
jsInteropObjectLiterals.add(map.getMember(node));
});
return NativeBasicData(env, false, nativeClassTagInfo, jsInteropLibraries,
jsInteropClasses, anonymousJsInteropClasses, jsInteropMembers);
return NativeBasicData(
env,
false,
nativeClassTagInfo,
jsInteropLibraries,
jsInteropClasses,
anonymousJsInteropClasses,
jsInteropMembers,
jsInteropObjectLiterals);
}
/// Deserializes a [NativeBasicData] object from [source].
@ -220,6 +241,7 @@ class NativeBasicData {
Set<ClassEntity> anonymousJsInteropClasses = source.readClasses().toSet();
Map<MemberEntity, String?> jsInteropMembers = source
.readMemberMap((MemberEntity member) => source.readStringOrNull());
Set<MemberEntity> jsInteropObjectLiterals = source.readMembers().toSet();
source.end(tag);
return NativeBasicData(
elementEnvironment,
@ -228,7 +250,8 @@ class NativeBasicData {
jsInteropLibraries,
jsInteropClasses,
anonymousJsInteropClasses,
jsInteropMembers);
jsInteropMembers,
jsInteropObjectLiterals);
}
/// Serializes this [NativeBasicData] to [sink].
@ -244,6 +267,7 @@ class NativeBasicData {
sink.writeClasses(_anonymousJsInteropClasses);
sink.writeMemberMap(_jsInteropMembers,
(MemberEntity member, String? name) => sink.writeStringOrNull(name));
sink.writeMembers(_jsInteropObjectLiterals);
sink.end(tag);
}
@ -348,6 +372,8 @@ class NativeBasicData {
map.toBackendClassSet(_anonymousJsInteropClasses);
Map<MemberEntity, String?> jsInteropMembers =
map.toBackendMemberMap(_jsInteropMembers, identity);
Set<MemberEntity> jsInteropObjectLiterals =
map.toBackendMemberSet(_jsInteropObjectLiterals);
return NativeBasicData(
environment,
isAllowInteropUsed,
@ -355,7 +381,8 @@ class NativeBasicData {
jsInteropLibraries,
jsInteropClasses,
anonymousJsInteropClasses,
jsInteropMembers);
jsInteropMembers,
jsInteropObjectLiterals);
}
}
@ -569,11 +596,20 @@ class NativeData implements NativeBasicData {
Map<MemberEntity, String?> get _jsInteropMembers =>
_nativeBasicData._jsInteropMembers;
@override
Set<MemberEntity> get _jsInteropObjectLiterals =>
_nativeBasicData._jsInteropObjectLiterals;
/// Returns `true` if [element] has an `@Anonymous` annotation.
bool isAnonymousJsInteropClass(ClassEntity element) {
return _anonymousJsInteropClasses.contains(element);
}
/// Returns `true` if [element] has an `@ObjectLiteral` annotation.
bool isJsInteropObjectLiteral(MemberEntity element) {
return _jsInteropObjectLiterals.contains(element);
}
@override
bool isNativeClass(ClassEntity element) =>
_nativeBasicData.isNativeClass(element);

View file

@ -86,7 +86,9 @@ class KernelAnnotationProcessor {
/*reporter.reportErrorMessage(
function, MessageKind.JS_INTEROP_NON_EXTERNAL_MEMBER);*/
} else {
_nativeBasicDataBuilder.markAsJsInteropMember(function, memberName);
_nativeBasicDataBuilder.markAsJsInteropMember(function, memberName,
isJsInteropObjectLiteral:
annotationData.isJsInteropObjectLiteral(memberNode));
// TODO(johnniwinther): It is unclear whether library can be
// implicitly js-interop. For now we allow it.
isJsLibrary = true;
@ -128,7 +130,8 @@ class KernelAnnotationProcessor {
// TODO(johnniwinther): The documentation states that explicit
// member name annotations are not allowed on instance members.
_nativeBasicDataBuilder.markAsJsInteropMember(
function, memberName);
function, memberName,
isJsInteropObjectLiteral: false);
}
}
});
@ -145,7 +148,8 @@ class KernelAnnotationProcessor {
// TODO(johnniwinther): The documentation states that explicit
// member name annotations are not allowed on instance members.
_nativeBasicDataBuilder.markAsJsInteropMember(
constructor, memberName);
constructor, memberName,
isJsInteropObjectLiteral: false);
}
});
}

View file

@ -3908,11 +3908,16 @@ class KernelSsaGraphBuilder extends ir.Visitor<void> with ir.VisitorVoidMixin {
List.from(_visitPositionalArguments(arguments));
if (target.namedParameters.isNotEmpty) {
// Only anonymous factory constructors involving JS interop are allowed to
// have named parameters. Otherwise, throw an error.
// Only anonymous factory or inline class literal constructors involving
// JS interop are allowed to have named parameters. Otherwise, throw an
// error.
final function =
_elementMap.getMember(target.parent as ir.Member) as FunctionEntity;
if (function is ConstructorEntity && function.isFactoryConstructor) {
if (function is ConstructorEntity &&
function.isFactoryConstructor &&
_nativeData.isAnonymousJsInteropClass(function.enclosingClass) ||
function.isTopLevel &&
_nativeData.isJsInteropObjectLiteral(function)) {
// TODO(sra): Have a "CompiledArguments" structure to just update with
// what values we have rather than creating a map and de-populating it.
Map<String, HInstruction> namedValues = _visitNamedArguments(arguments);
@ -5419,11 +5424,11 @@ class KernelSsaGraphBuilder extends ir.Visitor<void> with ir.VisitorVoidMixin {
assert(closedWorld.nativeData.isJsInteropMember(element));
if (element is ConstructorEntity &&
element.isFactoryConstructor &&
_nativeData.isAnonymousJsInteropClass(element.enclosingClass)) {
// Factory constructor that is syntactic sugar for creating a JavaScript
// object literal.
ConstructorEntity constructor = element;
element.isFactoryConstructor &&
_nativeData.isAnonymousJsInteropClass(element.enclosingClass) ||
element.isTopLevel && _nativeData.isJsInteropObjectLiteral(element)) {
// Constructor that is syntactic sugar for creating a JavaScript object
// literal.
int i = 0;
int positions = 0;
List<HInstruction> filteredArguments = [];
@ -5435,7 +5440,7 @@ class KernelSsaGraphBuilder extends ir.Visitor<void> with ir.VisitorVoidMixin {
// TODO(johnniwinther): can we elide those parameters? This should be
// consistent with what we do with instance methods.
final node =
_elementMap.getMemberDefinition(constructor).node as ir.Procedure;
_elementMap.getMemberDefinition(element).node as ir.Procedure;
List<ir.VariableDeclaration> namedParameters =
node.function.namedParameters.toList();
namedParameters.sort(nativeOrdering);

View file

@ -7,8 +7,9 @@ import 'package:_js_interop_checks/src/js_interop.dart'
calculateTransitiveImportsOfJsInteropIfUsed,
getJSName,
hasAnonymousAnnotation,
hasStaticInteropAnnotation,
hasJSInteropAnnotation;
hasJSInteropAnnotation,
hasObjectLiteralAnnotation,
hasStaticInteropAnnotation;
import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart'
show InlineExtensionIndex;
import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart';
@ -36,32 +37,64 @@ class _MethodLoweringConfig {
final Procedure procedure;
final _MethodType type;
final String jsString;
final StaticInvocation? invocation;
final InlineExtensionIndex _inlineExtensionIndex;
late final bool isConstructor =
type == _MethodType.jsObjectLiteralConstructor ||
type == _MethodType.constructor;
late final bool firstParameterIsObject =
_inlineExtensionIndex.isInstanceInteropMember(procedure);
late final List<VariableDeclaration> parameters =
type == _MethodType.jsObjectLiteralConstructor
? function.namedParameters
: function.positionalParameters;
late String tag = procedure.name.text.replaceAll(RegExp(r'[^a-zA-Z_]'), '_');
late final List<VariableDeclaration> parameters = _parameters;
late final List<Expression> arguments = _arguments;
_MethodLoweringConfig(
this.procedure, this.type, this.jsString, this._inlineExtensionIndex);
_MethodLoweringConfig(this.procedure, this.type, this.jsString,
this.invocation, this._inlineExtensionIndex);
FunctionNode get function => procedure.function;
Uri get fileUri => procedure.fileUri;
String generateJS(List<String> parameters) {
// The parameters that determine arity (and key names in the case of object
// literals) of the interop procedure that is created from this config.
List<VariableDeclaration> get _parameters {
if (type == _MethodType.jsObjectLiteralConstructor) {
// Compute the parameters that were used in the given `invocation`. Note
// that we preserve the procedure's ordering and not the invocations.
Set<String> usedArgs =
invocation!.arguments.named.map((expr) => expr.name).toSet();
return function.namedParameters
.where((decl) => usedArgs.contains(decl.name))
.toList();
}
return function.positionalParameters;
}
// The arguments that will be passed into the interop procedure that is
// created from this config.
List<Expression> get _arguments {
if (type == _MethodType.jsObjectLiteralConstructor) {
// Return the args in the order of the procedure's parameters and not the
// invocation.
Map<String, Expression> namedArgs = {};
for (NamedExpression expr in invocation!.arguments.named) {
namedArgs[expr.name] = expr.value;
}
return parameters
.map<Expression>((decl) => namedArgs[decl.name!]!)
.toList();
}
return function.positionalParameters
.map((decl) => VariableGet(decl))
.toList();
}
String generateJS(List<String> parameterNames) {
String object = isConstructor
? ''
: firstParameterIsObject
? parameters[0]
? parameterNames[0]
: 'globalThis';
List<String> callArguments =
firstParameterIsObject ? parameters.sublist(1) : parameters;
firstParameterIsObject ? parameterNames.sublist(1) : parameterNames;
String callArgumentsString = callArguments.join(',');
String functionParameters = firstParameterIsObject
? '$object${callArguments.isEmpty ? '' : ',$callArgumentsString'}'
@ -69,11 +102,10 @@ class _MethodLoweringConfig {
String bodyString;
switch (type) {
case _MethodType.jsObjectLiteralConstructor:
List<String> keys =
function.namedParameters.map((named) => named.name!).toList();
List<String> keys = parameters.map((named) => named.name!).toList();
List<String> keyValuePairs = [];
for (int i = 0; i < parameters.length; i++) {
keyValuePairs.add('${keys[i]}: ${parameters[i]}');
for (int i = 0; i < parameterNames.length; i++) {
keyValuePairs.add('${keys[i]}: ${parameterNames[i]}');
}
bodyString = '({${keyValuePairs.join(',')}})';
break;
@ -99,7 +131,7 @@ class _MethodLoweringConfig {
}
break;
}
if (parametersNeedParens(parameters)) {
if (parametersNeedParens(parameterNames)) {
return '($functionParameters) => $bodyString';
} else {
return '$functionParameters => $bodyString';
@ -141,6 +173,7 @@ class _JSLowerer extends Transformer {
int _methodN = 1;
late Library _library;
late String _libraryJSString;
final Map<Procedure, Map<String, Procedure>> _jsObjectLiteralMethods = {};
final CoreTypes _coreTypes;
late InlineExtensionIndex _inlineExtensionIndex;
@ -222,6 +255,19 @@ class _JSLowerer extends Transformer {
return _functionToJS(node.target, functionType as FunctionType, argument);
} else if (node.target == _inlineJSTarget) {
return _expandInlineJS(node.target, node);
} else if ((node.target.isInlineClassMember &&
node.target.isExternal &&
hasObjectLiteralAnnotation(node.target)) ||
(node.target.isFactory &&
node.target.isExternal &&
hasAnonymousAnnotation(node.target.enclosingClass!))) {
assert(node.arguments.positional.isEmpty);
return _specializeJSMethod(_MethodLoweringConfig(
node.target,
_MethodType.jsObjectLiteralConstructor,
'',
node,
_inlineExtensionIndex));
}
return node;
}
@ -271,11 +317,7 @@ class _JSLowerer extends Transformer {
Class cls = node.enclosingClass!;
jsString = _getTopLevelJSString(cls, cls.name);
if (node.isFactory) {
if (hasAnonymousAnnotation(cls)) {
type = _MethodType.jsObjectLiteralConstructor;
} else {
type = _MethodType.constructor;
}
if (!hasAnonymousAnnotation(cls)) type = _MethodType.constructor;
} else {
String memberSelectorString = _getJSString(node, node.name.text);
jsString = '$jsString.$memberSelectorString';
@ -289,8 +331,9 @@ class _JSLowerer extends Transformer {
_inlineExtensionIndex.getInlineClass(node.reference)!;
InlineClassMemberKind kind = nodeDescriptor.kind;
jsString = _getTopLevelJSString(cls, cls.name);
if (kind == InlineClassMemberKind.Constructor ||
kind == InlineClassMemberKind.Factory) {
if ((kind == InlineClassMemberKind.Constructor ||
kind == InlineClassMemberKind.Factory) &&
!hasObjectLiteralAnnotation(node)) {
type = _MethodType.constructor;
} else if (nodeDescriptor.isStatic) {
String memberSelectorString =
@ -332,8 +375,11 @@ class _JSLowerer extends Transformer {
type = _getTypeForNonExtensionMember(node);
}
if (type != null) {
transformedBody = _specializeJSMethod(
_MethodLoweringConfig(node, type, jsString, _inlineExtensionIndex));
Expression invocation = _specializeJSMethod(_MethodLoweringConfig(
node, type, jsString, null, _inlineExtensionIndex));
transformedBody = node.function.returnType is VoidType
? ExpressionStatement(invocation)
: ReturnStatement(invocation);
}
}
if (transformedBody != null) {
@ -657,9 +703,9 @@ class _JSLowerer extends Transformer {
functionType:
target.function.computeFunctionType(Nullability.nonNullable));
// Specializes a JS method for a given [_MethodLoweringConfig] and returns an
// invocation of the specialized method.
Statement _specializeJSMethod(_MethodLoweringConfig config) {
/// Creates a Dart procedure that calls out to a specialized JS method for the
/// given [config] and returns the created procedure.
Procedure _getInteropProcedure(_MethodLoweringConfig config) {
// Initialize variable declarations.
List<String> jsParameterStrings = [];
List<VariableDeclaration> originalParameters = config.parameters;
@ -687,40 +733,61 @@ class _JSLowerer extends Transformer {
jsMethods[dartProcedure] =
"$jsMethodName: ${config.generateJS(jsParameterStrings)}";
return dartProcedure;
}
/// Specializes a JS method for a given [config] and returns an invocation of
/// the specialized method.
Expression _specializeJSMethod(_MethodLoweringConfig config) {
Procedure interopProcedure;
if (config.type == _MethodType.jsObjectLiteralConstructor) {
// To avoid one method for every invocation, we optimize and compute one
// method per invocation shape. For example, `Cons(a: 0, b: 0)`,
// `Cons(a: 0)`, and `Cons(a: 1, b: 1)` only create two shapes:
// `{a: value, b: value}` and `{a: value}`. Therefore, we only need two
// methods to handle the `Cons` invocations.
String shape = config.parameters
.map((VariableDeclaration decl) => decl.name)
.join('|');
_jsObjectLiteralMethods
.putIfAbsent(config.procedure, () => {})
.putIfAbsent(shape, () => _getInteropProcedure(config));
interopProcedure = _jsObjectLiteralMethods[config.procedure]![shape]!;
} else {
interopProcedure = _getInteropProcedure(config);
}
// Return the replacement body.
// Because we simply don't have enough information, we leave all JS numbers
// as doubles. However, in cases where we know the user expects an `int` we
// insert a cast. We also let static interop types flow through without
// conversion, both as arguments, and as the return type.
DartType returnType = config.function.returnType;
Expression invocation = StaticInvocation(
dartProcedure,
Arguments(originalParameters
.map<Expression>((value) => StaticInvocation(
_jsifyTarget(value.type), Arguments([VariableGet(value)])))
.toList()));
List<Expression> positionalArgs = config.arguments
.map((expr) => StaticInvocation(
_jsifyTarget(expr.getStaticType(_staticTypeContext)),
Arguments([expr])))
.toList();
Expression invocation =
StaticInvocation(interopProcedure, Arguments(positionalArgs));
DartType returnTypeOverride = returnType == _coreTypes.intNullableRawType
? _coreTypes.doubleNullableRawType
: returnType == _coreTypes.intNonNullableRawType
? _coreTypes.doubleNonNullableRawType
: returnType;
if (returnType is VoidType) {
return ExpressionStatement(invocation);
} else {
Expression expression;
if (returnType is! VoidType) {
if (_isStaticInteropType(returnType)) {
// TODO(joshualitt): Expose boxed `JSNull` and `JSUndefined` to Dart
// code after migrating existing users of js interop on Dart2Wasm.
// expression = _createJSValue(invocation);
expression = _invokeOneArg(_jsValueBoxTarget, invocation);
// invocation = _createJSValue(invocation);
invocation = _invokeOneArg(_jsValueBoxTarget, invocation);
} else {
expression = AsExpression(
invocation = AsExpression(
_convertReturnType(returnType, returnTypeOverride,
_invokeOneArg(_dartifyRawTarget, invocation)),
returnType);
}
return ReturnStatement(expression);
}
return invocation;
}
// Handles any necessary return type conversions. Today this is just for

View file

@ -6088,6 +6088,17 @@ class ProgramCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
}
}
}
if (target.isExternal &&
target.isInlineClassMember &&
hasObjectLiteralAnnotation(target)) {
// Only JS interop inline class object literal constructors have the
// `@ObjectLiteral(...)` annotation.
assert(node.arguments.positional.isEmpty);
return _emitObjectLiteral(
Arguments(node.arguments.positional,
types: node.arguments.types, named: node.arguments.named),
target);
}
if (target == _coreTypes.identicalProcedure) {
return _emitCoreIdenticalCall(node.arguments.positional);
}

View file

@ -67,7 +67,7 @@ bool isJsRestAnnotation(Expression value) =>
bool isJSAnnotation(Expression value) =>
_annotationIsFromJSLibrary('JS', value) || isJSName(value);
/// Returns [true] if [e] is the `JS` annotation from `package:js`.
/// Returns [true] if [value] is the `JS` annotation from `package:js`.
bool isPublicJSAnnotation(Expression value) =>
_annotationIsFromJSLibrary('JS', value);
@ -113,6 +113,18 @@ bool isStaticInteropType(Class namedClass) {
bool isUndefinedAnnotation(Expression value) =>
isBuiltinAnnotation(value, '_js_helper', '_Undefined');
bool isObjectLiteralAnnotation(Expression value) {
var c = getAnnotationClass(value);
return c != null &&
c.name == 'ObjectLiteral' &&
_isLibrary(c.enclosingLibrary, ['dart:js_interop']);
}
/// Returns whether [a] is annotated with the `@ObjectLiteral(...)` annotation
/// from `dart:js_interop`.
bool hasObjectLiteralAnnotation(Annotatable a) =>
a.annotations.any(isObjectLiteralAnnotation);
/// Returns true iff the class has an `@JS(...)` annotation from `package:js`.
///
/// Note: usually [_usesJSInterop] should be used instead of this.

View file

@ -23,6 +23,21 @@ import 'dart:typed_data';
/// `package:js` classes.
export 'dart:_js_annotations' show JS;
/// The annotation for object literal constructors.
///
/// Use this on an external constructor for an interop inline class that only
/// has named args in order to create object literals performantly. The
/// resulting object literal will use the parameter names as keys and the
/// provided arguments as the values.
///
/// Note that object literal constructors ignore the default values of
/// parameters and only include the arguments you provide in the invocation of
/// the constructor. This is similar to the `@anonymous` annotation in
/// `package:js`.
class ObjectLiteral {
const ObjectLiteral();
}
/// The overall top type in the JS types hierarchy.
typedef JSAny = js_types.JSAny;

View file

@ -0,0 +1,59 @@
// 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.
// SharedOptions=--enable-experiment=inline-class
@JS()
library object_literal_constructor_test;
import 'dart:js_interop';
import 'dart:js_util';
import 'package:expect/minitest.dart';
@JS()
inline class Literal {
final JSObject obj;
@ObjectLiteral()
external Literal({double? a, String b = 'unused', bool? c = null});
}
@JS('Object.keys')
external JSArray objectKeys(Literal literal);
void main() {
// Test that the properties we assumed to exist in `literal` actually exist
// and that their values are as expected. Note that we don't check the order
// of the keys in the literal. This is not guaranteed to be the same across
// different backends.
void testProperties(Literal literal, {double? a, String? b, bool? c}) {
if (a != null) {
expect(hasProperty(literal, 'a'), true);
expect(getProperty<double>(literal, 'a'), a);
}
if (b != null) {
expect(hasProperty(literal, 'b'), true);
expect(getProperty<String>(literal, 'b'), b);
}
if (c != null) {
expect(hasProperty(literal, 'c'), true);
expect(getProperty<bool>(literal, 'c'), c);
}
}
testProperties(Literal());
testProperties(Literal(a: 0.0), a: 0.0);
testProperties(Literal(b: ''), b: '');
testProperties(Literal(c: true), c: true);
testProperties(Literal(a: 0.0, b: ''), a: 0.0, b: '');
testProperties(Literal(a: 0.0, c: true), a: 0.0, c: true);
testProperties(Literal(b: '', c: true), b: '', c: true);
testProperties(Literal(a: 0.0, b: '', c: true), a: 0.0, b: '', c: true);
// Re-run with the same shape for dart2wasm optimization check.
testProperties(Literal(a: 0.0, b: '', c: true), a: 0.0, b: '', c: true);
// Test that passing in a different order doesn't change the values.
testProperties(Literal(c: true, a: 0.0, b: ''), a: 0.0, b: '', c: true);
}

View file

@ -307,23 +307,24 @@ class AnonymousJSClass {
extension AnonymousJSClassExtension on AnonymousJSClass {
external String? get foo;
external String get bar;
external String? get bar;
external String? get bleep;
external int? get goo;
external int get ooo;
external int? get ooo;
external List<double>? saz;
external List<double> zoo;
external List<double>? zoo;
}
void anonymousTest() {
final anonymousJSClass = AnonymousJSClass.factory(foo: 'boo');
final anonymousJSClass = AnonymousJSClass.factory(
foo: 'boo', bleep: 'bleep', saz: const [1.0, 2.0], goo: 0);
Expect.equals('boo', anonymousJSClass.foo);
Expect.equals('baz', anonymousJSClass.bar);
Expect.equals(null, anonymousJSClass.bleep);
Expect.equals(null, anonymousJSClass.goo);
Expect.equals(1, anonymousJSClass.ooo);
Expect.equals(null, anonymousJSClass.saz);
Expect.listEquals(const [1.0, 2.0], anonymousJSClass.zoo);
Expect.equals(null, anonymousJSClass.bar);
Expect.equals('bleep', anonymousJSClass.bleep);
Expect.equals(0, anonymousJSClass.goo);
Expect.equals(null, anonymousJSClass.ooo);
Expect.listEquals(const [1.0, 2.0], anonymousJSClass.saz!);
Expect.equals(null, anonymousJSClass.zoo);
}
@JS()