jamesfredley opened a new issue, #16126:
URL: https://github.com/apache/grails-core/issues/16126
### Summary
A Grails 7 plugin whose AST transformation populates a **private trait
method** on the annotated class silently produces a class that is missing that
method under Grails 8 / Groovy 5. Compilation succeeds, the plugin loads, and
the failure only appears at runtime as a `MissingPropertyException` naming an
internal member the user never wrote.
Found while auditing released Grails 7 plugins against `8.0.0-M5`, using
`org.grails.plugins:i18n-enums:7.0.1` (latest release, catalog constraint
`7.0.0 > *`).
This is **not** #16123. That issue is about a *generic trait with fields*
failing with `MalformedParameterizedTypeException` through
`$Trait$FieldHelper`. The trait here is not generic, declares no fields, and
ships **no** `$Trait$FieldHelper` at all. The mechanism and the symptom are
different, so I am filing it separately rather than commenting there.
### Grails Version
8.0.0-M5 (Groovy 5.0.8, Spring Boot 4.1.0, JDK 21.0.11)
### Steps to Reproduce
In a stock `create-app --profile=web` application on 8.0.0-M5:
```groovy
implementation "org.grails.plugins:i18n-enums:7.0.1"
```
```groovy
package i18nenums
import grails.plugins.i18nEnums.annotations.I18nEnum
@I18nEnum
enum DeliveryStatus {
SHIPPED
}
```
with `i18nenums.DeliveryStatus.SHIPPED=Shipped` in `messages.properties`,
then resolve it through the documented public API:
```groovy
messageSource.getMessage(DeliveryStatus.SHIPPED, Locale.ENGLISH)
```
### Actual Behaviour
Compilation succeeds and the plugin loads (`i18nEnums (7.0.1)` appears in
the load order). The failure happens at runtime:
```text
groovy.lang.MissingPropertyException: No such property: i18nEnumASTConfig
for class: i18nenums.DeliveryStatus
at
grails.plugins.i18nEnums.traits.I18nEnumTrait$Trait$Helper.getConfigProperty(I18nEnumTrait.groovy:72)
at
grails.plugins.i18nEnums.traits.I18nEnumTrait$Trait$Helper.getCodes(I18nEnumTrait.groovy:18)
at
org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:168)
```
No Grails-owned frame appears; the frames are plugin-owned and Spring-owned.
### Root Cause
The plugin's `I18nEnumTransformation` does not add a new field. It **looks
up an existing method on the annotated class and replaces its body**:
```text
private static void addi18nEnumASTConfig(ClassNode, AnnotationNode);
...
25: aload_0
26: ldc // String getI18nEnumASTConfig
32: invokevirtual ClassNode.getMethod:(String, Parameter[]) -> MethodNode
...
85: invokevirtual MethodNode.setCode:(Statement) // installs the config
map as the method body
```
That method is declared **private in the trait**, so it exists only as a
helper static:
```text
grails.plugins.i18nEnums.traits.I18nEnumTrait$Trait$Helper
private static java.util.Map
getI18nEnumASTConfig(java.lang.Class<I18nEnumTrait>);
private static <T> T getConfigProperty(java.lang.Class<I18nEnumTrait>,
java.lang.String, java.lang.Class<T>, T);
```
The compiled application enum on Grails 8 receives the trait's **public**
members but **not** the private one:
```text
$ javap -p build/classes/groovy/main/i18nenums/DeliveryStatus.class
public final class i18nenums.DeliveryStatus extends
java.lang.Enum<i18nenums.DeliveryStatus>
implements grails.plugins.i18nEnums.traits.I18nEnumTrait,
groovy.lang.GroovyObject {
public java.lang.Object[] getArguments();
public java.lang.String[] getCodes();
public java.lang.String getDefaultMessage();
public java.lang.String getName();
...
}
```
`getI18nEnumASTConfig` is absent from the emitted class, so the runtime
property lookup for `i18nEnumASTConfig` finds nothing and every message
resolution through the plugin fails.
Notably the transformation did **not** fail: `MethodNode.setCode` is invoked
unconditionally on the result of `getMethod(...)`, so a `null` lookup would
have produced a `NullPointerException` during compilation. Compilation was
clean, which means the method node was visible to the transform at AST time but
is not present in the class that is finally emitted.
### Expected Behaviour
Either the private trait method remains reachable on the implementing class
the way it was under the Groovy 4 line, or the mismatch surfaces at **compile**
time rather than as a runtime `MissingPropertyException` that names an internal
member.
### Why this is worth tracking here
The user-visible failure gives no indication of the real problem. `No such
property: i18nEnumASTConfig` names a member the application author never wrote,
on a class they declared with a single annotation, and it appears only when the
message is resolved - potentially far from the annotated enum.
Alongside #16122, #16123, #16124, and #16125, this is a fourth distinct way
a released Grails 7 plugin breaks on Grails 8, and the second traced to a
Groovy 4 -> 5 trait ABI change. Even if the fix belongs in the plugin, the
diagnosis cost here is high and a compile-time signal (or an upgrade-guide note
about private trait members and AST transformations) would remove most of it.
### Notes
Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5.
In the same sweep, `greenmail:7.0.1`, `markdown:4.0.0`, `rendering:7.0.1`,
`x-frame-options:2.0.0`, `grails-csrf:2.0.0`, `joda-time:3.0.0`,
`taggable:7.0.0`, `cascade-validation:7.0.1`, `grails-cache-guava:7.0.0`, and
`grails-postgresql-extensions:8.0.0` all passed verification against their
documented public APIs.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]