|
| 1 | +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:kernel/ast.dart'; |
| 6 | +import 'package:kernel/clone.dart'; |
| 7 | +import 'package:kernel/core_types.dart'; |
| 8 | +import 'package:kernel/kernel.dart'; |
| 9 | +import 'package:kernel/src/replacement_visitor.dart'; |
| 10 | +import 'package:_js_interop_checks/src/js_interop.dart'; |
| 11 | + |
| 12 | +class _TypeSubstitutor extends ReplacementVisitor { |
| 13 | + final Class _javaScriptObject; |
| 14 | + _TypeSubstitutor(this._javaScriptObject); |
| 15 | + |
| 16 | + @override |
| 17 | + DartType? visitInterfaceType(InterfaceType type, int variance) { |
| 18 | + if (hasStaticInteropAnnotation(type.classNode)) { |
| 19 | + return InterfaceType(_javaScriptObject, type.declaredNullability); |
| 20 | + } |
| 21 | + return super.visitInterfaceType(type, variance); |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +/// Erases usage of `@JS` classes that are annotated with `@staticInterop` in |
| 26 | +/// favor of `JavaScriptObject`. |
| 27 | +class StaticInteropClassEraser extends Transformer { |
| 28 | + final Class _javaScriptObject; |
| 29 | + final CloneVisitorNotMembers _cloner = CloneVisitorNotMembers(); |
| 30 | + late final _TypeSubstitutor _typeSubstitutor; |
| 31 | + |
| 32 | + StaticInteropClassEraser(CoreTypes coreTypes) |
| 33 | + : _javaScriptObject = |
| 34 | + coreTypes.index.getClass('dart:_interceptors', 'JavaScriptObject') { |
| 35 | + _typeSubstitutor = _TypeSubstitutor(_javaScriptObject); |
| 36 | + } |
| 37 | + |
| 38 | + String _factoryStubName(Procedure factoryTarget) => |
| 39 | + '${factoryTarget.name}|staticInteropFactoryStub'; |
| 40 | + |
| 41 | + /// Either finds or creates a static method stub to replace factories with a |
| 42 | + /// body in a static interop class. |
| 43 | + /// |
| 44 | + /// Modifies [factoryTarget]'s enclosing class to include the new method. |
| 45 | + Procedure _findOrCreateFactoryStub(Procedure factoryTarget) { |
| 46 | + assert(factoryTarget.isFactory); |
| 47 | + var factoryClass = factoryTarget.enclosingClass!; |
| 48 | + assert(hasStaticInteropAnnotation(factoryClass)); |
| 49 | + var stubName = _factoryStubName(factoryTarget); |
| 50 | + var stubs = factoryClass.procedures |
| 51 | + .where((procedure) => procedure.name.text == stubName); |
| 52 | + if (stubs.isEmpty) { |
| 53 | + // Note that the return type of the cloned function is transformed. |
| 54 | + var functionNode = |
| 55 | + super.visitFunctionNode(_cloner.clone(factoryTarget.function)) |
| 56 | + as FunctionNode; |
| 57 | + var staticMethod = Procedure( |
| 58 | + Name(stubName), ProcedureKind.Method, functionNode, |
| 59 | + isStatic: true, fileUri: factoryTarget.fileUri) |
| 60 | + ..parent = factoryClass; |
| 61 | + factoryClass.procedures.add(staticMethod); |
| 62 | + return staticMethod; |
| 63 | + } else { |
| 64 | + assert(stubs.length == 1); |
| 65 | + return stubs.first; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + @override |
| 70 | + TreeNode visitConstructor(Constructor node) { |
| 71 | + if (hasStaticInteropAnnotation(node.enclosingClass)) { |
| 72 | + // Transform children of the constructor node excluding the return type. |
| 73 | + var returnType = node.function.returnType; |
| 74 | + var newConstructor = super.visitConstructor(node) as Constructor; |
| 75 | + newConstructor.function.returnType = returnType; |
| 76 | + return newConstructor; |
| 77 | + } |
| 78 | + return super.visitConstructor(node); |
| 79 | + } |
| 80 | + |
| 81 | + @override |
| 82 | + TreeNode visitProcedure(Procedure node) { |
| 83 | + // Avoid changing the return types of factories, but rather cast the type of |
| 84 | + // the invocation. |
| 85 | + if (node.isFactory && hasStaticInteropAnnotation(node.enclosingClass!)) { |
| 86 | + if (node.function.body != null && !node.isRedirectingFactory) { |
| 87 | + // Bodies of factories may undergo transformation, which may result in |
| 88 | + // type invariants breaking. For a motivating example, consider: |
| 89 | + // |
| 90 | + // ``` |
| 91 | + // factory Foo.fact() => Foo.cons(); |
| 92 | + // ``` |
| 93 | + // |
| 94 | + // The invocation of `cons` would have its type erased, but then it |
| 95 | + // would no longer match the return type of `fact`, whose return type |
| 96 | + // shouldn't get erased as it is a factory. Note that this is only an |
| 97 | + // issue when the factory has a body that doesn't simply redirect. |
| 98 | + // |
| 99 | + // In order to circumvent this, we introduce a new static method that |
| 100 | + // clones the factory body and has a return type of |
| 101 | + // `JavaScriptObject`. Invocations of the factory are turned into |
| 102 | + // invocations of the static method. The original factory is still kept |
| 103 | + // in order to make modular compilations work. |
| 104 | + _findOrCreateFactoryStub(node); |
| 105 | + return node; |
| 106 | + } else { |
| 107 | + // Transform children of the factory node excluding the return type and |
| 108 | + // return type of the signature type. |
| 109 | + var returnType = node.function.returnType; |
| 110 | + var signatureReturnType = node.signatureType?.returnType; |
| 111 | + var newProcedure = super.visitProcedure(node) as Procedure; |
| 112 | + newProcedure.function.returnType = returnType; |
| 113 | + var signatureType = newProcedure.signatureType; |
| 114 | + if (signatureType != null && signatureReturnType != null) { |
| 115 | + newProcedure.signatureType = FunctionType( |
| 116 | + signatureType.positionalParameters, |
| 117 | + signatureReturnType, |
| 118 | + signatureType.declaredNullability, |
| 119 | + namedParameters: signatureType.namedParameters, |
| 120 | + typeParameters: signatureType.typeParameters, |
| 121 | + requiredParameterCount: signatureType.requiredParameterCount, |
| 122 | + typedefType: signatureType.typedefType); |
| 123 | + } |
| 124 | + return newProcedure; |
| 125 | + } |
| 126 | + } |
| 127 | + return super.visitProcedure(node); |
| 128 | + } |
| 129 | + |
| 130 | + @override |
| 131 | + TreeNode visitConstructorInvocation(ConstructorInvocation node) { |
| 132 | + if (hasStaticInteropAnnotation(node.target.enclosingClass)) { |
| 133 | + // Add a cast so that the result gets typed as `JavaScriptObject`. |
| 134 | + var newInvocation = super.visitConstructorInvocation(node) as Expression; |
| 135 | + return AsExpression( |
| 136 | + newInvocation, |
| 137 | + InterfaceType(_javaScriptObject, |
| 138 | + node.target.function.returnType.declaredNullability)) |
| 139 | + ..fileOffset = newInvocation.fileOffset; |
| 140 | + } |
| 141 | + return super.visitConstructorInvocation(node); |
| 142 | + } |
| 143 | + |
| 144 | + /// Transform static invocations that correspond only to factories of static |
| 145 | + /// interop classes. |
| 146 | + @override |
| 147 | + TreeNode visitStaticInvocation(StaticInvocation node) { |
| 148 | + var targetClass = node.target.enclosingClass; |
| 149 | + if (node.target.isFactory && |
| 150 | + targetClass != null && |
| 151 | + hasStaticInteropAnnotation(targetClass)) { |
| 152 | + var factoryTarget = node.target; |
| 153 | + if (factoryTarget.function.body != null && |
| 154 | + !factoryTarget.isRedirectingFactory) { |
| 155 | + // Use or create the static method that replaces this factory instead. |
| 156 | + // Note that the static method will not have been created yet in the |
| 157 | + // case where we visit the factory later. Also note that a cast is not |
| 158 | + // needed since the static method already has its type erased. |
| 159 | + var args = super.visitArguments(node.arguments) as Arguments; |
| 160 | + return StaticInvocation(_findOrCreateFactoryStub(factoryTarget), args, |
| 161 | + isConst: node.isConst) |
| 162 | + ..fileOffset = node.fileOffset; |
| 163 | + } else { |
| 164 | + // Add a cast so that the result gets typed as `JavaScriptObject`. |
| 165 | + var newInvocation = super.visitStaticInvocation(node) as Expression; |
| 166 | + return AsExpression( |
| 167 | + newInvocation, |
| 168 | + InterfaceType(_javaScriptObject, |
| 169 | + node.target.function.returnType.declaredNullability)) |
| 170 | + ..fileOffset = newInvocation.fileOffset; |
| 171 | + } |
| 172 | + } |
| 173 | + return super.visitStaticInvocation(node); |
| 174 | + } |
| 175 | + |
| 176 | + @override |
| 177 | + DartType visitDartType(DartType type) { |
| 178 | + // Variance is not a factor in our type transformation here, so just choose |
| 179 | + // `unrelated` as a default. |
| 180 | + var substitutedType = type.accept1(_typeSubstitutor, Variance.unrelated); |
| 181 | + return substitutedType != null ? substitutedType : type; |
| 182 | + } |
| 183 | +} |
0 commit comments