IrGenerationExtension for transforming assert calls
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
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("org.jetbrains.dokka") version "0.10.0" apply false
|
||||||
id("nebula.release") version "13.1.1"
|
id("nebula.release") version "13.1.1"
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ val release = tasks.findByPath(":release")
|
|||||||
release?.finalizedBy(project.getTasksByName("publish", true))
|
release?.finalizedBy(project.getTasksByName("publish", true))
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "com.bnorm.assert.power"
|
group = "com.bnorm.power"
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.60")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
testImplementation(kotlin("test-junit"))
|
||||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.2.5")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+31
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user