Skip to content

Commit

Permalink
[JS IR] Throw exception if test class or test method has explicit par…
Browse files Browse the repository at this point in the history
  • Loading branch information
Schahen committed Nov 23, 2020
1 parent c4a8d1c commit 2d4e9af
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<FqName, FunctionWithBody>()
private val packageSuites = mutableMapOf<FqName, IrSimpleFunction>()

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 {
Expand All @@ -67,33 +67,54 @@ 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))

val refType = IrSimpleTypeImpl(context.ir.symbols.functionN(0), false, emptyList(), emptyList())
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<IrSimpleFunction>().filter { it.isBefore }
val afterFunctions = irClass.declarations.filterIsInstance<IrSimpleFunction>().filter { it.isAfter }

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
}
}
}
Expand All @@ -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())

Expand Down Expand Up @@ -145,13 +184,14 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: (
return if (kind == ClassKind.OBJECT) {
JsIrBuilder.buildGetObjectValue(defaultType, symbol)
} else {
declarations.asSequence().filterIsInstance<IrConstructor>().single { it.isPrimary }.let { constructor ->
IrConstructorCallImpl.fromSymbolOwner(defaultType, constructor.symbol).also {
if (isInner) {
it.dispatchReceiver = (parent as IrClass).instance()
declarations.asSequence().filterIsInstance<IrConstructor>().first { it.explicitParametersCount == if (isInner) 1 else 0 }
.let { constructor ->
IrConstructorCallImpl.fromSymbolOwner(defaultType, constructor.symbol).also {
if (isInner) {
it.dispatchReceiver = (parent as IrClass).instance()
}
}
}
}
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

196 changes: 196 additions & 0 deletions js/js.translator/testData/box/kotlin.test/illegalParameters.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
}
Loading

0 comments on commit 2d4e9af

Please sign in to comment.