From 9762bdffde00e274c079b1711316693b3e1f0065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Mon, 6 Jul 2026 12:38:59 +0100 Subject: [PATCH 1/3] docs: adopt two-step rename+translate commit structure in xtend-to-java skill Replace the single-commit rule with a rename -> translate structure: a pure git-mv commit (100% similarity, so git log --follow and blame permanently traverse the migration boundary) followed by an in-place translate commit (guaranteed side-by-side Xtend-vs-Java diff on the PR Commits tab), plus the existing infrastructure-cleanup commit when a module goes fully off Xtend. Documents the accepted trade-offs: the rename commit intentionally does not compile (one bisect-skip on master after rebase-merge), and the aggregate Files-changed tab pairing is unaffected by commit structure (it depends only on end-state content similarity). Co-Authored-By: Claude Opus 4.8 --- .../workflow/formatting-and-commit.md | 46 ++++++++++++++----- .../workflow/multi-file-batch.md | 8 ++-- .../workflow/validation-checklist.md | 2 +- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md b/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md index c364710e92..33ea532fd7 100644 --- a/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md +++ b/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md @@ -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 - +refactor: migrate Xtend to Java - (1/2: rename sources) +refactor: migrate Xtend to Java - (2/2: translate to Java 21) +refactor: drop Xtend build infrastructure from # 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 - `. diff --git a/.agents/skills/xtend-to-java/workflow/multi-file-batch.md b/.agents/skills/xtend-to-java/workflow/multi-file-batch.md index add7590e45..72dc1d0699 100644 --- a/.agents/skills/xtend-to-java/workflow/multi-file-batch.md +++ b/.agents/skills/xtend-to-java/workflow/multi-file-batch.md @@ -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 - +refactor: migrate Xtend to Java - (1/2: rename sources) +refactor: migrate Xtend to Java - (2/2: translate to Java 21) ``` Example: `refactor: migrate Xtend to Java - com.avaloq.tools.ddk.check.core.test` diff --git a/.agents/skills/xtend-to-java/workflow/validation-checklist.md b/.agents/skills/xtend-to-java/workflow/validation-checklist.md index 04f8773524..18cc88fc0a 100644 --- a/.agents/skills/xtend-to-java/workflow/validation-checklist.md +++ b/.agents/skills/xtend-to-java/workflow/validation-checklist.md @@ -83,7 +83,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 - `. 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) From c46ae14e3f29b23a86eeed1f01c170c41d27bd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 11 Jul 2026 18:11:21 +0200 Subject: [PATCH 2/3] docs: encode the newLineIfNotEmpty conversion rule in the xtend-to-java skill Adds rules/04-templates.md section 4.8 documenting the source-verified StringConcatenation semantics that govern coalescing append-chains into text blocks: append(String) newline normalization (the formal basis for coalescing), the line-scoped whitespace-retracting behaviour of newLineIfNotEmpty(), and the two-arg append(value, indent) continuation re-indent. The conversion rule: newLineIfNotEmpty() may become newLine() only when the last append on the current line is a static non-whitespace literal; it must stay after dynamic line-tails and after indent + conditional values; two-arg appends of potentially multi-line values are never folded; appendImmediate loops stay untouched. Validation checklist gains row 35 enforcing the rule. Distilled from the #1429 round-3 review rework, where these distinctions were byte-verified against an executable harness and an adversarial per-hunk review. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018ANGM7L3BaEGfGUmPVKRt4 --- .../xtend-to-java/rules/04-templates.md | 40 +++++++++++++++++++ .../workflow/validation-checklist.md | 1 + 2 files changed, 41 insertions(+) diff --git a/.agents/skills/xtend-to-java/rules/04-templates.md b/.agents/skills/xtend-to-java/rules/04-templates.md index 0495bf537c..58233cab13 100644 --- a/.agents/skills/xtend-to-java/rules/04-templates.md +++ b/.agents/skills/xtend-to-java/rules/04-templates.md @@ -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. diff --git a/.agents/skills/xtend-to-java/workflow/validation-checklist.md b/.agents/skills/xtend-to-java/workflow/validation-checklist.md index 18cc88fc0a..03c1804b51 100644 --- a/.agents/skills/xtend-to-java/workflow/validation-checklist.md +++ b/.agents/skills/xtend-to-java/workflow/validation-checklist.md @@ -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 From 6b303ae218011dc3e77653ca48eb4455596573e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sat, 11 Jul 2026 21:49:36 +0200 Subject: [PATCH 3/3] docs: add JVM model inferrer rule and missing replacement rows Adds rules/10-jvm-model-inferrer.md covering the JvmTypesBuilder DSL that the remaining inferrer migrations (CheckJvmModelInferrer and the check generators) are built on: extension-to-explicit-field mapping, dispatch infer, toClass/toMethod/toField/toParameter with Procedure1 initialisers, typeRef via the inherited _typeReferenceBuilder, property-assignment setters, both setBody forms, and the inferrer-specific gate notes (LambdaBodyLength/CONSTANTS-OFF). Anchored to the already-migrated FormatJvmModelInferrer.java with verified line references. Also adds the two missing iterable rows to the replacements table (filterNull, flatten - both recur across the remaining .xtend inventory) and links rule 10 from SKILL.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018ANGM7L3BaEGfGUmPVKRt4 --- .agents/skills/xtend-to-java/SKILL.md | 1 + .../references/xtend-library-replacements.md | 2 + .../rules/10-jvm-model-inferrer.md | 59 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md diff --git a/.agents/skills/xtend-to-java/SKILL.md b/.agents/skills/xtend-to-java/SKILL.md index e5b9c72697..73fe174164 100644 --- a/.agents/skills/xtend-to-java/SKILL.md +++ b/.agents/skills/xtend-to-java/SKILL.md @@ -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. diff --git a/.agents/skills/xtend-to-java/references/xtend-library-replacements.md b/.agents/skills/xtend-to-java/references/xtend-library-replacements.md index 176e50d38c..330ecc37fb 100644 --- a/.agents/skills/xtend-to-java/references/xtend-library-replacements.md +++ b/.agents/skills/xtend-to-java/references/xtend-library-replacements.md @@ -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) () -> 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)` | diff --git a/.agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md b/.agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md new file mode 100644 index 0000000000..bed5baeb42 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md @@ -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.accept(cls, initializer)` where `initializer` is a `Procedure1` (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` (`: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` that appends — + the form the migrated file uses throughout: + ```java + final Procedure1 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` 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.