mirror of
https://github.com/dart-lang/sdk
synced 2024-10-14 13:08:10 +00:00
[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:
parent
4cd0ca7693
commit
1f6d4ae1a8
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue