From 6d709a01f4a60fabfd6c238f734188317eeea326 Mon Sep 17 00:00:00 2001 From: Brian Norman Date: Wed, 5 Feb 2020 14:32:02 -0600 Subject: [PATCH] IrGenerationExtension for transforming assert calls --- build.gradle.kts | 4 +- kotlin-power-assert/build.gradle.kts | 5 +- .../bnorm/power/PowerAssertCallTransformer.kt | 135 ++++++++++++++++++ .../power/PowerAssertComponentRegistrar.kt | 36 +++++ .../power/PowerAssertIrGenerationExtension.kt | 31 ++++ .../src/test/kotlin/com/bnorm/power/test.kt | 70 +++++++++ 6 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt create mode 100644 kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertComponentRegistrar.kt create mode 100644 kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertIrGenerationExtension.kt create mode 100644 kotlin-power-assert/src/test/kotlin/com/bnorm/power/test.kt diff --git a/build.gradle.kts b/build.gradle.kts index 65ad8552656..2b5ef2454d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "1.3.50" apply false + kotlin("jvm") version "1.3.60" apply false id("org.jetbrains.dokka") version "0.10.0" apply false id("nebula.release") version "13.1.1" } @@ -8,7 +8,7 @@ val release = tasks.findByPath(":release") release?.finalizedBy(project.getTasksByName("publish", true)) allprojects { - group = "com.bnorm.assert.power" + group = "com.bnorm.power" } subprojects { diff --git a/kotlin-power-assert/build.gradle.kts b/kotlin-power-assert/build.gradle.kts index 06b973f7ba2..f1b108c6392 100644 --- a/kotlin-power-assert/build.gradle.kts +++ b/kotlin-power-assert/build.gradle.kts @@ -10,9 +10,10 @@ plugins { dependencies { implementation(kotlin("stdlib-jdk8")) + compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.60") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.1") - testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1") + testImplementation(kotlin("test-junit")) + testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.2.5") } tasks.withType { diff --git a/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt new file mode 100644 index 00000000000..18d7e9bb0f0 --- /dev/null +++ b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 Brian Norman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bnorm.power + +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.lower.at +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.common.lower.irIfThen +import org.jetbrains.kotlin.backend.common.lower.irNot +import org.jetbrains.kotlin.backend.common.lower.irThrow +import org.jetbrains.kotlin.backend.jvm.JvmBackendContext +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.SourceRangeInfo +import org.jetbrains.kotlin.ir.builders.irBlock +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irConcat +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.builders.irTemporary +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.path +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.getPackageFragment +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.ir.visitors.acceptVoid +import java.io.File + +fun FileLoweringPass.runOnFileInOrder(irFile: IrFile) { + irFile.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFile(declaration: IrFile) { + lower(declaration) + super.visitFile(declaration) + } + }) +} + +class PowerAssertCallTransformer( + private val context: JvmBackendContext +) : IrElementTransformerVoidWithContext(), FileLoweringPass { + private lateinit var file: IrFile + private lateinit var fileSource: String + + private val constructor = this@PowerAssertCallTransformer.context.ir.symbols.assertionErrorConstructor + + override fun lower(irFile: IrFile) { + file = irFile + fileSource = File(irFile.path).readText() + + irFile.transformChildrenVoid() + } + + override fun visitCall(expression: IrCall): IrExpression { + val function = expression.symbol.owner + if (!function.isAssert) + return super.visitCall(expression) + + val callSource = fileSource.substring(expression.startOffset, expression.endOffset) + val callIndent = file.info(expression).startColumnNumber + + context.createIrBuilder(expression.symbol).run { + at(expression) + + return irBlock { + val assertCondition = expression.getValueArgument(0)!! +// val argumentSource = fileSource.substring(assertCondition.startOffset, assertCondition.endOffset) + val indent = file.info(assertCondition).startColumnNumber - callIndent + +// println(buildString { +// append(callSource).newline() +// indent(indent).append("|").newline() +// indent(indent).append(argumentSource) +// }) + + // TODO transform tree of expressions and create irTemporary for each + val temp = irTemporary(assertCondition) + val throwError = irThrow(irCall(constructor).apply { + + val message = irConcat().apply { + addArgument(irString(buildString { + append("Assertion failed:").newline() + append(callSource).newline() + indent(indent).append("|").newline() + indent(indent) + })) + addArgument(irGet(temp)) + } + putValueArgument(0, message) + }) + +irIfThen(irNot(irGet(temp)), throwError) + } + } + } + + +} + +val IrFunction.isAssert: Boolean + get() = name.asString() == "assert" && getPackageFragment()?.fqName == KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME + +fun IrFile.info(expression: IrElement): SourceRangeInfo { + return fileEntry.getSourceRangeInfo(expression.startOffset, expression.endOffset) +} + +fun StringBuilder.indent(indentation: Int) = apply { + repeat(indentation) { + append(" ") + } +} + +fun StringBuilder.newline() = apply { + append("\n") +} diff --git a/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertComponentRegistrar.kt b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertComponentRegistrar.kt new file mode 100644 index 00000000000..d3b9a3aff38 --- /dev/null +++ b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertComponentRegistrar.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Brian Norman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bnorm.power + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.SourceRangeInfo +import org.jetbrains.kotlin.ir.declarations.IrFile + +class PowerAssertComponentRegistrar : ComponentRegistrar { + override fun registerProjectComponents( + project: MockProject, + configuration: CompilerConfiguration + ) { + IrGenerationExtension.registerExtension(project, PowerAssertIrGenerationExtension()) + } +} + + diff --git a/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertIrGenerationExtension.kt b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertIrGenerationExtension.kt new file mode 100644 index 00000000000..e3e9dd60d05 --- /dev/null +++ b/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertIrGenerationExtension.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Brian Norman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bnorm.power + +import org.jetbrains.kotlin.backend.common.BackendContext +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.jvm.JvmBackendContext +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.resolve.BindingContext + +class PowerAssertIrGenerationExtension : IrGenerationExtension { + override fun generate(file: IrFile, backendContext: BackendContext, bindingContext: BindingContext) { + if (backendContext is JvmBackendContext) { + PowerAssertCallTransformer(backendContext).runOnFileInOrder(file) + } + } +} diff --git a/kotlin-power-assert/src/test/kotlin/com/bnorm/power/test.kt b/kotlin-power-assert/src/test/kotlin/com/bnorm/power/test.kt new file mode 100644 index 00000000000..128c689dd79 --- /dev/null +++ b/kotlin-power-assert/src/test/kotlin/com/bnorm/power/test.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Brian Norman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bnorm.power + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.intellij.lang.annotations.Language +import org.junit.Test +import java.lang.reflect.InvocationTargetException +import kotlin.test.assertEquals +import kotlin.test.fail + +class CompilerTest { + @Test + fun testMyCompilerPlugin() { + assertMessage( + """ +fun main() { + val hello = "Brian" + assert(hello == "World") +}""", + """ +Assertion failed: +assert(hello == "World") + | + false +""".trimIndent() + ) + } + +} + +fun assertMessage(@Language("kotlin") source: String, message: String) { + val result = KotlinCompilation().apply { + sources = listOf(SourceFile.kotlin("main.kt", source)) + useIR = true + messageOutputStream = System.out + compilerPlugins = listOf(PowerAssertComponentRegistrar()) + inheritClassPath = true + }.compile() + + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + + val kClazz = result.classLoader.loadClass("MainKt") + val main = kClazz.declaredMethods.single { it.name == "main" } + try { + try { + main.invoke(null) + } catch (t: InvocationTargetException) { + throw t.cause!! + } + fail("should have thrown assertion") + } catch (t: AssertionError) { + assertEquals(message, t.message) + } +}