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
1 change: 1 addition & 0 deletions .agents/skills/xtend-to-java/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Do **not** invoke for normal Xtend editing where the file is staying in Xtend.
- [`rules/07-lambdas.md`](./rules/07-lambdas.md)
- [`rules/08-operator-overloads.md`](./rules/08-operator-overloads.md)
- [`rules/09-misc-syntax.md`](./rules/09-misc-syntax.md)
- [`rules/10-jvm-model-inferrer.md`](./rules/10-jvm-model-inferrer.md)
5. **Look up Xtend stdlib calls** in [`references/xtend-library-replacements.md`](./references/xtend-library-replacements.md).
6. **Apply the quality checklist** in [`workflow/validation-checklist.md`](./workflow/validation-checklist.md) before declaring done.
7. **Handle module infrastructure** via [`workflow/infrastructure-cleanup.md`](./workflow/infrastructure-cleanup.md) when all Xtend is removed from a module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Use Guava **only** where it is genuinely more concise (marked with ★).
| `IterableExtensions.isEmpty(iter)` | `!iter.iterator().hasNext()` |
| `IterableExtensions.size(iter)` | `(int) StreamSupport.stream(iter.spliterator(), false).count()` or loop |
| `IterableExtensions.toMap(iter, keyFn, valFn)` | `iter.stream().collect(Collectors.toMap(keyFn, valFn))` |
| `IterableExtensions.filterNull(iter)` | `iter.stream().filter(Objects::nonNull).toList()` |
| `IterableExtensions.flatten(iter)` | `iter.stream().flatMap(Collection::stream).toList()` (inner is Collection; for a general Iterable use `flatMap(i -> StreamSupport.stream(i.spliterator(), false))`) |
| `IteratorExtensions.toIterable(iter)` | `(Iterable<T>) () -> iter` (keep as-is for Xtext patterns) |
| `StringExtensions.isNullOrEmpty(s)` | `s == null \|\| s.isEmpty()` |
| `StringExtensions.toFirstUpper(s)` | `Character.toUpperCase(s.charAt(0)) + s.substring(1)` |
Expand Down
40 changes: 40 additions & 0 deletions .agents/skills/xtend-to-java/rules/04-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,43 @@ private static final String TEST_ORA_USER = "TEST.ORA USER";
```

Prefer constants over suppression whenever the repeated string has a meaningful name.

## 4.8 `StringConcatenation` append-chains — coalescing and the `newLineIfNotEmpty()` rule

Migrated generators (and `xtend-gen/` ground truth) build templates on
`org.eclipse.xtend2.lib.StringConcatenation`. When coalescing its append-chains into text blocks,
these source-verified semantics decide what is safe:

1. **`append(String)` normalizes embedded newlines.** Every `\n`/`\r\n` inside an appended string is
replaced by the builder's own line delimiter. Appending one multi-line text block is therefore
byte-equivalent to the original per-line `append(...)` + `newLine()` sequence — this is the formal
basis for coalescing static runs.

2. **`newLineIfNotEmpty()` is line-scoped and whitespace-retracting.** It inspects only the *current
line* (segments since the last delimiter): if it contains non-whitespace, it appends a newline
(identical to `newLine()`); if it is empty or whitespace-only, it **deletes** that trailing
whitespace and appends nothing.

3. **Two-arg `append(value, indent)` re-indents continuation lines** — the indent is prepended to
every line of the value except the first. No text block or one-arg append can reproduce this for
multi-line values.

**The conversion rule:**

- `newLineIfNotEmpty()` → `newLine()` **only when the last append on the current line is a static
non-whitespace literal** (then the guard provably always fires). Typical safe tails: `";"`,
`" {"`, `");"`, `".class)"`.
- **Keep** `newLineIfNotEmpty()` when the line-tail is a **dynamic value** (super-calls, names,
argument lists — if the value ever ends with `\n`, the guard correctly adds nothing where
`newLine()` would inject a blank line), and when it follows an **indent + conditional/possibly-empty
value** (the whitespace retraction is load-bearing: converting keeps the orphaned indent AND adds
a blank line).
- **Never fold** a two-arg `append(value, indent)` of a potentially multi-line value into a text
block or `%s` (drops the continuation re-indent — a confirmed drift class). Folding is safe only
for values that are provably single-line (identifiers, rule names, accessor paths).
- **Leave `appendImmediate(sep, indent)` loops untouched** — the separator insertion inspects
trailing segments; keep its surrounding append sequence as-is.

**Verification:** each coalesced run must be proven byte-identical with an executable old-vs-new
harness over an input battery (empty / single-line / multi-line / newline-terminated / `%`-bearing
values), per the ledger's Method 1.
59 changes: 59 additions & 0 deletions .agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# JVM model inferrer (JvmTypesBuilder)

Xtend model inferrers (`class X extends AbstractModelInferrer` with `def dispatch infer(...)`)
build Java types through the `JvmTypesBuilder` extension DSL. The migrated
`FormatJvmModelInferrer.java` (`com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/`)
is the canonical worked example — read it in full before migrating another inferrer.

## 10.1 Core mapping

| Xtend | Java |
|---|---|
| `@Inject extension JvmTypesBuilder` | `@Inject private JvmTypesBuilder jvmTypesBuilder;` — every extension call becomes explicit (`jvmTypesBuilder.toClass(...)`) |
| `def dispatch infer(X x, IJvmDeclaredTypeAcceptor acceptor, boolean preIndexingPhase)` | `_infer(final X x, final IJvmDeclaredTypeAcceptor acceptor, final boolean isPreIndexingPhase)` + the dispatcher pattern ([`rules/09-misc-syntax.md`](./09-misc-syntax.md) §9.7) |
| `x.toClass(name)` | `jvmTypesBuilder.toClass(x, name)` |
| `acceptor.accept(cls, [ ... ])` | `acceptor.<JvmGenericType>accept(cls, initializer)` where `initializer` is a `Procedure1<JvmGenericType>` (see `FormatJvmModelInferrer.java:182-192`) |
| `members += x` / `superTypes += x` / `annotations += x` | `it.getMembers().add(x)` / `it.getSuperTypes().add(x)` / `it.getAnnotations().add(x)` |
| `x.toMethod(name, type) [ ... ]` | `jvmTypesBuilder.toMethod(x, name, type, initializer)` with a `Procedure1<JvmOperation>` (`:223-235`) |
| `x.toField(name, type) [ ... ]` / `x.toParameter(name, type)` | `jvmTypesBuilder.toField(x, name, type, initializer)` / `jvmTypesBuilder.toParameter(x, name, type)` (`:245`) |
| `typeRef(T)` / `typeRef(name)` | `_typeReferenceBuilder.typeRef(...)` — the protected field inherited from `AbstractModelInferrer` (`:202-204`); for lookups needing a context object use `typeReferences.getTypeForName(name, context)` (`:235`) |
| `documentation = '''...'''` | `jvmTypesBuilder.setDocumentation(it, "...".formatted(...))` (`:198`) |
| `static = true` / `visibility = PROTECTED` / `abstract = true` | `it.setStatic(true)` / `method.setVisibility(JvmVisibility.PROTECTED)` / `it.setAbstract(true)` (`:207,224`) |
| `initializer = expr` (on a field) | set inside the field's initializer `Procedure1` via the corresponding setter/`jvmTypesBuilder` call — read `xtend-gen/` for the exact form |

## 10.2 Method bodies

Xtend assigns bodies two ways; both become `jvmTypesBuilder.setBody(method, ...)`:

- `body = [append('''...''')]` (procedure form) → `Procedure1<ITreeAppendable>` that appends —
the form the migrated file uses throughout:
```java
final Procedure1<ITreeAppendable> body = (final ITreeAppendable appendable) -> {
appendable.append("return (%sGrammarAccess) super.getGrammarAccess();".formatted(...));
};
jvmTypesBuilder.setBody(method, body);
```
(`FormatJvmModelInferrer.java:229-232`)
- `body = '''template'''` (template form) → the Xtend compiler emits the
`StringConcatenationClient` overload of `setBody`. Either keep that overload (check
`xtend-gen/`) or convert to the `Procedure1<ITreeAppendable>` form with the template text
built per [`rules/04-templates.md`](./04-templates.md) — the appended STRING must stay
byte-identical either way.

## 10.3 Gate notes specific to inferrers

- The inference closures are long by design; bracket the class with
`// CHECKSTYLE:CHECK-OFF LambdaBodyLength the model-inference closures mirror the Xtext JvmTypesBuilder API and are kept whole`
(see `FormatJvmModelInferrer.java:114`).
- Emitted Java source fragments are repeated literals — `// CHECKSTYLE:CONSTANTS-OFF` applies
(same file, `:113`).
- `members += list.map(...).flatten.filterNull` chains: see
[`references/xtend-library-replacements.md`](../references/xtend-library-replacements.md)
for `flatten`/`filterNull` stream equivalents; the result feeds `getMembers().addAll(...)`.

## 10.4 Verification

An inferrer is a generator: its OUTPUT (the inferred JVM model, and through it the generated
Java) is the ground truth. Byte-verify emitted body/documentation strings against `xtend-gen/`
exactly as for any template (rules 04/§4.8); structural calls (`toClass`/`toMethod`/setters)
must match the `xtend-gen/` call sequence one-for-one.
46 changes: 34 additions & 12 deletions .agents/skills/xtend-to-java/workflow/formatting-and-commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,44 @@ Select all migrated files in Package Explorer → Source → Format. Then amend

The Eclipse formatter re-indents text block content AND closing `"""` together, preserving indent stripping semantics. Formatted text blocks produce the same string values as unformatted ones — the formatter is safe for text blocks.

## Commit structure

**One single commit** containing everything:
- Migrated `.java` files
- Deleted `.xtend` originals
- Deleted `xtend-gen/` directory (if module fully off Xtend)
- All infrastructure changes (MANIFEST.MF, build.properties, .classpath, .project)

Do not split into multiple commits.
## Commit structure — two-step rename → translate

Structure every migration slice as **two commits plus an optional infrastructure commit**:

1. **Rename commit** — a pure `git mv` of every `.xtend` in the slice to `.java`, content
**unchanged**. Because the rename edge has 100% content similarity, Git's rename detection
always traverses it: `git log --follow` and `git blame` permanently connect the `.java`
history to its `.xtend` past. (Git stores no rename metadata — a combined delete+add commit
only keeps history if the finished Java stays >50% similar to the Xtend source, which heavy
rewrites don't.)
2. **Translate commit** — rewrite the renamed files **in place** to the final Java. Reviewers
get a side-by-side Xtend-vs-Java diff per file on the PR's Commits tab (the paths are
unchanged, so the pairing is guaranteed regardless of similarity). Put the per-file
faithfulness notes (null-semantics decisions, documented deviations) in this commit's body.
3. **Infrastructure commit** (only when the module is now fully off Xtend) — the
[`infrastructure-cleanup.md`](./infrastructure-cleanup.md) changes: build.properties,
.classpath, .project, `xtend-gen/` deletion, MANIFEST check.

Known trade-offs (accepted):
- The rename commit **intentionally does not compile** (Xtend syntax in `.java` files). PR CI
only builds the head, so CI stays green; after a rebase-merge the intermediate commit costs
one `git bisect skip` on master.
- The aggregate **Files-changed tab is unaffected** by the commit structure: it diffs
base…head and pairs old/new only when end-state similarity crosses Git's threshold.
Near-transliterations pair either way; heavy rewrites pair under neither. Point reviewers to
the Commits tab for the guaranteed in-place diff.

Do not split the translate step per file — every intermediate commit before the last file
would be broken anyway, so per-file translate commits only multiply the broken range.

## Commit message format

```
refactor: migrate Xtend to Java - <plugin name>
refactor: migrate Xtend to Java - <plugin name> (1/2: rename sources)
refactor: migrate Xtend to Java - <plugin name> (2/2: translate to Java 21)
refactor: drop Xtend build infrastructure from <plugin name> # when applicable
```

Example: `refactor: migrate Xtend to Java - com.avaloq.tools.ddk.check.core.test`
The rename commit body must state that it is a pure `git mv` and intentionally non-compiling.

PR title: same as commit message.
PR title: `refactor: migrate Xtend to Java - <plugin name>`.
8 changes: 5 additions & 3 deletions .agents/skills/xtend-to-java/workflow/multi-file-batch.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ A red gate means you do not start the next batch. Diagnose first.

### Commit structure

**One single commit per module** containing everything: migrated Java files, deleted `.xtend`
originals, deleted `xtend-gen/` directory (if module is fully off Xtend), and all infrastructure changes.
**Two-step rename → translate per module** (see [`formatting-and-commit.md`](./formatting-and-commit.md)):
a pure `git mv` rename commit for all `.xtend` in the slice, an in-place translate commit, and —
when the module is fully off Xtend — an infrastructure-cleanup commit.

Commit message format:
```
refactor: migrate Xtend to Java - <plugin name>
refactor: migrate Xtend to Java - <plugin name> (1/2: rename sources)
refactor: migrate Xtend to Java - <plugin name> (2/2: translate to Java 21)
```
Example: `refactor: migrate Xtend to Java - com.avaloq.tools.ddk.check.core.test`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Run through this list before declaring a conversion done. Every item is a hard g
| 23 | Template whitespace | Must match `xtend-gen/` output exactly. Never guess from Xtend source. |
| 29 | Never `String.format()` | Use `.formatted()` consistently. |
| 30 | `.formatted()` fallback rules | Fall back to concatenation only when `%s` is ambiguous/unreadable or template has literal `%`. |
| 35 | `newLineIfNotEmpty()` conversion | Convert to `newLine()` only when the line-tail is a static literal; keep after dynamic values and after indent + conditional values (whitespace retraction). Never fold two-arg `append(value, indent)` of multi-line values. See `rules/04-templates.md` §4.8. |

### Annotations and modifiers

Expand Down Expand Up @@ -83,7 +84,7 @@ Run through this list before declaring a conversion done. Every item is a hard g
| # | Rule | Requirement |
|---|------|-------------|
| 21 | Copyright headers | File starts with the exact Avaloq `/**…**/` banner header (see [`formatting-and-commit.md`](./formatting-and-commit.md)). This **replaces** any header the source carried — a `/* generated by Xtext x.y */` stub marker or a `/** … */` Javadoc-style copyright block. Normalising to the banner is **not** "inventing" (rule 1 does not apply to the copyright header). Match a sibling `.java` in the module. |
| 22 | Commit format | `refactor: migrate Xtend to Java - <plugin name>`. Single commit. |
| 22 | Commit format | Two-step: `(1/2: rename sources)` pure `git mv` commit + `(2/2: translate to Java 21)` in-place rewrite commit (+ infra commit when module fully off Xtend). See [`formatting-and-commit.md`](./formatting-and-commit.md). |
| 28 | Infrastructure cleanup | Remove Xtend from MANIFEST.MF, build.properties, .classpath, .project; delete `xtend-gen/` directory (when module fully off Xtend). |

### Migration-campaign gates (learned)
Expand Down