diff --git a/.agents/skills/xtend-to-java/SKILL.md b/.agents/skills/xtend-to-java/SKILL.md index aa6b8589b..13847ea12 100644 --- a/.agents/skills/xtend-to-java/SKILL.md +++ b/.agents/skills/xtend-to-java/SKILL.md @@ -126,8 +126,8 @@ Use this table for quick mechanical transforms. Full details in the rule files. | `#{a, b, c}` (immutable set) | `Set.of(a, b, c)` | | `newArrayList(...)` | `new ArrayList<>(List.of(...))` | | `newHashMap(...)` | `new HashMap<>(Map.of(...))` | -| `list += element` | `list.add(element)` | -| `list += otherList` | `list.addAll(otherList)` | +| `list += element` | `list.add(element)` for an ordinary collection; an inferrer's `EList` may bind to null-skipping `JvmTypesBuilder.operator_add` — see [`rules/10`](rules/10-jvm-model-inferrer.md) §10.4 | +| `list += otherList` | `list.addAll(otherList)` for an ordinary collection; preserve the same inferrer null-skip exception | | `list -= element` | `list.remove(element)` | ### Extension methods diff --git a/.agents/skills/xtend-to-java/examples/00-basic-generator.md b/.agents/skills/xtend-to-java/examples/00-basic-generator.md index 00c7ded08..1cc7e94b0 100644 --- a/.agents/skills/xtend-to-java/examples/00-basic-generator.md +++ b/.agents/skills/xtend-to-java/examples/00-basic-generator.md @@ -1,6 +1,6 @@ # Example: basic generator conversion -A small generator with `@Inject extension`, `override`, null-safe navigation, `typeof`, template expression with `«FOR»` and `«IF»`, and a `static extension` import. Touches rules 01, 02, 03, 04, 06, 08, and 09. +A small generator with `@Inject extension`, `override`, null-safe navigation, `typeof`, template expression with `«FOR»` and `«IF»`, and a `static extension` import. Touches rules 01, 02, 03, 04, 06, 08, and 09. Assume the illustrative `BaseGenerator` declares `doGenerate(Resource)`. ## Xtend input @@ -12,7 +12,7 @@ import org.eclipse.emf.ecore.resource.Resource import static org.eclipse.xtext.xbase.lib.IteratorExtensions.* import static extension com.example.NamingExtensions.* -class MyGenerator { +class MyGenerator extends BaseGenerator { @Inject extension MyHelper helper override void doGenerate(Resource resource) { @@ -50,7 +50,7 @@ import com.google.common.collect.Iterables; import com.google.inject.Inject; @SuppressWarnings("nls") -public class MyGenerator { +public class MyGenerator extends BaseGenerator { @Inject private MyHelper helper; 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 330ecc37f..6ad4f69ed 100644 --- a/.agents/skills/xtend-to-java/references/xtend-library-replacements.md +++ b/.agents/skills/xtend-to-java/references/xtend-library-replacements.md @@ -9,7 +9,7 @@ Use Guava **only** where it is genuinely more concise (marked with ★). | `IterableExtensions.filter(iter, fn)` | `iter.stream().filter(fn).toList()` | | `IterableExtensions.filter(iter, Type.class)` | ★ `Iterables.filter(iter, Type.class)` (Guava — type-safe, no cast needed) | | `IterableExtensions.toList(iter)` | `StreamSupport.stream(iter.spliterator(), false).toList()` or loop | -| `IterableExtensions.toSet(iter)` | `StreamSupport.stream(iter.spliterator(), false).collect(Collectors.toSet())` | +| `IterableExtensions.toSet(iter)` | `StreamSupport.stream(iter.spliterator(), false).collect(Collectors.toCollection(LinkedHashSet::new))` — preserves first-encounter order; if aliasing matters, note that Xtend returns `iter` unchanged when it is already a `Set` | | `IterableExtensions.head(iter)` | ★ `Iterables.getFirst(iter, null)` (Guava — null-safe one-liner) | | `IterableExtensions.join(iter, sep)` | `String.join(sep, iter)` (if `Iterable`) or `StreamSupport.stream(...).map(Object::toString).collect(Collectors.joining(sep))` | | `IterableExtensions.join(iter, sep, fn)` | `iter.stream().map(fn).collect(Collectors.joining(sep))` | diff --git a/.agents/skills/xtend-to-java/rules/03-methods.md b/.agents/skills/xtend-to-java/rules/03-methods.md index 055c9bd29..1ea601166 100644 --- a/.agents/skills/xtend-to-java/rules/03-methods.md +++ b/.agents/skills/xtend-to-java/rules/03-methods.md @@ -62,10 +62,16 @@ public SomeType foo() { ## 3.5 Checked exceptions -Xtend doesn't enforce checked exceptions. Java does. When the body calls APIs that throw checked exceptions: - -- Add `throws ...` to the method signature, **or** -- Wrap in `try`/`catch`. -- Common cases: `CoreException` from Eclipse APIs, `IOException` from I/O. - -Catch specific exceptions — never generic `Exception`. See the quality checklist for IllegalCatch. +Xtend doesn't enforce checked exceptions. Java does. Treat that mismatch as an API-design decision rather +than copying the compiler's workaround: + +- keep explicit catches specific, and catch only the narrow checked types Java requires; +- declare the exact checked exception when the method is private/package-local or its inherited API permits it; +- when an override or compatibility-sensitive public API cannot declare the exception, stop and obtain explicit + review for the boundary strategy (for example, an established project-specific unchecked exception), preserving + the original exception as the cause. + +Common cases are `CoreException` from Eclipse APIs and `IOException` from I/O. Never introduce an arbitrary +wrapper merely to make the migration compile. See [`rules/05-control-flow.md`](./05-control-flow.md) §5.5 and +the quality checklist. Never copy `xtend-gen`'s broad `catch (Throwable)` scaffold unless the invoked API itself +declares `Throwable` and no narrower catch can compile. diff --git a/.agents/skills/xtend-to-java/rules/05-control-flow.md b/.agents/skills/xtend-to-java/rules/05-control-flow.md index 2789f207d..86446286a 100644 --- a/.agents/skills/xtend-to-java/rules/05-control-flow.md +++ b/.agents/skills/xtend-to-java/rules/05-control-flow.md @@ -54,3 +54,27 @@ final String label = x != null ? x.getName() : ""; ``` For multi-line bodies, factor to a helper method or write `if`/`else` with an assignment in each branch. + +## 5.5 Exception handling — preserve behaviour without broad catches + +Xtend hides checked exceptions. Its generated Java may contain broad exception-handling scaffolding, but that is +**not a migration template**. Write an explicit Java exception contract using the narrowest types that compile: + +- Keep every explicit Xtend `catch (SpecificException e)` as the same specific Java catch. Non-matching + exceptions and errors already propagate unchanged without being caught. +- If an uncaught checked exception can be declared without violating an override or compatibility-sensitive + public API, add that exact exception to the `throws` clause and let it propagate normally. +- If the method cannot declare it, do not choose a workaround mechanically. Obtain explicit review for the + boundary strategy. When the project has an established unchecked counterpart (for example, + `UncheckedIOException` or Xtext's `RuntimeIOException`), catch only the precise checked type at the smallest + scope and preserve it as the cause. +- **Do not catch `Throwable`, `Exception`, or `RuntimeException` merely to copy `xtend-gen`.** A broad catch is + permitted only when the invoked API itself declares that exact broad type, no narrower compiler-visible + catch can compile, and preserving the public signature is required. Keep that exceptional catch as small + as possible and justify the narrow `@SuppressWarnings("checkstyle:IllegalCatch")` at the site. +- Adding a checked `throws` clause to a compatibility-sensitive public API is an API change and requires explicit + review. +- **Do not invent an arbitrary wrapper.** `new RuntimeException(e)` / `new IllegalStateException(e)` changes the + exception contract and is not a neutral migration. Use an established domain-specific counterpart only after + explicit review, and always preserve the caught exception as the cause. (A legitimate + `throw new IllegalStateException("message")` for a genuinely bad state, with no caught cause, is unrelated.) diff --git a/.agents/skills/xtend-to-java/rules/08-operator-overloads.md b/.agents/skills/xtend-to-java/rules/08-operator-overloads.md index ae1c3a71e..67365dd4f 100644 --- a/.agents/skills/xtend-to-java/rules/08-operator-overloads.md +++ b/.agents/skills/xtend-to-java/rules/08-operator-overloads.md @@ -58,5 +58,9 @@ Same in Java. - `list += element` → `list.add(element)` - `list += otherList` → `list.addAll(otherList)` +- ⚠ **Exception**: when the receiver is an `EList` and `JvmTypesBuilder` is an in-scope extension + (every JVM model inferrer), `+=` binds to `JvmTypesBuilder.operator_add`; **both overloads skip nulls** + (and also no-op on a null list). A plain `add`/`addAll` is then NOT faithful — see + [`rules/10-jvm-model-inferrer.md`](./10-jvm-model-inferrer.md) §10.4 before translating any `+=` in an inferrer. - `list -= element` → `list.remove(element)` - `map[key]` (Xtend bracket access) → `map.get(key)` diff --git a/.agents/skills/xtend-to-java/rules/09-misc-syntax.md b/.agents/skills/xtend-to-java/rules/09-misc-syntax.md index 5ec959665..b347924ea 100644 --- a/.agents/skills/xtend-to-java/rules/09-misc-syntax.md +++ b/.agents/skills/xtend-to-java/rules/09-misc-syntax.md @@ -170,3 +170,25 @@ Rules: - **Copy Javadoc from the Xtend source verbatim.** Never generate, guess, or infer Javadoc that was not in the original. Invented comments are misleading. - **`@throws` tags**: Only add when (1) the method already has Javadoc AND (2) the migrated signature declares a `throws` clause. Do not add Javadoc just to host a `@throws` tag. - Do **not** add `@SuppressWarnings("all")` — the Xtend compiler injects this into `xtend-gen/`; human-converted Java shouldn't have it. + +## 9.11 Charset — verify the contract before deviating from `xtend-gen` + +When the Xtend source constructs a reader/writer with **no charset** (`new InputStreamReader(stream)`, +`new String(bytes)`, `.getBytes()`), `xtend-gen` faithfully reproduces the **platform-default** charset. +Do not mechanically reproduce it. PMD `RelianceOnDefaultCharset` flags the *implicit* default — an explicit +`Charset.defaultCharset()` would pass the gate, but it usually keeps unintended platform dependence. Determine +the **data contract** first: + +- if the API or format supplies an encoding, honour it — e.g. pass `file.getCharset()` when reading an Eclipse + `IFile` (the `InputStreamReader(InputStream, String)` overload accepts that value); +- for repository-owned text governed by `ddk-parent/pom.xml`'s UTF-8 project encoding, use + `java.nio.charset.StandardCharsets.UTF_8`: + `new InputStreamReader(stream, StandardCharsets.UTF_8)`; +- for opaque external data with no documented encoding, do **not** guess UTF-8 from the Java source-encoding + setting. Establish the contract. If platform-default encoding is genuinely part of that contract, use + `Charset.defaultCharset()` explicitly and record why preserving it is intentional. + +This is a sanctioned divergence from `xtend-gen` only when the chosen charset follows a verified contract; +call it out and test it. Do not use a lint warning as blanket permission for an unrelated behaviour change. +Two legacy `// NOPMD` suppressions of this rule exist in hand-written code (`CheckPreferencesHelper`, +`XtextGMFResourceUtil`); they are grandfathered, not a precedent for migrations. 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 index bed5baeb4..8cf4611ca 100644 --- a/.agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md +++ b/.agents/skills/xtend-to-java/rules/10-jvm-model-inferrer.md @@ -12,13 +12,13 @@ is the canonical worked example — read it in full before migrating another inf | `@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`) | +| `acceptor.accept(cls, [ ... ])` | `acceptor.accept(cls, initializer)` where `initializer` is a `Procedure1` (see `FormatJvmModelInferrer._infer`) | +| `members += x` / `superTypes += x` / `annotations += x` | `it.getMembers().add(x)` / … — **only when `x` is provably non-null**; both `JvmTypesBuilder.operator_add` overloads skip nulls (single element and collection), so see §10.4 before translating any `+=` | +| `x.toMethod(name, type) [ ... ]` | `jvmTypesBuilder.toMethod(x, name, type, initializer)` with a `Procedure1` (see `FormatJvmModelInferrer.inferGetGrammarAccess`) | +| `x.toField(name, type) [ ... ]` / `x.toParameter(name, type)` | `jvmTypesBuilder.toField(x, name, type, initializer)` / `jvmTypesBuilder.toParameter(x, name, type)` | +| `typeRef(T)` / `typeRef(name)` | `_typeReferenceBuilder.typeRef(...)` — the protected field inherited from `AbstractModelInferrer`; for lookups needing a context object use `typeReferences.getTypeForName(name, context)` | +| `documentation = '''...'''` | `jvmTypesBuilder.setDocumentation(it, "...".formatted(...))` (see `FormatJvmModelInferrer.inferClass`) | +| `static = true` / `visibility = PROTECTED` / `abstract = true` | `it.setStatic(true)` / `method.setVisibility(JvmVisibility.PROTECTED)` / `it.setAbstract(true)` | | `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 @@ -33,7 +33,7 @@ Xtend assigns bodies two ways; both become `jvmTypesBuilder.setBody(method, ...) }; jvmTypesBuilder.setBody(method, body); ``` - (`FormatJvmModelInferrer.java:229-232`) + (see `FormatJvmModelInferrer.inferGetGrammarAccess`) - `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 @@ -44,14 +44,72 @@ Xtend assigns bodies two ways; both become `jvmTypesBuilder.setBody(method, ...) - 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`). + (see the class-level suppression in `FormatJvmModelInferrer`). - Emitted Java source fragments are repeated literals — `// CHECKSTYLE:CONSTANTS-OFF` applies - (same file, `:113`). + (see the class-level suppression in `FormatJvmModelInferrer`). - `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(...)`. + for `flatten`/`filterNull` stream equivalents; the result feeds the add — but read §10.4 first + for the null-skip requirement, which is the most dangerous inferrer-migration trap. -## 10.4 Verification +## 10.4 ⚠ `operator_add` (`+=`) SKIPS nulls — plain `add`/`addAll` does NOT + +**This is the highest-risk inferrer defect: it passes every static gate (PMD/Checkstyle/SpotBugs) and +every test that does not happen to feed a null — and then fails at runtime the moment one does.** +The failure is fast and loud, not silent: JVM model containment lists (`getMembers()` etc.) are EMF +`EObjectEList`s with `canContainNull() == false`, so a bare `.add(null)`/`.addAll(...)` throws +`IllegalArgumentException("The 'no null' constraint is violated")` **at the add call**. Xtend's `+=` +never produces that null add in the first place — that is the behaviour a faithful migration must keep. + +`JvmTypesBuilder` provides **two** `operator_add` overloads, and **both skip nulls** (they also no-op on +a null list): `operator_add(EList, T)` is `if (list != null && element != null) list.add(element)`, +and the `Iterable` overload delegates to it per element. So the trap covers the single-element form too: +Xtend `members += toField(...)` silently skips a null factory result, while the doc-obvious +`it.getMembers().add(toField(...))` throws on it. + +And the factories DO return null. In Xtext 2.43.0, named builders such as `toClass`, `toInterface`, +`toAnnotationType`, `toEnumerationType`, `toField`, `toMethod`, `toParameter`, and +`toEnumerationLiteral` guard their **source element and name**; `toConstructor` guards its source element; +and `toGetter` / `toSetter` guard the source element plus their accessor/field names. A null **type** +argument does not by itself trigger a null return in the field/method/parameter/accessor builders. Check the +exact overload used rather than treating this list as a substitute for source inspection. Any local helper +with a `return null` fall-through (a `switch`/`if` that doesn't match) is a trigger too. + +So the faithful Java of any `+=` whose right-hand side can be null is a guarded add: + +```java +// WRONG — throws IllegalArgumentException("The 'no null' constraint is violated") at the add +// the first time createConstant returns null (value-less constant): +for (final Constant c : constants) { + it.getMembers().add(createConstant(format, c)); +} + +// RIGHT — reproduce operator_add's null-skip (either form): +for (final Constant c : constants) { + final JvmMember member = createConstant(format, c); + if (member != null) { + it.getMembers().add(member); + } +} +// or, matching the Xtend chain shape with the JDK stream equivalents +// (per references/xtend-library-replacements.md — no xbase.lib in migrated Java): +it.getMembers().addAll(constants.stream() + .map(c -> createConstant(format, c)) + .filter(Objects::nonNull) + .toList()); +``` + +**Checklist for every `+=` site in a migrated inferrer — single element or collection:** can the producer +return null (nullable source element or name, or a `return null` branch)? If yes, there MUST be a null +guard / `Objects::nonNull` filter. A bare `add`/`addAll` over a null-capable producer is a faithfulness +regression. + +> Real shipped example: `FormatJvmModelInferrer.inferConstants` used a bare add for +> `members += allConstants.map[createConstant]`, although `createConstant` returns null for a value-less +> constant. The guard now in that method and its regression test are the canonical fix; the defect escaped +> the gates because no earlier test supplied the null-producing input. + +## 10.5 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/` diff --git a/.agents/skills/xtend-to-java/workflow/known-pitfalls.md b/.agents/skills/xtend-to-java/workflow/known-pitfalls.md index 16fa0cde3..a5efda4bd 100644 --- a/.agents/skills/xtend-to-java/workflow/known-pitfalls.md +++ b/.agents/skills/xtend-to-java/workflow/known-pitfalls.md @@ -10,7 +10,7 @@ Consolidated table of common mistakes and their fixes. Review before and after e | **Implicit `it` in lambdas** | Xtend lambdas with no declared parameter use an implicit `it`. In Java, name it explicitly. | | **Implicit returns** | Xtend methods return the last expression. The compiler catches missing returns but not wrong ones. | | **Property access vs getter** | Xtend `obj.name` may call `getName()`. In Java, write `obj.getName()` explicitly. Check `xtend-gen/` if unsure. | -| **`CoreException` handling** | Xtend silently wraps checked exceptions. Java doesn't. Add explicit `try/catch` — the `xtend-gen/` file shows what was generated. | +| **Checked-exception handling** | Xtend may let a checked exception escape without declaring it. Do not copy its generated exception-handling scaffolding. Declare the exact checked type when compatible; otherwise obtain explicit review for a project-established boundary strategy and preserve the cause. See [`rules/05-control-flow.md`](../rules/05-control-flow.md) §5.5. | | **Invented Javadoc** | Never add **class/member Javadoc** that wasn't in the original. This is a migration, not a rewrite. (The file copyright header is the one exception — see next row — it is always normalised, not preserved.) | | **Generated supertypes live in `src-gen/`, not `xtend-gen/`** | When the class extends/overrides a generated `Abstract*` base (Module/Setup/runtime/UI), read that base in `src-gen/` (committed, present without a build — unlike `xtend-gen/`) for inherited constructor signatures, the real `@Override` targets, and the return types Xtend inferred. Don't guess the supertype API. | | **Copyright header ≠ "preserve original"** | Always normalise to the Avaloq banner, replacing whatever the source had — see [`formatting-and-commit.md`](./formatting-and-commit.md) §Copyright header. | @@ -24,7 +24,7 @@ Consolidated table of common mistakes and their fixes. Review before and after e | **IDE save actions** | "Organize Imports" in Eclipse may trigger save actions that auto-convert string concatenation to text blocks. Auto-conversion produces wrong results. Review `git diff` after any IDE action. | | **Import order** | See [`rules/01-imports-and-package.md`](../rules/01-imports-and-package.md) for the canonical order. Not enforced by checkstyle (no `ImportOrder` module); wrong order is diff churn only — `com.avaloq.*` precedes `com.google.*`. | | **Eclipse CLI formatter** | Does NOT organize imports — only code formatting. Import order must be correct from the start. | -| **IllegalCatch / IllegalThrows** | checkstyle `IllegalCatch` bans `catch (Exception/Throwable/RuntimeException)` — use the specific type, or a multi-catch (`catch (BadLocationException \| TemplateException e)`) re-thrown as `new IllegalStateException(e)`. `IllegalThrows` bans `throws Throwable/RuntimeException/Error` (plain `throws Exception` IS allowed — acceptable on a `@Test` when the JUnit-invoked API declares it; otherwise narrow to the actual checked type). Don't suppress `PMD.AvoidCatchingGenericException`. | +| **IllegalCatch / IllegalThrows** | Checkstyle `IllegalCatch` bans `catch (Exception/Throwable/RuntimeException)`. Do not copy broad catches from `xtend-gen`; catch the narrow checked types Java requires. Suppress `IllegalCatch` only in the exceptional case where an invoked API itself declares a broad type and no narrower catch compiles. Wrap only when the original behaviour or an intentional change calls for wrapping. `IllegalThrows` bans `throws Throwable/RuntimeException/Error` (plain `throws Exception` is allowed on a `@Test` when the JUnit-invoked API declares it; otherwise preserve/narrow the actual signature). Don't suppress `PMD.AvoidCatchingGenericException`. | | **Rollback** | A slice is 2-3 commits — plain `HEAD~1` strands the rename commit. Use the recipe in [`formatting-and-commit.md`](./formatting-and-commit.md) §Rollback. After reverting, build and test. | | **`ByteArrayInputStream.close()`** | It's a no-op. Safe to remove entirely. | | **`==` in Xtend** | On object/boxed operands, Xtend `==` is `.equals()` — convert to `.equals()`/`Objects.equals()`; only `===`/`!==` are identity. Between primitive-typed operands it compiles to Java `==` — check operand types in `xtend-gen/` (see [`rules/08-operator-overloads.md`](../rules/08-operator-overloads.md) §8.1). | @@ -41,4 +41,7 @@ Consolidated table of common mistakes and their fixes. Review before and after e | **Empty method body needs a comment** | PMD `UncommentedEmptyMethodBody` fires on a bare `{}`. Keep a comment (e.g. the original `// TODO …`) in genuinely-empty bodies. | | **Text block ≠ inline-`'''` exactly** | Java text blocks strip trailing whitespace on each content line and add a trailing newline before the closing `"""`; an inline-`'''` Xtend template preserves trailing spaces and omits the trailing newline. For string OUTPUT, match `xtend-gen` exactly (`\s` / `\` escapes). When the delta is provably behaviour-inert (e.g. a parser "no syntax errors" assertion) a clean text block is fine — say so in the commit/PR. | | **`final`-on-locals consistency** | Not an enforced gate, but keep locals consistently `final` within a file; mixed `final`/non-`final` siblings is a readability nit only. | -| **Don't carry `xbase.lib` types into migrated Java** | The `->` pair operator compiles to `org.eclipse.xtext.xbase.lib.Pair` — an Xtend runtime type. Don't keep it in the `.java`: replace with a small `private record` (named fields, accepts `null`) or `java.util.Map.entry` — but `Map.entry` **rejects null** keys/values, so use a record when nulls are possible. Bonus: a non-generic record vararg drops the `@SafeVarargs` a `Pair<…>` vararg required. Migrating off Xtend means migrating off `xbase.lib` too. | +| **Don't carry `xbase.lib` types into migrated Java** | The `->` pair operator compiles to `org.eclipse.xtext.xbase.lib.Pair` — an Xtend runtime type. Don't keep it in the `.java`: replace with a small `private record` (named fields, accepts `null`) or `java.util.Map.entry` — but `Map.entry` **rejects null** keys/values, so use a record when nulls are possible. Bonus: a non-generic record vararg drops the `@SafeVarargs` a `Pair<…>` vararg required. Migrating off Xtend means migrating off `xbase.lib`. | +| **`operator_add` (`+=`) skips nulls — both overloads** | Before translating any inferrer `EList +=`, follow [`rules/10-jvm-model-inferrer.md`](../rules/10-jvm-model-inferrer.md) §10.4. Both overloads skip nulls, while bare `add`/`addAll` rejects them; this exact mismatch shipped once in `FormatJvmModelInferrer.inferConstants`. | +| **`IterableExtensions.toSet` has stable order** | It returns an existing `Set` unchanged; otherwise it builds a `LinkedHashSet` in encounter order. `Collectors.toSet()` does not promise that order and can reorder generated output. Use `Collectors.toCollection(LinkedHashSet::new)` and check whether aliasing is observable. | +| **Behavioural equivalence ≠ literal-token equivalence** | When verifying a migration (or reconciling two migrations) against `xtend-gen`, do NOT decide "faithful" by whether a token (`filterNull`, a `catch`, a charset arg) textually appears. `xtend-gen` semantics can live in a call whose Java equivalent needs *extra* code (e.g. `operator_add`'s null-skip → an explicit null filter; §10.4). **Prove every behavioural divergence against fresh `xtend-gen` and cover it with a test — gates and existing tests only catch what they already exercise** (the shipped null-leak passed them all because no test fed a null). The `filterNull`-looks-spurious trap cost a real regression when trusted without such proof. | diff --git a/.agents/skills/xtend-to-java/workflow/validation-checklist.md b/.agents/skills/xtend-to-java/workflow/validation-checklist.md index 5af79f33a..8694b228a 100644 --- a/.agents/skills/xtend-to-java/workflow/validation-checklist.md +++ b/.agents/skills/xtend-to-java/workflow/validation-checklist.md @@ -64,15 +64,23 @@ Every rule below is a hard gate. | # | Rule | Requirement | |---|------|-------------| -| 12 | Preserved stack traces | `catch` blocks that re-throw must pass caught exception as cause. | +| 12 | Preserved throwables | Declare exact checked types when compatible. If an explicitly reviewed boundary strategy wraps one, use an established exception type and preserve the caught exception as its cause. | | 13 | try-with-resources | Any `AutoCloseable` — no manual `close()` in finally. | -| 18 | IllegalCatch | Catch specific exceptions, never generic `Exception`. | +| 18 | IllegalCatch | Never copy `xtend-gen`'s broad catch scaffold. Catch only the narrow checked types Java requires. A broad catch is allowed only when the invoked API declares that type and no narrower catch compiles, with a narrow justified suppression. See [`rules/05-control-flow.md`](../rules/05-control-flow.md) §5.5. | ### Collections | # | Rule | Requirement | |---|------|-------------| | 19 | UseCollectionIsEmpty | `.isEmpty()` not `.size() == 0`. | +| 36 | Inferrer `operator_add` | Before translating `+=` on an inferrer `EList`, prove the producer non-null or preserve `JvmTypesBuilder.operator_add`'s null-skip with a guard/filter. See [`rules/10-jvm-model-inferrer.md`](../rules/10-jvm-model-inferrer.md) §10.4. | +| 37 | `toSet` encounter order | Replace `IterableExtensions.toSet` with an encounter-ordered `LinkedHashSet` collector, not `Collectors.toSet()`; check whether its return-existing-`Set` aliasing matters. | + +### I/O and encodings + +| # | Rule | Requirement | +|---|------|-------------| +| 38 | Charset contract | Replace an implicit default charset only after establishing the data contract; do not infer arbitrary external data is UTF-8 merely from the project source encoding. See [`rules/09-misc-syntax.md`](../rules/09-misc-syntax.md) §9.11. | ### Extension methods and implicit behavior @@ -123,14 +131,14 @@ Every rule below is a hard gate. - [ ] All `@Data` / `@Accessors` / `@FinalFieldsConstructor` expanded. - [ ] All property access converted to getter/setter calls where applicable. - [ ] All `isNullOrEmpty` and Xtend library calls replaced. -- [ ] All `+=` / `-=` on collections converted to `.add()` / `.addAll()` / `.remove()`. +- [ ] All `+=` / `-=` on ordinary collections converted to `.add()` / `.addAll()` / `.remove()`; inferrer `EList +=` sites preserve `JvmTypesBuilder.operator_add` null-skipping. - [ ] All `::` static-access converted. - [ ] Semicolons added to every statement. - [ ] Explicit visibility on every class, method, and field. - [ ] All imports updated (no wildcards, no unused, correct order). - [ ] All comments and class/member Javadoc preserved exactly. - [ ] Copyright header is the exact Avaloq banner — **replacing** any generated-stub or Javadoc-style header the source had (not preserved from source). -- [ ] Checked exceptions handled (`throws` clause or `try`/`catch` with specific types). +- [ ] Checked exceptions use explicit Java contracts: exact `throws` types where compatible, or an explicitly reviewed project-established boundary strategy with the original exception preserved as the cause. - [ ] The `.xtend` no longer exists — renamed to `.java` via `git mv` then translated in place; no `.xtend`/`.java` pair coexists. See rule 22. ---