Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .agents/skills/xtend-to-java/rules/09-misc-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,46 @@ Write the accessor methods explicitly with the specified visibility.

Generates a constructor taking all final fields as parameters. Write it manually.

### `@Tag`

Assigns sequential integers (starting at `TagCompilationParticipant.COUNTER_BASE = 10000`) to test
marker fields at Xtend compile time via `TagCompilationParticipant`.

Standard Java APT (`AbstractProcessor`) **cannot** replicate this — it can only generate new source
files, not modify initializers of existing fields. Use `TagExtension` instead: a JUnit 5
`BeforeEachCallback` in `com.avaloq.tools.ddk.xtext.test.core` that performs the same sequential
assignment at runtime via reflection.

**Migration steps:**

1. Declare each `@Tag` field as `private int` — **no `final`**, **no explicit initializer**:
```java
@Tag
private int MY_TAG;
```
2. Add `TagExtension.class` to `@ExtendWith` (alongside any existing extensions):
```java
@ExtendWith({InjectionExtension.class, TagExtension.class})
```
3. Add the import:
```java
import com.avaloq.tools.ddk.xtext.test.TagExtension;
```

**Why not `final`?** `TagExtension` uses `Field.setInt()` to write the value at runtime.
`Field.setInt()` on a `final` field throws `IllegalAccessException` even after `setAccessible(true)`
on Java 9+. Formatters and IDE save-actions routinely add `final` to `int` fields — always remove it
for `@Tag` fields.

**Why not explicit initializers?** The extension assigns values in declaration order starting from
`COUNTER_BASE`. Hardcoded values are redundant and will silently drift out of sync when fields are
reordered or inserted.

**Field ordering matters.** `TagExtension` iterates `getDeclaredFields()` in source-declaration order
(guaranteed by HotSpot; JVM spec does not mandate it, but all production JVMs preserve it).
Inserting a new `@Tag` field in the middle shifts subsequent values — exactly as the Xtend active
annotation would have done.

## 9.7 Dispatch methods

Xtend's multi-dispatch (`def dispatch ...`) has no Java equivalent. Convert to: individual `protected` methods prefixed with `_` plus a single dispatcher method that does the type-test routing.
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/xtend-to-java/workflow/known-pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Consolidated table of common mistakes and their fixes. Review before and after e
| **Missing `@throws` tags** | When Java migration adds `throws` and method already has Javadoc, Checkstyle requires `@throws`. Add it; don't create Javadoc just for the tag. |
| **Duplicate string literals** | Checkstyle flags strings appearing 2+ times. In tests, extract to constants. In generators, use `CHECKSTYLE:CONSTANTS-OFF/ON`. |
| **`@Data` / `@Accessors`** | These generate code at compile time. The `xtend-gen/` output shows exactly what — copy equals/hashCode/toString/getters from there. |
| **`@Tag` fields must not be `final`** | `TagExtension` assigns tag values via `Field.setInt()` at runtime. `Field.setInt()` on a `final` field fails on Java 9+ even after `setAccessible(true)`. IDE formatters and save-actions silently add `final` to `int` fields — always strip it from `@Tag` fields. See [`rules/09-misc-syntax.md`](../rules/09-misc-syntax.md) §9.6. |
| **`BasicEList` in generic code** | Needs explicit type parameter — `new BasicEList<X>()`. |
| **StringBuilder in `xtend-gen/`** | If `xtend-gen/` has `StringConcatenation` but Xtend has a template, that's the signal to use text block or `.formatted()` (tier 1–3) or `StringBuilder` (tier 4). |
| **Non-parameterized logging** | Xtend files often have `"msg" + x` in log calls. Fix to `{}` placeholders. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
*******************************************************************************/
package com.avaloq.tools.ddk.xtext.test

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import org.eclipse.xtend.lib.macro.Active

/**
Expand All @@ -18,6 +20,7 @@ import org.eclipse.xtend.lib.macro.Active
* Usage example: @Tag int MEM_DOC
*/
@Active(typeof(TagCompilationParticipant))
@Retention(RetentionPolicy.RUNTIME)
annotation Tag {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*******************************************************************************
* Copyright (c) 2016 Avaloq Group AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Avaloq Group AG - initial API and implementation
*******************************************************************************/
package com.avaloq.tools.ddk.xtext.test;

import java.lang.reflect.Field;

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
* JUnit Jupiter extension that replicates the compile-time behaviour of the {@link Tag} active annotation in plain Java.
* <p>
* When Xtend processes a class containing {@link Tag}-annotated fields, {@link TagCompilationParticipant} assigns
* sequential integer values starting from {@link TagCompilationParticipant#COUNTER_BASE} to those fields at compile
* time. This extension performs the equivalent assignment at runtime, immediately before each test method, so that
* plain Java test classes can use {@link Tag} without the Xtend compiler.
* </p>
* <p>
* Fields are processed in their declaration order. The counter is reset to {@link TagCompilationParticipant#COUNTER_BASE}
* for each test instance, matching the per-class scope of the Xtend compile-time transformation.
* </p>
* <p>
* Usage: add {@code @ExtendWith(TagExtension.class)} to the test class. The annotated fields must not be
* {@code final}.
* </p>
*/
public class TagExtension implements BeforeEachCallback {

/**
* Assigns sequential integer values to all {@link Tag}-annotated fields declared on the test class.
* <p>
* Only fields declared directly on the test class are processed, not inherited ones. This matches
* the per-class scope of {@link TagCompilationParticipant}.
* </p>
*
* @param context
* the current extension context, must not be {@code null}
* @throws IllegalAccessException
* if a {@link Tag}-annotated field cannot be written via reflection
*/
@Override
public void beforeEach(final ExtensionContext context) throws IllegalAccessException {
Object testInstance = context.getRequiredTestInstance();
int counter = TagCompilationParticipant.COUNTER_BASE;
for (Field field : testInstance.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Tag.class)) {
field.setAccessible(true);
field.setInt(testInstance, counter++);
}
}
}
}