apache / apache/grails-core

Grails 7 plugins whose AST transformation targets a private trait method silently lose it on Grails 8

Open
#16,126 0 comments 0 reactions 0 assignees View on GitHub
relates-to: external-plugin
Dominant language
Groovy
Stars
2.9k
Forks
975
Avg merge
1d 22h
Merged PRs (30d)
92

Description

### 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);
private static T getConfigProperty(java.lang.Class, java.lang.String, java.lang.Class, 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
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.

Contributor guide

Open the contributing guide

Research direction

Reproduce the failure in a stock Grails 8.0.0-M5 web app with org.grails.plugins:i18n-enums:7.0.1 and inspect the annotated enum with javap. Read I18nEnumTransformation and I18nEnumTrait.groovy:72, then compare the trait method visible during transformation with the emitted class. Done means the private method remains reachable or the mismatch is reported during compilation rather than at message resolution.

Written by the indexing model from the issue text.

Assessment

Tech stack
groovy
Domain
backend, compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.