Kotlin: Possible to modify functions during compile time through metaprogramming? - kotlin

In dynamic languages like JavaScript/Python, it's possible to overwrite or "modify" functions during run-time. For example, in order to modify the alert function in JS, one could do:
const _prev_alert = window.alert;
window.alert = function() {
_prev_alert.apply(this, arguments);
console.log("Alert function was called!");
}
This would output "Alert function was called!" to the console every time the alert function is called.
Now, obviously something like this would be impossible during runtime in Kotlin-JVM or Kotlin-Native due to their static nature. However, in regards to those same languages, is it possible to perhaps modify a non-compiled function during compile time? I don't mean pre-compiled functions from libraries, but instead functions I have written in the same project I'm developing on.
For example, let's say I have a function I wrote called get_number. Could I modify get_number to return a different number without changing how its called in main and without modifying its code directly? (Or is there a way I COULD write the original get_number so modification IS possible down the line?)
fun main(args: Array<String>) {
println(get_number())
}
fun get_number(): Int {
return 3
}
// Without modifying the code above, can I get main to print something besides 3?
I've been reading into Kotlin's metaprogramming with Annotations and Reflections, so perhaps those could control the compiler's behavior and overwrite get_number's code? Or is this complete lunacy and the only way something of this nature would be possible is through developing my own, separate, metaprogramming wrapper over Kotlin?
Also, just to double-clarify, this question is not about Kotlin-JS and the answer (if it exists) should be applicable to Kotlin-JVM or Native.

As stated in my comment: in almost all cases, it's more desirable to use an appropriate design pattern than to start relying on things like dynamic proxies, reflection, or AOP to address this kind of problem.
That being said, the question asks whether it's possible to modify Kotlin functions at compile time through meta-programming, and the answer is "Yes". To demonstrate, below is a complete example that uses AspectJ.
Project structure
I set up a small Maven-based project with the following structure:
.
├── pom.xml
└── src
└── main
└── kotlin
├── Aop.kt
└── Main.kt
I'll reproduce the contents of all files in the sections below.
Application code
The actual application code is in the file named Main.kt, and—except for the fact that I renamed your function to be in line with Kotlin naming rules—it's identical to the code provided in your question. The getNumber() method is designed to return 3.
fun main(args: Array<String>) {
println(getNumber())
}
fun getNumber(): Int {
return 3
}
AOP code
The AOP-related code is in Aop.kt, and is very simple. It has an #Around advice with a point cut that matches the execution of the getNumber() function. The advice will intercept the call to the getNumber() method and return 42 (instead of 3).
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
#Aspect
class Aop {
#Around("execution(* MainKt.getNumber(..))")
fun getRealNumber(joinPoint: ProceedingJoinPoint): Any {
return 42
}
}
(Note how the name of the generated class for the Main.kt file is MainKt.)
POM file
The POM file puts everything together. I'm using 4 plugins:
The kotlin-maven-plugin takes care of compiling the Kotline files. The configuration includes the execution of the kapt plugin to process the AspectJ annotations.
The jcabi-maven-plugin executes the AspectJ compiler/weaver to weave the aspects into the binary classes.
The maven-jar-plugin builds the JAR file with a manifest that references the main class.
The maven-shade-plugin builds a fat JAR that includes all library dependencies.
This is the complete POM file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>x.y.z</groupId>
<artifactId>kotlin-aop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<kotlin.version>1.2.61</kotlin.version>
<aspectj.version>1.9.1</aspectj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
</execution>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<version>0.14.1</version>
<executions>
<execution>
<goals>
<goal>ajc</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>MainKt</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Building and executing
To build, as with any Maven project, you just need to run:
mvn clean package
This will build a fat JAR at the target/kotlin-aop-1.0-SNAPSHOT.jar location. This JAR can then be executed using the java command:
java -jar target/kotlin-aop-1.0-SNAPSHOT.jar
Execution then gives us the following result, demonstrating that everything worked as expected:
42
(Application was built and executed using the most recent Oracle Java 8 JDK at the time of writing—1.8.0_181)
Conclusion
As the example above demonstrates, it's certainly possible to redefine Kotlin functions, but—to reiterate my original point—in almost all cases, there are more elegant solutions available to achieve what you need.

Related

Cannot access 'kotlin.ranges.OpenEndRange' in IDEA and Kotlin 1.7.0

I have a Kotlin + Maven project which uses Kotlin 1.7.0. When I import the project in IDEA I see an 'error' (more on that later) in the in operator when used with an IntRange:
My IDEA version is the latest as of this writing (Build #IU-223.7571.182, built on November 29, 2022).
It looks like for some reason IDEA is trying to find the OpenEndRange type introduced in 1.7.20 (https://kotlinlang.org/docs/whatsnew1720.html#preview-of-the-operator-for-creating-open-ended-ranges), but my project is using 1.7.0 and the type doesn't exist.
I noticed that in IDEA preferences it uses a newer plugin and analyzer versions than 1.7.0:
When the project is compiled on the CLI with just maven, all works. Actually, it also builds and runs in IDEA, so the red underscore is somehow bogus and confusing.
Here is my pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>idea_kotlin_test</artifactId>
<version>0.0.1</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<kotlin.version>1.7.0</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
and the sample code that shows the issue:
package com.example
import java.time.LocalDate
import java.time.temporal.ChronoField
fun foo(month: Int): Boolean {
val start = LocalDate.now().get(ChronoField.MONTH_OF_YEAR) // as Int
return month in (start..12) // <=== here
}
fun main() {
println(foo(1))
}
I imported the project into IDEA by just choosing 'File' -> 'Open' and choosing the project folder, it detects everything automatically.
It looks as though IDEA somehow doesn't like the LocalDate.get(TemporalField field) call, or rather its return value. When I add as Int, the 'error' goes away but then as Int is greyed out as 'needless cast' (which I expect); the 'error' is also not shown when I just assign a hardcoded Int to the start val. I tried calling different Java methods returning int but to no avail, I couldn't reproduce the issue.
What is going on and how can I tell IDEA to stop showing this strange 'error'? Why is the LocalDate.get(TemporalField) method special?

Compiling gRPC,with both Kotlin and Java using Maven (various packages not found)

I have a mixed kotlin/java project and I wanted to add gRPC to it. I configured the gRPC plugin as usual:
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:3.11.2:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.25.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
...
but when building from scratch
mvn clean && mvn package
the kotlin code would never find the generated sources. When building first from IntelliJ it would work fine - IntelliJ knows what to do.
I tried configuring build-helper-maven-plugin to include the generated-sources directories that the protobuf compiler and stub generator use, but that didn't seem to have any effect (and in fact seems unnecessary).
The answer it turns out is because of the order plugins execute. Generally for kotlin-java hybrid projects you want to compile the kotlin first so that the java compiler knows how to find kotlin classes. The problem is, if you do that the kotlin compiler doesn't see the protoc-generated java classes yet. It's a circular dependency.
It took me a while to figure it out, but to fix it you just have to tell the kotlin compiler where to find the protoc-generated sources like this:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals> <goal>compile</goal> </goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
<sourceDir>${project.build.directory}/generated-sources/protobuf/java/</sourceDir>
<sourceDir>${project.build.directory}/generated-sources/protobuf/grpc-java/</sourceDir>
</sourceDirs>
</configuration>
</execution>
...
</plugin>
It took me entirely too long to figure that out, so I'm posting this as a self-answered question to perhaps save somebody else some time.
I saw a number of related questions but none exactly the same. Good luck.

How to create a subdirectory within target with Maven2

I need to create a subdirectory within the target directory when compiling with maven2. The reason is that I'm using a plugin which grabs responses to SOAP-requests and store them in /target/xml in the integration-test phase.
The problem is that if I specify the plugin's savepath to (in example): ${basedir}/target/xml the plugin throws a FileNotFoundException. The reason I want the file to be in /target is so that the directory is cleaned when invoking mvn clean.
Any suggestions?
You could create a common abstract base class that your test case classes extend.
Add a static initializer initializer to the abstract base class that checks whether the directory exists and if not then creates it.
The static initializer block will be executed the first time that the base class is loaded, and will be executed before any static initializer blocks or constructors in the test case sub-classes.
EDIT:
OK, then you'll have to uglify your pom with the plugin definition below, which will bind to the generate-test-resources phase, and invoke the antrun plugin to create the directory.
<build>
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>generate-test-resources</phase>
<configuration>
<tasks>
<echo message="Creating test output directory"/>
<mkdir dir="./target/xml"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>

Maven-2: avoid default packaging?

My project uses many assemblies, hence I'm interested only in the assemblies.
On executing mvn install apart from the assemblies, I'm getting the default packaged .jar.
How can I avoid this?
I have a pom.xml similar to the one you have provided.
On executing mvn install, I'm getting App1.jar, App2.jar, and snapshot jar containing all contents
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-2</version>
<executions>
<execution>
<id>assemblyone</id>
<phase>compile</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>App1</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>${basedir}/src/main/resources/assemblies/report.xml</descriptor>
</descriptors>
</configuration>
</execution>
<execution>
<id>assemblytwo</id>
<phase>compile</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>App2</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>${basedir}/src/main/resources/assemblies/src.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
How can I avoid this snapshot (not sure of the exact term) jar and ensure that only assemblies are created?
I can read your question two ways, I've outlined answers for both below. If neither is correct, can you modify your question with a bit more explanation please.
1) Do you mean you have a project with default (jar) packaging, and you want to avoid the creation of the jar when no assembly is defined? If this is the case, what is the build achieving if no assembly is defined?
2) Do you instead mean that you are running mvn assembly:assembly to generate the assembly and want to know how to get that assembly when running the install goal?
For option 2, you can bind the assembly-plugin to a lifecycle phase to ensure it is always run, if you specify that <appendAssemblyId> should be false, then the assembly will replace the default jar.
For example, this configuration will invoke the assembly plugin during the packaging phase and replace the default jar:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/archive.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
For option 1, this is actually quite tricky to do. The best I can think of is to specify that the project has pom packaging and configure the project with the executions normally bound to the jar lifecycle in a profile. The lifecycle bindings you'd need to configure are listed in the introduction to the build lifecycle
I'm not sure that you can really do that in a really simple way.
A solution is to call the clean plugin once the build is achieved, by doing that:
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<executions>
<execution>
<id>auto-clean</id>
<phase>package</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
...
</build>
This way, the JAR created in the target/ directory will be deleted at the end of the Maven2 execution.
However, you will have to define another directory to store the assemblies created by Maven2. Otherwise, it will be deleted by the call of the clean plugin... If you want to store them in the directory assemblies-target/, you can add that in the pom.xml file:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-4</version>
<configuration>
...
<!-- Copy the ZIP in target/ of the ROOT directory. -->
<outputDirectory>assemblies-target</outputDirectory>
</configuration>
...
I think it would be much more clear if you showed us your whole POM and the artifacts that are being built. I can only guess as to what the problem is because your terminology is not what I am familiar with. This is my guess as to the problem: you have a POM configured to generated two assembly JARs, but you get a third JAR for the POM itself. In other words, if your POM's artifactId is MyApp, you are getting a MyApp-1.0.0.jar or similar in addition to the two JARs you actually want.
If that is the case, the problem boils down to that you are using Maven to create multiple artifacts from a single module. Maven is designed to produce only one primary artifact from each module. What I would do is change your POM to have a packaging type of "pom" and give it two modules in a <modules> section named App1 and App2. Create sub-directories under your module, one for each App. Give them each a POM configured for a single assembly, with a packaging type of "jar". Move the code/files/etc. as appropriate into each sub-module so there aren't any remaining in the parent module.
With Maven, if you find yourself generating two artifacts from one module (which you are), you should first consider that you are probably violating a Maven best-practice and rearrange things so you only produce one artifact per module.
Please let me know if this doesn't make sense and I will try to clarify.

Is there a variable in Maven that holds the current goal?

In order to invoke the Maven invoker plugin with the same goal that is currently running in my uber-pom, I need a way to pass the current goal into the invoker-plugin's config.
Somethig like
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-invoker-plugin</artifactId>
...
<configuration>
<goals>
<goal>${maven.goal}</goal>
</goals>
...
The Maven Help plugin might help you get where you want to go. The ${reactorProjects} variable holds what you are looking for, but perhaps not in precisely the format you are looking to reuse it.
You can view all the expressions available to you via:
mvn help:expressions
and then you can test one of them without the tedium of a pom via evaluation:
mvn help:evaluate
which takes you to a prompt you can use to try expressions.
If I use the help:evaluate and type ${reactorProjects}, I'll get a lot of output, but part of which includes the data you are after:
<plugins>
<plugin>
<inheritanceApplied>true</inheritanceApplied>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-help-plugin</artifactId>
<version>2.1</version>
<extensions>false</extensions>
<dependencies/>
</plugin>
</plugins>
<pluginMap class="linked-hash-map">
<entry>
<string>org.apache.maven.plugins:maven-help-plugin</string>
<plugin reference="../../../plugins/plugin"/>
</entry>
</pluginMap>
I added the following dependency:
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.0.3</version>
</dependency>
And then in my MOJO:
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
...
#Parameter(defaultValue = "${session}", readonly = true)
private MavenSession session;
Finally, I was able to get the current goals:
MavenExecutionRequest executionRequest = session.getRequest();
List<String> goals = executionRequest.getGoals();