From bb47e9d756054545cee547b96b611596e9dca2a0 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] 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 04f8773524..bb73b512a5 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