JS: when translating content of js() function, rename all free references if they point to Kotlin local variable with the same name. See KT-15212
This commit is contained in:
committed by
Alexey Andreev
parent
5ee61702ee
commit
22ff7c6c45
@@ -16,9 +16,14 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.js.resolve
|
package org.jetbrains.kotlin.js.resolve
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||||
|
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||||
|
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
|
||||||
import org.jetbrains.kotlin.serialization.js.ModuleKind
|
import org.jetbrains.kotlin.serialization.js.ModuleKind
|
||||||
import org.jetbrains.kotlin.util.slicedMap.BasicWritableSlice
|
import org.jetbrains.kotlin.util.slicedMap.BasicWritableSlice
|
||||||
import org.jetbrains.kotlin.util.slicedMap.RewritePolicy
|
import org.jetbrains.kotlin.util.slicedMap.RewritePolicy
|
||||||
|
|
||||||
@JvmField val MODULE_KIND = BasicWritableSlice<ModuleDescriptor, ModuleKind>(RewritePolicy.DO_NOTHING)
|
@JvmField val MODULE_KIND = BasicWritableSlice<ModuleDescriptor, ModuleKind>(RewritePolicy.DO_NOTHING)
|
||||||
|
|
||||||
|
@JvmField val LEXICAL_SCOPE_FOR_JS = BasicWritableSlice<ResolvedCall<out FunctionDescriptor>, LexicalScope>(RewritePolicy.DO_NOTHING)
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ import com.google.gwt.dev.js.rhino.Utils.isEndOfLine
|
|||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||||
|
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
|
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
|
||||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1
|
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1
|
||||||
import org.jetbrains.kotlin.js.parser.parse
|
import org.jetbrains.kotlin.js.parser.parse
|
||||||
import org.jetbrains.kotlin.js.patterns.DescriptorPredicate
|
import org.jetbrains.kotlin.js.patterns.DescriptorPredicate
|
||||||
import org.jetbrains.kotlin.js.patterns.PatternBuilder
|
import org.jetbrains.kotlin.js.patterns.PatternBuilder
|
||||||
|
import org.jetbrains.kotlin.js.resolve.LEXICAL_SCOPE_FOR_JS
|
||||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||||
import org.jetbrains.kotlin.psi.KtExpression
|
import org.jetbrains.kotlin.psi.KtExpression
|
||||||
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
|
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
|
||||||
@@ -96,6 +98,9 @@ class JsCallChecker(
|
|||||||
} catch (e: AbortParsingException) {
|
} catch (e: AbortParsingException) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
context.trace.record(LEXICAL_SCOPE_FOR_JS, resolvedCall as ResolvedCall<FunctionDescriptor>, context.scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,12 @@ package org.jetbrains.kotlin.js.inline.util
|
|||||||
|
|
||||||
import com.google.dart.compiler.backend.js.ast.JsExpression
|
import com.google.dart.compiler.backend.js.ast.JsExpression
|
||||||
import com.google.dart.compiler.backend.js.ast.JsName
|
import com.google.dart.compiler.backend.js.ast.JsName
|
||||||
import com.google.dart.compiler.backend.js.ast.JsNameRef
|
|
||||||
import com.google.dart.compiler.backend.js.ast.JsNode
|
import com.google.dart.compiler.backend.js.ast.JsNode
|
||||||
import org.jetbrains.kotlin.js.inline.util.rewriters.NameReplacingVisitor
|
import org.jetbrains.kotlin.js.inline.util.rewriters.NameReplacingVisitor
|
||||||
import org.jetbrains.kotlin.js.inline.util.rewriters.ReturnReplacingVisitor
|
|
||||||
import org.jetbrains.kotlin.js.inline.util.rewriters.ThisReplacingVisitor
|
import org.jetbrains.kotlin.js.inline.util.rewriters.ThisReplacingVisitor
|
||||||
|
|
||||||
import java.util.IdentityHashMap
|
fun <T : JsNode> replaceNames(node: T, replaceMap: Map<JsName, JsExpression>): T {
|
||||||
|
return if (replaceMap.isEmpty()) node else NameReplacingVisitor(replaceMap).accept(node)!!
|
||||||
fun <T : JsNode> replaceNames(node: T, replaceMap: IdentityHashMap<JsName, JsExpression>): T {
|
|
||||||
return NameReplacingVisitor(replaceMap).accept(node)!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : JsNode> replaceThisReference(node: T, replacement: JsExpression) {
|
fun <T : JsNode> replaceThisReference(node: T, replacement: JsExpression) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public external fun js(code: String): dynamic = noImpl
|
|||||||
/**
|
/**
|
||||||
* Function corresponding to JavaScript's `typeof` operator
|
* Function corresponding to JavaScript's `typeof` operator
|
||||||
*/
|
*/
|
||||||
public inline fun jsTypeOf(a: Any?): String = js("Kotlin").jsTypeOf(a)
|
public inline fun jsTypeOf(a: Any?): String = js("typeof a")
|
||||||
|
|
||||||
@library
|
@library
|
||||||
internal fun deleteProperty(`object`: Any, property: Any): Unit = noImpl
|
internal fun deleteProperty(`object`: Any, property: Any): Unit = noImpl
|
||||||
@@ -3806,6 +3806,18 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
|
|||||||
doTest(fileName);
|
doTest(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("jsCode.kt")
|
||||||
|
public void testJsCode() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inline/jsCode.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("jsCodeVarDeclared.kt")
|
||||||
|
public void testJsCodeVarDeclared() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inline/jsCodeVarDeclared.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("lambdaInLambda.kt")
|
@TestMetadata("lambdaInLambda.kt")
|
||||||
public void testLambdaInLambda() throws Exception {
|
public void testLambdaInLambda() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inline/lambdaInLambda.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/inline/lambdaInLambda.kt");
|
||||||
@@ -4955,6 +4967,12 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
|
|||||||
doTest(fileName);
|
doTest(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("referenceToKotlin.kt")
|
||||||
|
public void testReferenceToKotlin() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/jsCode/referenceToKotlin.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("switch.kt")
|
@TestMetadata("switch.kt")
|
||||||
public void testSwitch() throws Exception {
|
public void testSwitch() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/jsCode/switch.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/jsCode/switch.kt");
|
||||||
@@ -5681,12 +5699,6 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
|
|||||||
doTest(fileName);
|
doTest(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestMetadata("jsTypeOf.kt")
|
|
||||||
public void testJsTypeOf() throws Exception {
|
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/jsTypeOf.kt");
|
|
||||||
doTest(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TestMetadata("kt2209.kt")
|
@TestMetadata("kt2209.kt")
|
||||||
public void testKt2209() throws Exception {
|
public void testKt2209() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/kt2209.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/native/kt2209.kt");
|
||||||
|
|||||||
+53
-7
@@ -22,18 +22,24 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys;
|
import org.jetbrains.kotlin.config.CommonConfigurationKeys;
|
||||||
import org.jetbrains.kotlin.descriptors.*;
|
import org.jetbrains.kotlin.descriptors.*;
|
||||||
|
import org.jetbrains.kotlin.incremental.components.NoLookupLocation;
|
||||||
|
import org.jetbrains.kotlin.js.inline.util.CollectUtilsKt;
|
||||||
|
import org.jetbrains.kotlin.js.inline.util.RewriteUtilsKt;
|
||||||
import org.jetbrains.kotlin.js.parser.ParserUtilsKt;
|
import org.jetbrains.kotlin.js.parser.ParserUtilsKt;
|
||||||
|
import org.jetbrains.kotlin.js.resolve.BindingContextSlicesJsKt;
|
||||||
import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker;
|
import org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker;
|
||||||
import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator;
|
import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator;
|
||||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
||||||
|
import org.jetbrains.kotlin.name.Name;
|
||||||
import org.jetbrains.kotlin.psi.KtCallExpression;
|
import org.jetbrains.kotlin.psi.KtCallExpression;
|
||||||
import org.jetbrains.kotlin.psi.KtExpression;
|
import org.jetbrains.kotlin.psi.KtExpression;
|
||||||
import org.jetbrains.kotlin.psi.ValueArgument;
|
import org.jetbrains.kotlin.psi.ValueArgument;
|
||||||
import org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilKt;
|
import org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilKt;
|
||||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
||||||
import org.jetbrains.kotlin.resolve.inline.InlineUtil;
|
import org.jetbrains.kotlin.resolve.inline.InlineUtil;
|
||||||
|
import org.jetbrains.kotlin.resolve.scopes.LexicalScope;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
|
|
||||||
import static org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker.isJsCall;
|
import static org.jetbrains.kotlin.js.resolve.diagnostics.JsCallChecker.isJsCall;
|
||||||
import static org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator.getConstant;
|
import static org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator.getConstant;
|
||||||
@@ -97,18 +103,58 @@ public final class CallExpressionTranslator extends AbstractCallExpressionTransl
|
|||||||
List<JsStatement> statements = parseJsCode(argumentExpression);
|
List<JsStatement> statements = parseJsCode(argumentExpression);
|
||||||
int size = statements.size();
|
int size = statements.size();
|
||||||
|
|
||||||
|
JsNode node;
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
return JsLiteral.NULL;
|
node = JsLiteral.NULL;
|
||||||
} else if (size > 1) {
|
}
|
||||||
return new JsBlock(statements);
|
else if (size > 1) {
|
||||||
} else {
|
node = new JsBlock(statements);
|
||||||
|
}
|
||||||
|
else {
|
||||||
JsStatement resultStatement = statements.get(0);
|
JsStatement resultStatement = statements.get(0);
|
||||||
if (resultStatement instanceof JsExpressionStatement) {
|
if (resultStatement instanceof JsExpressionStatement) {
|
||||||
return ((JsExpressionStatement) resultStatement).getExpression();
|
node = ((JsExpressionStatement) resultStatement).getExpression();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
node = resultStatement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LexicalScope lexicalScope = context().bindingContext().get(BindingContextSlicesJsKt.LEXICAL_SCOPE_FOR_JS, resolvedCall);
|
||||||
|
Map<JsName, JsExpression> replacements = new HashMap<JsName, JsExpression>();
|
||||||
|
if (lexicalScope != null) {
|
||||||
|
Set<JsName> references = CollectUtilsKt.collectUsedNames(node);
|
||||||
|
references.removeAll(CollectUtilsKt.collectDefinedNames(node));
|
||||||
|
|
||||||
|
for (JsName name : references) {
|
||||||
|
VariableDescriptor variable = getVariableByName(lexicalScope, Name.identifier(name.getIdent()));
|
||||||
|
if (variable != null) {
|
||||||
|
replacements.put(name, ReferenceTranslator.translateAsValueReference(variable, context()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultStatement;
|
if (!replacements.isEmpty()) {
|
||||||
|
node = RewriteUtilsKt.replaceNames(node, replacements);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static VariableDescriptor getVariableByName(@NotNull LexicalScope scope, @NotNull Name name) {
|
||||||
|
while (true) {
|
||||||
|
Collection<VariableDescriptor> variables = scope.getContributedVariables(name, NoLookupLocation.FROM_BACKEND);
|
||||||
|
if (!variables.isEmpty()) {
|
||||||
|
return variables.size() == 1 ? variables.iterator().next() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(scope.getParent() instanceof LexicalScope)) break;
|
||||||
|
LexicalScope parentScope = (LexicalScope) scope.getParent();
|
||||||
|
if (scope.getOwnerDescriptor() != parentScope.getOwnerDescriptor()) break;
|
||||||
|
scope = parentScope;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package foo
|
||||||
|
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test
|
||||||
|
|
||||||
|
internal inline fun sum(x: Int, y: Int): Int = js("x + y")
|
||||||
|
|
||||||
|
internal fun test(x: Int, y: Int): Int = sum(sum(x, x), sum(y, y))
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
assertEquals(4, test(1, 1))
|
||||||
|
assertEquals(8, test(1, 3))
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package foo
|
||||||
|
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test
|
||||||
|
|
||||||
|
internal inline fun sum(x: Int, y: Int): Int = js("var a = x; a + y")
|
||||||
|
|
||||||
|
internal fun test(x: Int, y: Int): Int {
|
||||||
|
val xx = sum(x, x)
|
||||||
|
js("var a = 0;")
|
||||||
|
val yy = sum(y, y)
|
||||||
|
|
||||||
|
return sum(xx, yy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
assertEquals(4, test(1, 1))
|
||||||
|
assertEquals(8, test(1, 3))
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
function p(x) {
|
||||||
|
return x + ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = "";
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
external fun p(m: String): String
|
||||||
|
|
||||||
|
var log = ""
|
||||||
|
|
||||||
|
fun test1(p: String): String {
|
||||||
|
log += p("start1")
|
||||||
|
return js("return p;")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test2(): String {
|
||||||
|
val p = "test2"
|
||||||
|
log += p("start2")
|
||||||
|
return js("p")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test3(): String {
|
||||||
|
val p = "wrong3"
|
||||||
|
return js("var p = 'test3'; p")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test4(): String {
|
||||||
|
return js("p('test4')")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test5(): String {
|
||||||
|
val p = "wrong5"
|
||||||
|
fun f() = js("p('test5')")
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test6(): String {
|
||||||
|
log += p("start6")
|
||||||
|
val p = "test6"
|
||||||
|
return js("""
|
||||||
|
var x = p;
|
||||||
|
var g = function(p) {
|
||||||
|
return x + p;
|
||||||
|
};
|
||||||
|
g("-ok");
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test7(): String {
|
||||||
|
log += p("start7")
|
||||||
|
val p = "test7"
|
||||||
|
return js("""
|
||||||
|
var g = function() {
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
g();
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test8(): String {
|
||||||
|
val p = "wrong8"
|
||||||
|
val list = listOf("t", "e", "s", "t", "8")
|
||||||
|
var result = ""
|
||||||
|
for (p in list) {
|
||||||
|
result += js("p")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test9(): String {
|
||||||
|
val p = "wrong9"
|
||||||
|
val list = listOf("t" to "e", "s" to "t", "9" to "!")
|
||||||
|
var result = ""
|
||||||
|
for ((p, q) in list) {
|
||||||
|
result += js("p")
|
||||||
|
result += js("q")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test10(): String {
|
||||||
|
val list = listOf("O" to "K")
|
||||||
|
var result = ""
|
||||||
|
for ((p, _) in list) {
|
||||||
|
result += js("typeof p")
|
||||||
|
result += ";"
|
||||||
|
result += js("typeof _")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
var result = test1("test1")
|
||||||
|
if (result != "test1") return "fail1: $result"
|
||||||
|
|
||||||
|
result = test2()
|
||||||
|
if (result != "test2") return "fail2: $result"
|
||||||
|
|
||||||
|
result = test3()
|
||||||
|
if (result != "test3") return "fail3: $result"
|
||||||
|
|
||||||
|
result = test4()
|
||||||
|
if (result != "test4;") return "fail4: $result"
|
||||||
|
|
||||||
|
result = test5()
|
||||||
|
if (result != "test5;") return "fail5: $result"
|
||||||
|
|
||||||
|
result = test6()
|
||||||
|
if (result != "test6-ok") return "fail6: $result"
|
||||||
|
|
||||||
|
result = test7()
|
||||||
|
if (result != "test7") return "fail7: $result"
|
||||||
|
|
||||||
|
result = test8()
|
||||||
|
if (result != "test8") return "fail8: $result"
|
||||||
|
|
||||||
|
result = test9()
|
||||||
|
if (result != "test9!") return "fail9: $result"
|
||||||
|
|
||||||
|
result = test10()
|
||||||
|
if (result != "string;object") return "fail10: $result"
|
||||||
|
|
||||||
|
if (log != "start1;start2;start6;start7;") return "fail log: $log"
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
fun box(): String {
|
|
||||||
var result = jsTypeOf(23)
|
|
||||||
if (result != "number") return "fail1: $result"
|
|
||||||
|
|
||||||
result = jsTypeOf("23")
|
|
||||||
if (result != "string") return "fail2: $result"
|
|
||||||
|
|
||||||
result = jsTypeOf({ x: Int -> x })
|
|
||||||
if (result != "function") return "fail3: $result"
|
|
||||||
|
|
||||||
result = jsTypeOf(object {})
|
|
||||||
if (result != "object") return "fail4: $result"
|
|
||||||
|
|
||||||
result = jsTypeOf(true)
|
|
||||||
if (result != "boolean") return "fail5: $result"
|
|
||||||
|
|
||||||
return "OK"
|
|
||||||
}
|
|
||||||
@@ -270,8 +270,6 @@
|
|||||||
return cache.value;
|
return cache.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Kotlin.jsTypeOf = function(a) { return typeof a; };
|
|
||||||
|
|
||||||
////////////////////////////////// packages & modules //////////////////////////////
|
////////////////////////////////// packages & modules //////////////////////////////
|
||||||
|
|
||||||
Kotlin.modules = {};
|
Kotlin.modules = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user