Idea 2021.2.1 Breaks The Gosu Plugin Implementation by Making Mappings final
[Submitted by request of Yann Cébron on Slack #intellij-platform]
Our incremental builder for Gosu relies on extending the Mappings class, which has worked since 2014 through Idea 2021.1.3. But Idea 2021.2.1 breaks the Gosu plugin Implementation by making mappings final.
Background on Gosu Enhancements
The Gosu language provides a concept called enhancement that allows the programmer to enhance a Gosu or Java class with additional methods and properties, as if those methods and properties were part of the original Gosu or Java class. It works by creating a helper class with static methods, each with an initial parameter that is an instance of the class being enhanced. So, for example, if you want to enhance java.lang.String to have the method startsWithIgnoreCase, the Gosu enhancement class would be something like this:
enhancement CoreStringEnhancement : java.lang.String {
function startsWithIgnoreCase(substring : String) : boolean {
// appropriate implementation
}
}
and it would result in a generated Java class something like this:
public class CoreStringEnhancement {
public static boolean startsWithIgnoreCase(String thisString, String substring) {
// appropriate implementation in Java
}
}
In a Gosu class, any String object then can call this new pseudo-member method. The Gosu compiler translates a call to this pseudo-method to a call to the static method in CoreStringEnhancement.startsWithIgnoreCase(). For example,
name.startsWithIgnoreCase("bob")
is translated to
CoreStringEnhancement.startsWithIgnoreCase(name, "bob")
Since 2014, we have been implementing our Gosu builder using code that extends
org.jetbrains.jps.builders.java.dependencyView.Mappings
so that we can insert our own logic in our incremental builder to figure out if a Java class being recompiled has been changed in a way that requires us to recompile Gosu files, including the enhancement class itself and any Gosu classes that call any of the enhancement methods in it.
This mechanism has worked correctly from Idea 2014 through Idea 2021.1.3, i.e., for 7 years.
Problem in Idea 2021.2.1
In Idea 2021.2.1, you made Mappings a final class, so our incremental build mechanism that works by extending it is now broken, with no obvious way to fix it.
We need a way to accomplish what we have been doing for 7 years in our language plugin.
Is it really necessary for you to make Mappings final? It causes us grief.
To understand how our current implementation works is difficult to explain simply. The following example and outline of an explanation shows how our implementation works, so that you can tell me if there is some reasonable alternative to accomplish it without extending your Mappings class.
Example
// base Java class being enhanced: A.java
public class A {
// Uncomment method bar and make sure the Gosu class B
// gets recompiled to invoke A.bar(String) here, rather than
// the less specific AEnh.bar(Object)
// public int bar(String s) {
// return 0;
// }
}
// Gosu enhancement class: AEnh.gsx
enhancement AEnh : A {
function bar(o : Object) : int { return 1 }
}
// Gosu class invoking bar(Object) as pseudo-member of A: B.gs
public class B {
// check that this gets recompiled so it picks up method on A in preference to enhancement
var v : int = new A().bar("")
public static function main(args : String[]) {
print(new B().v == 0 ? "bar() from A" : "bar() from AEnh");
}
}
If the definition of method A#bar(String) is uncommented, and an incremental recompilation gets done, we expect that both the enhancement AEnh.gsx and the invocation of bar("") in B.gs get recompiled to call the actual A#bar("") instead of the static one based AEnh.bar(Object).
How We Make It Work
Our class GosuMappings extends the JetBrains class
org.jetbrains.jps.builders.java.dependencyView.Mappings
and our builder makes sure that we end up with an instance of GosuMappings (instead of Mappings) in the delta we see, so that code in GosuMappings can detect the change that happens as a result of adding (uncommenting) the method bar(String) in A.java. There is actually no textual change made to the class B.gs at all; rather, GosuMappings is invoked through our builder when A.java is changed (in the delta), and code in GosuMappings notices that A.java now has the more specific instance of bar(). See the methods in GosuMappings with names like
affectRelatedMethodsInEnhancement()
for example.
Our class GosuMappingsReplacer replaces the global mappings that the IntelliJ incremental compiler created for its delta with instances of GosuMappings. We extend BuilderService with GosuBuilderService, which creates a new set of builders with GosuMappings instead of Mappings. The GosuBuilder defines a method updateDependencies that defines a delta based on the replaced mappings and calls it in GosuBuilder.doBuild(). If necessary, an additional pass is required.
[Sorry for this wordy and approximate explanation -- setting breakpoints at the affectRelatedMethodsInEnhancement()
class in GosuMappings while trying the simple example is the best way to see what happens.]
I uploaded the version of our Gosu Plugin for JetBrains 2021.1.3 on the JetBrains plugins Marketplace website so you can view in detail how this is all put together. If you require sources, we can supply those.
We welcome your reasonable suggestions as to how we can accomplish this same result in 2021.2, now that Mappings is final and we cannot extend it. [We realize also that in the 2021.2 minor release, you removed some other build methods we depended on as well.]
Please sign in to leave a comment.
I uploaded our OS Gosu plugin to the JetBrains Marketplace website. It is listed as compatible with IDEA 2021.1.3, but has 26 compatibility problems with 2021.2.2. One is the inability to extend your Mappings class. A bunch more have to do with things we used to locate things in our GosuMappings and friends classes, such as ClassRepr.findMethods(), etc., which have apparently been removed.
If you need source, I can get it to you somehow. But if you install our plugin into IJ 2021.1.3 and try the simple example, you can watch what we do in GosuMappings when the method bar() is uncommented and re-commented in A.java and a build gets done.
Our plugin has been approved.
To allow us to track your releases of IntelliJ, we request that Mappings be made non-final and just annotated as non-extensible for 2021.2, pending your suggestions for a possible workaround, so that we can make our plugin work for 2021.2.x (or 2021.3.x, or whatever you choose).
NOTE: Guidewire Software, Inc. has based its Studio implementation on JetBrains' IntelliJ Idea for many years. We have many customers (P&C insurance companies) and we are also supporting a cloud version of our software. Many developers, both within Guidewire itself and at our "on premises" customers' sites, depend on Studio. We release the OS Gosu plugin for developers who wish to program in the Gosu language through IntelliJ Idea, but its more significant use is as part of Guidewire Studio.
We've reverted the final both in 212 and 213.
Thank you. We'll work from there, and see if there are further issues we need help with.
But it looks like whatever build this happened in has not been released? I tried 212.5457.46 but it still has final Mappings.
I see that the current github sources have Mappings reverted to not be final.