[pkg:js] Erase static interop types

Closes https://github.com/dart-lang/sdk/issues/47324

Erases static interop class types in favor of `BaseJavaScriptObject`.
Types are erased everywhere except for constructors, whose invocations
are instead wrapped with a cast. Constructor type erasure is avoided
to ensure the AST is still type-valid. Factories with bodies have
static stubs added instead, and tests are added for this change.

Change-Id: I7231aaaf32d22bc6261adb672a602e8a9e6c86a9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/215948
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Reviewed-by: Riley Porter <rileyporter@google.com>
This commit is contained in:
Srujan Gaddam 2021-12-10 18:36:00 +00:00 committed by Commit Bot
parent 17b2e3a893
commit 758cb462f5
9 changed files with 381 additions and 0 deletions

View file

@ -0,0 +1,183 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:kernel/clone.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/src/replacement_visitor.dart';
import 'package:_js_interop_checks/src/js_interop.dart';
class _TypeSubstitutor extends ReplacementVisitor {
final Class _baseJavaScriptObject;
_TypeSubstitutor(this._baseJavaScriptObject);
@override
DartType? visitInterfaceType(InterfaceType type, int variance) {
if (hasStaticInteropAnnotation(type.classNode)) {
return InterfaceType(_baseJavaScriptObject, type.declaredNullability);
}
return super.visitInterfaceType(type, variance);
}
}
/// Erases usage of `@JS` classes that are annotated with `@staticInterop` in
/// favor of `BaseJavaScriptObject`.
class StaticInteropClassEraser extends Transformer {
final Class _baseJavaScriptObject;
final CloneVisitorNotMembers _cloner = CloneVisitorNotMembers();
late final _TypeSubstitutor _typeSubstitutor;
StaticInteropClassEraser(CoreTypes coreTypes)
: _baseJavaScriptObject = coreTypes.index
.getClass('dart:_interceptors', 'BaseJavaScriptObject') {
_typeSubstitutor = _TypeSubstitutor(_baseJavaScriptObject);
}
String _factoryStubName(Procedure factoryTarget) =>
'${factoryTarget.name}|staticInteropFactoryStub';
/// Either finds or creates a static method stub to replace factories with a
/// body in a static interop class.
///
/// Modifies [factoryTarget]'s enclosing class to include the new method.
Procedure _findOrCreateFactoryStub(Procedure factoryTarget) {
assert(factoryTarget.isFactory);
var factoryClass = factoryTarget.enclosingClass!;
assert(hasStaticInteropAnnotation(factoryClass));
var stubName = _factoryStubName(factoryTarget);
var stubs = factoryClass.procedures
.where((procedure) => procedure.name.text == stubName);
if (stubs.isEmpty) {
// Note that the return type of the cloned function is transformed.
var functionNode =
super.visitFunctionNode(_cloner.clone(factoryTarget.function))
as FunctionNode;
var staticMethod = Procedure(
Name(stubName), ProcedureKind.Method, functionNode,
isStatic: true, fileUri: factoryTarget.fileUri)
..parent = factoryClass;
factoryClass.procedures.add(staticMethod);
return staticMethod;
} else {
assert(stubs.length == 1);
return stubs.first;
}
}
@override
TreeNode visitConstructor(Constructor node) {
if (hasStaticInteropAnnotation(node.enclosingClass)) {
// Transform children of the constructor node excluding the return type.
var returnType = node.function.returnType;
var newConstructor = super.visitConstructor(node) as Constructor;
newConstructor.function.returnType = returnType;
return newConstructor;
}
return super.visitConstructor(node);
}
@override
TreeNode visitProcedure(Procedure node) {
// Avoid changing the return types of factories, but rather cast the type of
// the invocation.
if (node.isFactory && hasStaticInteropAnnotation(node.enclosingClass!)) {
if (node.function.body != null && !node.isRedirectingFactory) {
// Bodies of factories may undergo transformation, which may result in
// type invariants breaking. For a motivating example, consider:
//
// ```
// factory Foo.fact() => Foo.cons();
// ```
//
// The invocation of `cons` would have its type erased, but then it
// would no longer match the return type of `fact`, whose return type
// shouldn't get erased as it is a factory. Note that this is only an
// issue when the factory has a body that doesn't simply redirect.
//
// In order to circumvent this, we introduce a new static method that
// clones the factory body and has a return type of
// `BaseJavaScriptObject`. Invocations of the factory are turned into
// invocations of the static method. The original factory is still kept
// in order to make modular compilations work.
_findOrCreateFactoryStub(node);
return node;
} else {
// Transform children of the factory node excluding the return type and
// return type of the signature type.
var returnType = node.function.returnType;
var signatureReturnType = node.signatureType?.returnType;
var newProcedure = super.visitProcedure(node) as Procedure;
newProcedure.function.returnType = returnType;
var signatureType = newProcedure.signatureType;
if (signatureType != null && signatureReturnType != null) {
newProcedure.signatureType = FunctionType(
signatureType.positionalParameters,
signatureReturnType,
signatureType.declaredNullability,
namedParameters: signatureType.namedParameters,
typeParameters: signatureType.typeParameters,
requiredParameterCount: signatureType.requiredParameterCount,
typedefType: signatureType.typedefType);
}
return newProcedure;
}
}
return super.visitProcedure(node);
}
@override
TreeNode visitConstructorInvocation(ConstructorInvocation node) {
if (hasStaticInteropAnnotation(node.target.enclosingClass)) {
// Add a cast so that the result gets typed as `BaseJavaScriptObject`.
var newInvocation = super.visitConstructorInvocation(node) as Expression;
return AsExpression(
newInvocation,
InterfaceType(_baseJavaScriptObject,
node.target.function.returnType.declaredNullability))
..fileOffset = newInvocation.fileOffset;
}
return super.visitConstructorInvocation(node);
}
/// Transform static invocations that correspond only to factories of static
/// interop classes.
@override
TreeNode visitStaticInvocation(StaticInvocation node) {
var targetClass = node.target.enclosingClass;
if (node.target.isFactory &&
targetClass != null &&
hasStaticInteropAnnotation(targetClass)) {
var factoryTarget = node.target;
if (factoryTarget.function.body != null &&
!factoryTarget.isRedirectingFactory) {
// Use or create the static method that replaces this factory instead.
// Note that the static method will not have been created yet in the
// case where we visit the factory later. Also note that a cast is not
// needed since the static method already has its type erased.
var args = super.visitArguments(node.arguments) as Arguments;
return StaticInvocation(_findOrCreateFactoryStub(factoryTarget), args,
isConst: node.isConst)
..fileOffset = node.fileOffset;
} else {
// Add a cast so that the result gets typed as `BaseJavaScriptObject`.
var newInvocation = super.visitStaticInvocation(node) as Expression;
return AsExpression(
newInvocation,
InterfaceType(_baseJavaScriptObject,
node.target.function.returnType.declaredNullability))
..fileOffset = newInvocation.fileOffset;
}
}
return super.visitStaticInvocation(node);
}
@override
DartType visitDartType(DartType type) {
// Variance is not a factor in our type transformation here, so just choose
// `unrelated` as a default.
var substitutedType = type.accept1(_typeSubstitutor, Variance.unrelated);
return substitutedType != null ? substitutedType : type;
}
}

View file

@ -12,6 +12,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
show Message, LocatedMessage;
import 'package:_js_interop_checks/js_interop_checks.dart';
import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart';
import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart';
import 'package:kernel/ast.dart' as ir;
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
@ -142,6 +143,7 @@ class Dart2jsTarget extends Target {
ChangedStructureNotifier? changedStructureNotifier}) {
var nativeClasses = JsInteropChecks.getNativeClasses(component);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
var staticInteropClassEraser = StaticInteropClassEraser(coreTypes);
for (var library in libraries) {
JsInteropChecks(
coreTypes,
@ -151,6 +153,7 @@ class Dart2jsTarget extends Target {
// TODO (rileyporter): Merge js_util optimizations with other lowerings
// in the single pass in `transformations/lowering.dart`.
jsUtilOptimizer.visitLibrary(library);
staticInteropClassEraser.visitLibrary(library);
}
lowering.transformLibraries(libraries, coreTypes, hierarchy, options);
logger?.call("Lowering transformations performed");

View file

@ -8,6 +8,7 @@ import 'package:_fe_analyzer_shared/src/messages/codes.dart'
show Message, LocatedMessage;
import 'package:_js_interop_checks/js_interop_checks.dart';
import 'package:_js_interop_checks/src/transformations/js_util_optimizer.dart';
import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
@ -188,6 +189,7 @@ class DevCompilerTarget extends Target {
ChangedStructureNotifier? changedStructureNotifier}) {
_nativeClasses ??= JsInteropChecks.getNativeClasses(component);
var jsUtilOptimizer = JsUtilOptimizer(coreTypes, hierarchy);
var staticInteropClassEraser = StaticInteropClassEraser(coreTypes);
for (var library in libraries) {
_CovarianceTransformer(library).transform();
JsInteropChecks(
@ -196,6 +198,7 @@ class DevCompilerTarget extends Target {
_nativeClasses!)
.visitLibrary(library);
jsUtilOptimizer.visitLibrary(library);
staticInteropClassEraser.visitLibrary(library);
}
}

View file

@ -0,0 +1 @@
js:../../../pkg/js/lib

View file

@ -0,0 +1,10 @@
// Copyright (c) 2021, 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 'static_interop.dart';
void main() {
setUp();
var staticJs = StaticJSClass.factory();
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2021, 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.
#
# Test that modular compilation still works with static interop classes that
# contain factories.
dependencies:
main: static_interop
static_interop: js

View file

@ -0,0 +1,24 @@
// Copyright (c) 2021, 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.
@JS()
library static_interop;
import 'package:js/js.dart';
@JS()
external void eval(String code);
@JS('JSClass')
@staticInterop
class StaticJSClass {
external StaticJSClass();
factory StaticJSClass.factory() {
return StaticJSClass();
}
}
void setUp() {
eval('''function JSClass() {}''');
}

View file

@ -0,0 +1,73 @@
// Copyright (c) 2021, 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.
// Test that factory methods with bodies are stubbed correctly in static interop
// type erasure.
@JS()
library factory_stub_test;
import 'dart:_interceptors' show JavaScriptObject;
import 'package:js/js.dart';
import '../native_testing.dart';
import '../native_testing.dart' as native_testing;
NativeClass makeNativeClass() native;
@Native('NativeClass')
class NativeClass extends JavaScriptObject {
factory NativeClass() => makeNativeClass();
}
@JS('NativeClass')
@staticInterop
class StaticNativeClass {
external StaticNativeClass();
factory StaticNativeClass.redirectingFactory() = StaticNativeClass;
factory StaticNativeClass.simpleFactory() => StaticNativeClass();
factory StaticNativeClass.factoryWithParam(
StaticNativeClass staticNativeClass) =>
staticNativeClass;
// This and `StaticNativeClassCopy.nestedFactory` exist to ensure that we
// cover the case where invocations on factories are visible before their
// declarations in the AST. This will test whether we correctly create the
// stub even if we haven't visited the declaration yet.
factory StaticNativeClass.nestedFactory() {
StaticNativeClassCopy.nestedFactory();
return StaticNativeClass();
}
}
@JS('NativeClass')
@staticInterop
class StaticNativeClassCopy {
external StaticNativeClassCopy();
factory StaticNativeClassCopy.nestedFactory() {
StaticNativeClass.simpleFactory();
return StaticNativeClassCopy();
}
}
void main() {
nativeTesting();
native_testing.JS('', r'''
(function(){
function NativeClass() {}
self.NativeClass = NativeClass;
self.makeNativeClass = function(){return new NativeClass()};
self.nativeConstructor(NativeClass);
})()
''');
applyTestExtensions(['NativeClass']);
// Make the Native class live.
NativeClass();
// Invoke factories and ensure they're typed correctly through assignment.
StaticNativeClass staticNativeClass = StaticNativeClass.redirectingFactory();
staticNativeClass = StaticNativeClass.simpleFactory();
staticNativeClass = StaticNativeClass.factoryWithParam(staticNativeClass);
staticNativeClass = StaticNativeClass.nestedFactory();
}

View file

@ -0,0 +1,75 @@
// Copyright (c) 2021, 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.
// @dart = 2.7
// Test that factory methods with bodies are stubbed correctly in static interop
// type erasure.
@JS()
library factory_stub_test;
import 'dart:_interceptors' show JavaScriptObject;
import 'package:js/js.dart';
import '../native_testing.dart';
import '../native_testing.dart' as native_testing;
NativeClass makeNativeClass() native;
@Native('NativeClass')
class NativeClass extends JavaScriptObject {
factory NativeClass() => makeNativeClass();
}
@JS('NativeClass')
@staticInterop
class StaticNativeClass {
external StaticNativeClass();
factory StaticNativeClass.redirectingFactory() = StaticNativeClass;
factory StaticNativeClass.simpleFactory() => StaticNativeClass();
factory StaticNativeClass.factoryWithParam(
StaticNativeClass staticNativeClass) =>
staticNativeClass;
// This and `StaticNativeClassCopy.nestedFactory` exist to ensure that we
// cover the case where invocations on factories are visible before their
// declarations in the AST. This will test whether we correctly create the
// stub even if we haven't visited the declaration yet.
factory StaticNativeClass.nestedFactory() {
StaticNativeClassCopy.nestedFactory();
return StaticNativeClass();
}
}
@JS('NativeClass')
@staticInterop
class StaticNativeClassCopy {
external StaticNativeClassCopy();
factory StaticNativeClassCopy.nestedFactory() {
StaticNativeClass.simpleFactory();
return StaticNativeClassCopy();
}
}
void main() {
nativeTesting();
native_testing.JS('', r'''
(function(){
function NativeClass() {}
self.NativeClass = NativeClass;
self.makeNativeClass = function(){return new NativeClass()};
self.nativeConstructor(NativeClass);
})()
''');
applyTestExtensions(['NativeClass']);
// Make the Native class live.
NativeClass();
// Invoke factories and ensure they're typed correctly through assignment.
StaticNativeClass staticNativeClass = StaticNativeClass.redirectingFactory();
staticNativeClass = StaticNativeClass.simpleFactory();
staticNativeClass = StaticNativeClass.factoryWithParam(staticNativeClass);
staticNativeClass = StaticNativeClass.nestedFactory();
}