diff --git a/debuglog-plugin/build.gradle.kts b/debuglog-plugin/build.gradle.kts index 51621c1..6c8479f 100644 --- a/debuglog-plugin/build.gradle.kts +++ b/debuglog-plugin/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0") testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable") testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.2.6") + testImplementation("org.bitbucket.mstrobel:procyon-compilertools:0.5.36") } buildConfig { diff --git a/debuglog-plugin/src/main/kotlin/com/bnorm/debug/log/DebugLogTransformer.kt b/debuglog-plugin/src/main/kotlin/com/bnorm/debug/log/DebugLogTransformer.kt index 2f510ac..9dfbe93 100644 --- a/debuglog-plugin/src/main/kotlin/com/bnorm/debug/log/DebugLogTransformer.kt +++ b/debuglog-plugin/src/main/kotlin/com/bnorm/debug/log/DebugLogTransformer.kt @@ -99,10 +99,15 @@ class DebugLogTransformer( if (expression.returnTargetSymbol != function.symbol) return super.visitReturn(expression) return DeclarationIrBuilder(pluginContext, function.symbol).irBlock { - val result = irTemporary(expression.value) - +irDebugExit(function, startTime, irGet(result)) - +expression.apply { - value = irGet(result) + if (expression.value.type == typeUnit) { + +irDebugExit(function, startTime) + +expression + } else { + val result = irTemporary(expression.value) + +irDebugExit(function, startTime, irGet(result)) + +expression.apply { + value = irGet(result) + } } } } diff --git a/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginEarlyReturnTest.kt b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginEarlyReturnTest.kt new file mode 100644 index 0000000..8982278 --- /dev/null +++ b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginEarlyReturnTest.kt @@ -0,0 +1,119 @@ +/* + * 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.debug.log + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class IrPluginEarlyReturnTest { + + private val earlyReturn = SourceFile.kotlin( + "EarlyReturn.kt", """ +import com.bnorm.debug.log.DebugLog + +@DebugLog +fun earlyReturn(input: String): String { + if (input == "EARLY_RETURN_1") { + return "Early return - 1" + } + if (input == "EARLY_RETURN_2") { + return "Early return - 2" + } + return "Normal return" +} +""" + ) + + private val earlyExit = SourceFile.kotlin( + "EarlyExit.kt", """ +import com.bnorm.debug.log.DebugLog + +@DebugLog +fun earlyExit(input: String) { + if (input == "EARLY_RETURN_1") { + return + } + if (input == "EARLY_RETURN_2") { + return + } +} +""" + ) + + @Test + fun `IR plugin enabled - with early return`() { + val result = compile(sourceFile = earlyReturn, DebugLogComponentRegistrar(true)) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + + assertFunction(result.javaCode("EarlyReturnKt"), "public static final String earlyReturn", + """ + public static final String earlyReturn(@NotNull final String input) { + Intrinsics.checkNotNullParameter(input, "input"); + System.out.println((Object)("⇢ earlyReturn(input=" + input + ')')); + final TimeMark markNow = TimeSource.Monotonic.INSTANCE.markNow(); + try { + if (Intrinsics.areEqual(input, "EARLY_RETURN_1")) { + System.out.println((Object)("⇠ earlyReturn [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + "Early return - 1")); + return "Early return - 1"; + } + if (Intrinsics.areEqual(input, "EARLY_RETURN_2")) { + System.out.println((Object)("⇠ earlyReturn [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + "Early return - 2")); + return "Early return - 2"; + } + System.out.println((Object)("⇠ earlyReturn [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + "Normal return")); + return "Normal return"; + } + catch (Throwable t) { + System.out.println((Object)("⇠ earlyReturn [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + t)); + throw t; + } + } + """.trimIndent()) + } + + @Test + fun `IR plugin enabled - with early exit`() { + val result = compile(sourceFile = earlyExit, DebugLogComponentRegistrar(true)) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + + assertFunction(result.javaCode("EarlyExitKt"), "public static final void earlyExit", + """ + public static final void earlyExit(@NotNull final String input) { + Intrinsics.checkNotNullParameter(input, "input"); + System.out.println((Object)("⇢ earlyExit(input=" + input + ')')); + final TimeMark markNow = TimeSource.Monotonic.INSTANCE.markNow(); + try { + if (Intrinsics.areEqual(input, "EARLY_RETURN_1")) { + System.out.println((Object)("⇠ earlyExit [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + ']')); + return; + } + if (Intrinsics.areEqual(input, "EARLY_RETURN_2")) { + System.out.println((Object)("⇠ earlyExit [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + ']')); + return; + } + System.out.println((Object)("⇠ earlyExit [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + ']')); + } + catch (Throwable t) { + System.out.println((Object)("⇠ earlyExit [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + t)); + throw t; + } + } + """.trimIndent()) + } +} diff --git a/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginTest.kt b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginTest.kt index 8136f7c..3d4800a 100644 --- a/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginTest.kt +++ b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginTest.kt @@ -18,13 +18,9 @@ package com.bnorm.debug.log import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile -import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import java.io.ByteArrayOutputStream -import java.io.PrintStream -import java.lang.reflect.InvocationTargetException class IrPluginTest { @@ -56,6 +52,44 @@ fun doSomething() { val result = compile(sourceFile = main, DebugLogComponentRegistrar(true)) assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + val javaCode = result.javaCode("MainKt") + + assertFunction(javaCode, "public static final String greet", + """ + public static final String greet(@NotNull final String greeting, @NotNull final String name) { + Intrinsics.checkNotNullParameter(greeting, "greeting"); + Intrinsics.checkNotNullParameter(name, "name"); + System.out.println((Object)("⇢ greet(greeting=" + greeting + ", name=" + name + ')')); + final TimeMark markNow = TimeSource.Monotonic.INSTANCE.markNow(); + try { + Thread.sleep(15L); + final String string = greeting + ", " + name + '!'; + System.out.println((Object)("⇠ greet [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + string)); + return string; + } + catch (Throwable t) { + System.out.println((Object)("⇠ greet [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + t)); + throw t; + } + } + """.trimIndent()) + + assertFunction(javaCode, "public static final void doSomething", + """ + public static final void doSomething() { + System.out.println((Object)"⇢ doSomething()"); + final TimeMark markNow = TimeSource.Monotonic.INSTANCE.markNow(); + try { + Thread.sleep(15L); + System.out.println((Object)("⇠ doSomething [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + ']')); + } + catch (Throwable t) { + System.out.println((Object)("⇠ doSomething [" + (Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc()) + "] = " + t)); + throw t; + } + } + """.trimIndent()) + val out = invokeMain(result, "MainKt").trim().split("""\r?\n+""".toRegex()) assert(out.size == 8) assert(out[0] == "⇢ greet(greeting=Hello, name=World)") @@ -73,49 +107,28 @@ fun doSomething() { val result = compile(sourceFile = main, DebugLogComponentRegistrar(false)) assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + val javaCode = result.javaCode("MainKt") + + assertFunction(javaCode, "public static final String greet", + """ + public static final String greet(@NotNull final String greeting, @NotNull final String name) { + Intrinsics.checkNotNullParameter(greeting, "greeting"); + Intrinsics.checkNotNullParameter(name, "name"); + Thread.sleep(15L); + return greeting + ", " + name + '!'; + } + """.trimIndent()) + + assertFunction(javaCode, "public static final void doSomething", + """ + public static final void doSomething() { + Thread.sleep(15L); + } + """.trimIndent()) + val out = invokeMain(result, "MainKt").trim().split("""\r?\n+""".toRegex()) assertTrue(out.size == 2) assertTrue(out[0] == "Hello, World!") assertTrue(out[1] == "Hello, Kotlin IR!") } } - -fun compile( - sourceFiles: List, - plugin: ComponentRegistrar, -): KotlinCompilation.Result { - return KotlinCompilation().apply { - sources = sourceFiles - useIR = true - compilerPlugins = listOf(plugin) - inheritClassPath = true - verbose = false - }.compile() -} - -fun compile( - sourceFile: SourceFile, - plugin: ComponentRegistrar, -): KotlinCompilation.Result { - return compile(listOf(sourceFile), plugin) -} - -fun invokeMain(result: KotlinCompilation.Result, className: String): String { - val oldOut = System.out - try { - val buffer = ByteArrayOutputStream() - System.setOut(PrintStream(buffer, false, "UTF-8")) - - try { - val kClazz = result.classLoader.loadClass(className) - val main = kClazz.declaredMethods.single { it.name == "main" && it.parameterCount == 0 } - main.invoke(null) - } catch (e: InvocationTargetException) { - throw e.targetException - } - - return buffer.toString("UTF-8") - } finally { - System.setOut(oldOut) - } -} diff --git a/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/TestUtils.kt b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/TestUtils.kt new file mode 100644 index 0000000..1aa1f6b --- /dev/null +++ b/debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/TestUtils.kt @@ -0,0 +1,142 @@ +/* + * 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.debug.log + +import com.strobel.assembler.InputTypeLoader +import com.strobel.assembler.metadata.ArrayTypeLoader +import com.strobel.assembler.metadata.CompositeTypeLoader +import com.strobel.assembler.metadata.ITypeLoader +import com.strobel.decompiler.Decompiler +import com.strobel.decompiler.DecompilerSettings +import com.strobel.decompiler.PlainTextOutput +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.io.StringWriter +import java.lang.reflect.InvocationTargetException +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.utils.indexOfFirst +import org.junit.jupiter.api.Assertions + +fun assertFunction(javaCode: String, functionStatement: String, expectedFunction: String) { + Assertions.assertEquals(expectedFunction, fetchMethodByPrefix(javaCode, functionStatement)) +} + +fun fetchMethodByPrefix(classText: String, methodSignaturePrefix: String): String { + val classLines = classText.split("\n") + val methodSignaturePredicate: (String) -> Boolean = { line -> line.contains(methodSignaturePrefix) } + val methodFirstLineIndex = classLines.indexOfFirst(methodSignaturePredicate) + + check(methodFirstLineIndex != -1) { + "Method with prefix '$methodSignaturePrefix' not found within class:\n$classText" + } + + val multiplePrefixMatches = classLines + .indexOfFirst(methodFirstLineIndex + 1, methodSignaturePredicate) + .let { index -> index != -1 } + + check(!multiplePrefixMatches) { + "Multiple methods with prefix '$methodSignaturePrefix' found within class:\n$classText" + } + + val indentationSize = classLines[methodFirstLineIndex].takeWhile { it == ' ' }.length + + var curleyBraceCount = 1 + var currentLineIndex: Int = methodFirstLineIndex + 1 + + while (curleyBraceCount != 0 && currentLineIndex < classLines.lastIndex) { + if (classLines[currentLineIndex].contains("{")) { + curleyBraceCount++ + } + if (classLines[currentLineIndex].contains("}")) { + curleyBraceCount-- + } + currentLineIndex++ + } + + return classLines + .subList(methodFirstLineIndex, currentLineIndex) + .joinToString("\n") { it.substring(indentationSize) } +} + +fun compile( + sourceFiles: List, + plugin: ComponentRegistrar +): KotlinCompilation.Result { + return KotlinCompilation().apply { + sources = sourceFiles + useIR = true + compilerPlugins = listOf(plugin) + inheritClassPath = true + verbose = false + }.compile() +} + +fun compile( + sourceFile: SourceFile, + plugin: ComponentRegistrar +): KotlinCompilation.Result { + return compile(listOf(sourceFile), plugin) +} + +fun invokeMain(result: KotlinCompilation.Result, className: String): String { + val oldOut = System.out + try { + val buffer = ByteArrayOutputStream() + System.setOut(PrintStream(buffer, false, "UTF-8")) + + try { + val kClazz = result.classLoader.loadClass(className) + val main = kClazz.declaredMethods.single { it.name == "main" && it.parameterCount == 0 } + main.invoke(null) + } catch (e: InvocationTargetException) { + throw e.targetException + } + + return buffer.toString("UTF-8") + } finally { + System.setOut(oldOut) + } +} + +fun KotlinCompilation.Result.javaCode(className: String): String { + val decompilerSettings = DecompilerSettings.javaDefaults().apply { + typeLoader = CompositeTypeLoader(*(mutableListOf() + .apply { + // Ensure every class is available. + generatedFiles.forEach { + add(ArrayTypeLoader(it.readBytes())) + } + + // Loads any standard classes already on the classpath. + add(InputTypeLoader()) + } + .toTypedArray())) + + isUnicodeOutputEnabled = true + } + + return StringWriter().let { writer -> + Decompiler.decompile( + className, + PlainTextOutput(writer).apply { isUnicodeOutputEnabled = true }, + decompilerSettings + ) + writer.toString().trimEnd().trimIndent() + } +}