diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt index ee3d9915f9c..21fb5e8eb31 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt @@ -6,21 +6,23 @@ package org.jetbrains.kotlin.ir.backend.js.lower import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder import org.jetbrains.kotlin.ir.builders.declarations.buildFun +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.IrBlockBody import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl -import org.jetbrains.kotlin.ir.util.defaultType -import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable -import org.jetbrains.kotlin.ir.util.isEffectivelyExternal +import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -37,26 +39,24 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( override fun lower(irFile: IrFile) { irFile.declarations.forEach { if (it is IrClass) { - generateTestCalls(it) { suiteForPackage(irFile.fqName).function } + generateTestCalls(it) { suiteForPackage(irFile.fqName) } } // TODO top-level functions } } - private val packageSuites = mutableMapOf() + private val packageSuites = mutableMapOf() private fun suiteForPackage(fqName: FqName) = packageSuites.getOrPut(fqName) { context.suiteFun!!.createInvocation(fqName.asString(), testContainerFactory()) } - private data class FunctionWithBody(val function: IrSimpleFunction, val body: IrBlockBody) - private fun IrSimpleFunctionSymbol.createInvocation( name: String, parentFunction: IrSimpleFunction, ignored: Boolean = false - ): FunctionWithBody { + ): IrSimpleFunction { val body = context.irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET, emptyList()) val function = context.irFactory.buildFun { @@ -67,8 +67,7 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( function.parent = parentFunction function.body = body - val parentBody = parentFunction.body as IrBlockBody - parentBody.statements += JsIrBuilder.buildCall(this).apply { + (parentFunction.body as IrBlockBody).statements += JsIrBuilder.buildCall(this).apply { putValueArgument(0, JsIrBuilder.buildString(context.irBuiltIns.stringType, name)) putValueArgument(1, JsIrBuilder.buildBoolean(context.irBuiltIns.booleanType, ignored)) @@ -76,13 +75,15 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( putValueArgument(2, JsIrBuilder.buildFunctionExpression(refType, function)) } - return FunctionWithBody(function, body) + return function } private fun generateTestCalls(irClass: IrClass, parentFunction: () -> IrSimpleFunction) { if (irClass.modality == Modality.ABSTRACT || irClass.isEffectivelyExternal() || irClass.isExpect) return - val suiteFunBody by lazy { context.suiteFun!!.createInvocation(irClass.name.asString(), parentFunction(), irClass.isIgnored) } + val suiteFunBody by lazy { + context.suiteFun!!.createInvocation(irClass.name.asString(), parentFunction(), irClass.isIgnored) + } val beforeFunctions = irClass.declarations.filterIsInstance().filter { it.isBefore } val afterFunctions = irClass.declarations.filterIsInstance().filter { it.isAfter } @@ -90,10 +91,30 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( irClass.declarations.forEach { when { it is IrClass -> - generateTestCalls(it) { suiteFunBody.function } + generateTestCalls(it) { suiteFunBody } it is IrSimpleFunction && it.isTest -> - generateCodeForTestMethod(it, beforeFunctions, afterFunctions, irClass, suiteFunBody.function) + generateCodeForTestMethod(it, beforeFunctions, afterFunctions, irClass, suiteFunBody) + } + } + } + + private fun IrDeclarationWithVisibility.isVisibleFromTests() = + (visibility == DescriptorVisibilities.PUBLIC) || (visibility == DescriptorVisibilities.INTERNAL) + + private fun IrDeclarationWithVisibility.isEffectivelyVisibleFromTests(): Boolean { + return generateSequence(this) { it.parent as? IrDeclarationWithVisibility }.all { + it.isVisibleFromTests() + } + } + + private fun IrClass.canBeInstantiated(): Boolean { + val isClassReachable = isEffectivelyVisibleFromTests() + return if (isObject) { + isClassReachable + } else { + isClassReachable && constructors.any { + it.isVisibleFromTests() && it.explicitParametersCount == if (isInner) 1 else 0 } } } @@ -105,7 +126,25 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( irClass: IrClass, parentFunction: IrSimpleFunction ) { - val (fn, body) = context.testFun!!.createInvocation(testFun.name.asString(), parentFunction, testFun.isIgnored) + val fn = context.testFun!!.createInvocation(testFun.name.asString(), parentFunction, testFun.isIgnored) + val body = fn.body as IrBlockBody + + val exceptionMessage = when { + testFun.valueParameters.isNotEmpty() || !testFun.isEffectivelyVisibleFromTests() -> + "Test method ${irClass.fqNameWhenAvailable ?: irClass.name}::${testFun.name} should have public or internal visibility, can not have parameters" + !irClass.canBeInstantiated() -> + "Test class ${irClass.fqNameWhenAvailable ?: irClass.name} must declare a public or internal constructor with no explicit parameters" + else -> null + } + + if (exceptionMessage != null) { + val irBuilder = context.createIrBuilder(fn.symbol) + body.statements += irBuilder.irCall(context.irBuiltIns.illegalArgumentExceptionSymbol).apply { + putValueArgument(0, irBuilder.irString(exceptionMessage)) + } + + return + } val classVal = JsIrBuilder.buildVar(irClass.defaultType, fn, initializer = irClass.instance()) @@ -145,13 +184,14 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( return if (kind == ClassKind.OBJECT) { JsIrBuilder.buildGetObjectValue(defaultType, symbol) } else { - declarations.asSequence().filterIsInstance().single { it.isPrimary }.let { constructor -> - IrConstructorCallImpl.fromSymbolOwner(defaultType, constructor.symbol).also { - if (isInner) { - it.dispatchReceiver = (parent as IrClass).instance() + declarations.asSequence().filterIsInstance().first { it.explicitParametersCount == if (isInner) 1 else 0 } + .let { constructor -> + IrConstructorCallImpl.fromSymbolOwner(defaultType, constructor.symbol).also { + if (isInner) { + it.dispatchReceiver = (parent as IrClass).instance() + } } } - } } } diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrBoxJsES6TestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrBoxJsES6TestGenerated.java index 9febd26bcd2..947af4adfc7 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrBoxJsES6TestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrBoxJsES6TestGenerated.java @@ -5401,6 +5401,11 @@ public class IrBoxJsES6TestGenerated extends AbstractIrBoxJsES6Test { runTest("js/js.translator/testData/box/kotlin.test/ignore.kt"); } + @TestMetadata("illegalParameters.kt") + public void testIllegalParameters() throws Exception { + runTest("js/js.translator/testData/box/kotlin.test/illegalParameters.kt"); + } + @TestMetadata("incremental.kt") public void testIncremental() throws Exception { runTest("js/js.translator/testData/box/kotlin.test/incremental.kt"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrBoxJsTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrBoxJsTestGenerated.java index f3f38e10b55..d5393fe7a60 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrBoxJsTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrBoxJsTestGenerated.java @@ -5401,6 +5401,11 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest { runTest("js/js.translator/testData/box/kotlin.test/ignore.kt"); } + @TestMetadata("illegalParameters.kt") + public void testIllegalParameters() throws Exception { + runTest("js/js.translator/testData/box/kotlin.test/illegalParameters.kt"); + } + @TestMetadata("incremental.kt") public void testIncremental() throws Exception { runTest("js/js.translator/testData/box/kotlin.test/incremental.kt"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java index e7cc60e9713..b8f16ffd577 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/BoxJsTestGenerated.java @@ -5416,6 +5416,11 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { runTest("js/js.translator/testData/box/kotlin.test/ignore.kt"); } + @TestMetadata("illegalParameters.kt") + public void testIllegalParameters() throws Exception { + runTest("js/js.translator/testData/box/kotlin.test/illegalParameters.kt"); + } + @TestMetadata("incremental.kt") public void testIncremental() throws Exception { runTest("js/js.translator/testData/box/kotlin.test/incremental.kt"); diff --git a/js/js.translator/testData/box/kotlin.test/illegalParameters.kt b/js/js.translator/testData/box/kotlin.test/illegalParameters.kt new file mode 100644 index 00000000000..ff92b5799dc --- /dev/null +++ b/js/js.translator/testData/box/kotlin.test/illegalParameters.kt @@ -0,0 +1,196 @@ +// IGNORE_BACKEND: JS +// KJS_WITH_FULL_RUNTIME +// SKIP_DCE_DRIVEN + +import common.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class BadClass(id: Int) { + @Test + fun foo() {} +} + +private class BadPrivateClass { + @Test + fun foo() {} +} + +class BadProtectedMethodClass { + @Test + protected fun foo() {} +} + +class BadPrimaryGoodSecondary(private val id: Int) { + constructor(): this(3) + @Test + fun foo() { + assertEquals(id, 3) + } +} + +class GoodSecondaryOnly { + constructor() { + triggered = 3 + } + constructor(id: Int) { + triggered = id + } + companion object { + private var triggered = 0 + } + @Test + fun foo() { + assertEquals(triggered, 3) + } +} + +class BadSecondaryOnly { + private constructor() {} + constructor(id: Int) {} + @Test + fun foo() {} +} + +class BadConstructorClass private constructor() { + @Test + fun foo() {} +} + +class BadProtectedConstructorClass protected constructor() { + constructor(flag: Boolean): this() + @Test + fun foo() {} +} + +class GoodClass() { + constructor(id: Int): this() + @Test + fun foo() {} +} + +class GoodNestedClass { + class NestedTestClass { + @Test + fun foo() {} + + fun helperMethod(param: String) {} + } +} + +class BadNestedClass { + class NestedTestClass(id: Int) { + @Test + fun foo() {} + } +} + +class BadMethodClass() { + @Test + fun foo(id: Int) {} + + @Test + private fun ping() {} +} + +// non-reachable scenarios are tested in nested.kt +class OuterWithPrivateCompanion { + private companion object { + object InnerCompanion { + @Test + fun innerCompanionTest() { + } + } + } +} + +class OuterWithPrivateMethod { + companion object { + object InnerCompanion { + @Test + private fun innerCompanionTest() { + } + } + } +} + +fun box() = checkLog { + suite("BadClass") { + test("foo") { + caught("Test class BadClass must declare a public or internal constructor with no explicit parameters") + } + } + suite("BadPrivateClass") { + test("foo") { + caught("Test method BadPrivateClass::foo should have public or internal visibility, can not have parameters") + } + } + suite("BadProtectedMethodClass") { + test("foo") { + caught("Test method BadProtectedMethodClass::foo should have public or internal visibility, can not have parameters") + } + } + suite("BadPrimaryGoodSecondary") { + test("foo") + } + suite("GoodSecondaryOnly") { + test("foo") + } + suite("BadSecondaryOnly") { + test("foo") { + caught("Test class BadSecondaryOnly must declare a public or internal constructor with no explicit parameters") + } + } + suite("BadConstructorClass") { + test("foo") { + caught("Test class BadConstructorClass must declare a public or internal constructor with no explicit parameters") + } + } + suite("BadProtectedConstructorClass") { + test("foo") { + caught("Test class BadProtectedConstructorClass must declare a public or internal constructor with no explicit parameters") + } + } + suite("GoodClass") { + test("foo") + } + suite("GoodNestedClass") { + suite("NestedTestClass") { + test("foo") + } + } + suite("BadNestedClass") { + suite("NestedTestClass") { + test("foo") { + caught("Test class BadNestedClass.NestedTestClass must declare a public or internal constructor with no explicit parameters") + } + } + } + suite("BadMethodClass") { + test("foo") { + caught("Test method BadMethodClass::foo should have public or internal visibility, can not have parameters") + } + test("ping") { + caught("Test method BadMethodClass::ping should have public or internal visibility, can not have parameters") + } + } + suite("OuterWithPrivateCompanion") { + suite("Companion") { + suite("InnerCompanion") { + test("innerCompanionTest") { + caught("Test method OuterWithPrivateCompanion.Companion.InnerCompanion::innerCompanionTest should have public or internal visibility, can not have parameters") + } + } + } + } + suite("OuterWithPrivateMethod") { + suite("Companion") { + suite("InnerCompanion") { + test("innerCompanionTest") { + caught("Test method OuterWithPrivateMethod.Companion.InnerCompanion::innerCompanionTest should have public or internal visibility, can not have parameters") + } + } + } + } +} \ No newline at end of file diff --git a/js/js.translator/testData/box/kotlin.test/nested.kt b/js/js.translator/testData/box/kotlin.test/nested.kt index 2c02cee72a0..d76ceb6bed2 100644 --- a/js/js.translator/testData/box/kotlin.test/nested.kt +++ b/js/js.translator/testData/box/kotlin.test/nested.kt @@ -20,7 +20,7 @@ class Outer { } inner class Inneer { - @Test fun inneerTest() { + @Test fun innermostTest() { call(prop + "Inneer") } } @@ -68,7 +68,7 @@ fun box() = checkLog { call("propInner") } suite("Inneer") { - test("inneerTest") { + test("innermostTest") { call("propInneer") } }