diff --git a/.agents/skills/xtend-to-java/SKILL.md b/.agents/skills/xtend-to-java/SKILL.md new file mode 100644 index 000000000..a2f4238f0 --- /dev/null +++ b/.agents/skills/xtend-to-java/SKILL.md @@ -0,0 +1,155 @@ +--- +name: xtend-to-java +description: > + Migrates Xtend (.xtend) source files to idiomatic Java 21 using a repeatable slice-based process. + Use this skill whenever the user wants to convert Xtend files to Java, port a group of Xtend files, + work through the xtend-to-java migration checklist, clean up PMD/Checkstyle violations in migrated files, + or verify that a migrated Java file faithfully preserves the behavior of its Xtend original. + Also triggers for phrases like "convert this xtend", "migrate these files", "port to java", or "xtend migration". +globs: + - "**/*.xtend" + - "**/*.java" +--- + +# Xtend → Java Migration Skill + +> For general coding standards (copyright headers, Javadoc, import rules, naming), see `AGENTS.md` at the repository root. + +## Overview + +This skill drives a slice-based Xtend-to-Java migration. A *slice* is a user-defined batch of files +migrated together, verified locally, and merged independently. The ground truth for behavior is always +the `xtend-gen/` output — what Xtend compiled to Java before the migration. + +## Hard Rules — Non-Negotiable + +> **Hard rule — read BOTH `.xtend` source AND `xtend-gen/` output in full before writing ANY Java.** +> No exceptions regardless of file size. A 5-line file with one template expression can have +> surprising whitespace behavior. See [`workflow/overview.md`](./workflow/overview.md) for the full rationale. + +> **Template whitespace is the #1 source of migration bugs.** +> Xtend strips indentation relative to control structures. The only reliable way to know what +> a template produces is to read `xtend-gen/`. Never guess from Xtend source — always verify. + +## When to invoke + +Use this skill when: + +- A user asks to convert an `.xtend` file (or several) to `.java`. +- A user asks to migrate a module off Xtend. +- You are touching an `.xtend` file with the intent to replace it (not just edit it). + +Do **not** invoke for normal Xtend editing where the file is staying in Xtend. + +## Decision tree + +1. **One file or several?** + - One file → [`workflow/one-file-conversion.md`](./workflow/one-file-conversion.md). + - Several files → [`workflow/multi-file-batch.md`](./workflow/multi-file-batch.md) for batching strategy. +2. **Read the full workflow** in [`workflow/overview.md`](./workflow/overview.md) — Steps 0–7. +3. **Internalise the binding decisions** in [`rules/00-decisions.md`](./rules/00-decisions.md). +4. **Walk the rules** for the constructs that actually appear in your source file: + - [`rules/01-imports-and-package.md`](./rules/01-imports-and-package.md) + - [`rules/02-variables.md`](./rules/02-variables.md) + - [`rules/03-methods.md`](./rules/03-methods.md) + - [`rules/04-templates.md`](./rules/04-templates.md) + - [`rules/05-control-flow.md`](./rules/05-control-flow.md) + - [`rules/06-extension-methods.md`](./rules/06-extension-methods.md) + - [`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) +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. +8. **Format and commit** via [`workflow/formatting-and-commit.md`](./workflow/formatting-and-commit.md). +9. **Review known pitfalls** in [`workflow/known-pitfalls.md`](./workflow/known-pitfalls.md). + +## Binding decisions cheat-sheet + +Detail in [`rules/00-decisions.md`](./rules/00-decisions.md). Quick recall: + +- Target **Java 21**. +- **String building**: literal → text block → `.formatted()` → `StringBuilder` (control flow only). +- **No `var` keyword.** Explicit types everywhere. +- **Explicit visibility** on classes, methods, fields. +- **Java stdlib by default** for collections and streams. Guava only where genuinely superior. +- **Never `String.format()`** — always `.formatted()`. +- **Parameterized Log4j2** — `{}` placeholders, never concatenation in loggers. + +## Xtend → Java Syntax Reference + +Use this table for quick mechanical transforms. Full details in the rule files. + +### Declarations and types + +| Xtend | Java | +|-------|------| +| `def methodName()` | `public ReturnType methodName()` (explicit access + return type) | +| `def private methodName()` | `private ReturnType methodName()` | +| `val x = expr` | `final ExplicitType x = expr;` (never `var`) | +| `var x = expr` | `ExplicitType x = expr;` | +| `typeof(MyClass)` | `MyClass.class` | +| `def dispatch method(Type1 x)` | Keep as `_method(Type1 x)` with `@SuppressWarnings` | + +### Operators and null handling + +| Xtend | Java | +|-------|------| +| `obj?.method()` | `obj != null ? obj.method() : null` (or guard clause) | +| `x ?: default` | `x != null ? x : default` | +| `===` / `!==` (identity) | `==` / `!=` | +| `==` / `!=` (equality) | `.equals()` / `!Objects.equals(a, b)` | +| `a..b` (range) | `IntStream.rangeClosed(a, b)` | + +### Lambdas and collections + +| Xtend | Java | +|-------|------| +| `[param \| body]` | `(param) -> { return body; }` | +| `[body]` (implicit `it`) | `(it) -> { return body; }` — name the parameter explicitly | +| `list.filter[condition]` | `list.stream().filter(x -> condition).toList()` | +| `list.map[transform]` | `list.stream().map(x -> transform).toList()` | +| `list.forEach[action]` | `list.forEach(x -> action)` (or `for` loop) | +| `list.exists[condition]` | `list.stream().anyMatch(x -> condition)` | +| `list.forall[condition]` | `list.stream().allMatch(x -> condition)` | +| `list.findFirst[condition]` | `list.stream().filter(x -> condition).findFirst().orElse(null)` | +| `list.head` | `list.isEmpty() ? null : list.get(0)` | +| `list.tail` | `list.subList(1, list.size())` | +| `#[a, b, c]` (immutable list) | `List.of(a, b, c)` | +| `#{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.remove(element)` | + +### Extension methods + +| Xtend pattern | Java equivalent | +|---------------|-----------------| +| `obj.extensionMethod()` | `ExtensionClass.extensionMethod(obj)` or `helper.extensionMethod(obj)` | +| `@Inject extension MyHelper helper` | `@Inject private MyHelper helper;` then `helper.method(obj)` | +| `import static extension com.foo.Util.*` | `import com.foo.Util;` then `Util.method(obj)` | + +### Property access + +| Xtend | Java | +|-------|------| +| `obj.name` (property access) | `obj.getName()` | +| `obj.name = value` (property write) | `obj.setName(value)` | + +### Template expressions + +| Xtend | Java | +|-------|------| +| `'''static text'''` | `"static text"` or text block | +| `'''text «expr» more'''` | `"text %s more".formatted(expr)` or concatenation | +| `'''«IF cond»...«ENDIF»'''` | `StringBuilder` with explicit `if` | +| `'''«FOR item : list»...«ENDFOR»'''` | `StringBuilder` with `for` loop | + +## Examples + +Worked end-to-end examples: + +- [`examples/00-basic-generator.md`](./examples/00-basic-generator.md) +- [`examples/01-template-with-for-loop.md`](./examples/01-template-with-for-loop.md) diff --git a/.agents/skills/xtend-to-java/examples/00-basic-generator.md b/.agents/skills/xtend-to-java/examples/00-basic-generator.md new file mode 100644 index 000000000..7859716ff --- /dev/null +++ b/.agents/skills/xtend-to-java/examples/00-basic-generator.md @@ -0,0 +1,104 @@ +# 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. + +## Xtend input + +```xtend +package com.example + +import com.google.inject.Inject +import org.eclipse.emf.ecore.resource.Resource +import static org.eclipse.xtext.xbase.lib.IteratorExtensions.* +import static extension com.example.NamingExtensions.* + +class MyGenerator { + @Inject extension MyHelper helper + + override void doGenerate(Resource resource) { + val config = getConfig(resource?.URI) + for (model : toIterable(resource.allContents).filter(typeof(MyModel))) { + model.compile + } + } + + def compile(MyModel it) ''' + package «packageName»; + «IF !imports.isNullOrEmpty» + + «FOR imp : imports» + import «imp»; + «ENDFOR» + «ENDIF» + + public class «name» { + } + ''' +} +``` + +## Java output + +```java +package com.example; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.xbase.lib.IteratorExtensions; + +import com.google.common.collect.Iterables; +import com.google.inject.Inject; + +@SuppressWarnings("nls") +public class MyGenerator { + + @Inject private MyHelper helper; + + @Override + public void doGenerate(final Resource resource) { + final URI uri = resource != null ? resource.getURI() : null; + final Config config = getConfig(uri); + for (final MyModel model : Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), MyModel.class)) { + helper.compile(model); + } + } + + public CharSequence compile(final MyModel model) { + final StringBuilder builder = new StringBuilder(512); + builder.append("package ").append(model.getPackageName()).append(";\n"); + if (!(model.getImports() == null || model.getImports().isEmpty())) { + builder.append("\n"); + for (final String imp : model.getImports()) { + builder.append("import ").append(imp).append(";\n"); + } + } + builder.append("\npublic class ").append(model.getName()).append(" {\n}\n"); + return builder; + } +} +``` + +## What changed + +| Xtend construct | Java equivalent | Rule | +|---|---|---| +| `class MyGenerator` (implicit public) | `public class MyGenerator` | 01 | +| `@Inject extension MyHelper helper` | `@Inject private MyHelper helper;` + rewrite call sites | 06 | +| `override void doGenerate(...)` | `@Override public void doGenerate(...)` | 03 | +| `val config = getConfig(...)` | `final Config config = getConfig(...);` | 02 | +| `resource?.URI` | `resource != null ? resource.getURI() : null` | 08 | +| `typeof(MyModel)` | `MyModel.class` | 09 | +| `toIterable(resource.allContents).filter(typeof(MyModel))` | `Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), MyModel.class)` | Guava exception (type-safe filtering) | +| `model.compile` (extension call) | `helper.compile(model)` | 06 | +| `def compile(MyModel it) '''...'''` | `public CharSequence compile(final MyModel model)` + StringBuilder | 04, 09.8 | +| `«packageName»` (property on `it`) | `model.getPackageName()` | 09.5 | +| `!imports.isNullOrEmpty` | `!(model.getImports() == null \|\| model.getImports().isEmpty())` | 09.4 | +| `«FOR imp : imports»` | `for (final String imp : model.getImports())` | 04 | +| `import static extension com.example.NamingExtensions.*` | Removed (calls rewritten) | 06 | + +## Key decisions + +- **Template → StringBuilder** (tier 4) because the template has `«IF»` and `«FOR»` control flow. +- **`it` parameter renamed** to `model` (descriptive name). +- **`Iterables.filter(iter, Type.class)`** kept as Guava — genuinely more concise for type-safe filtering. +- **Whitespace verified against `xtend-gen/`** — the template output was confirmed by reading the generated code. diff --git a/.agents/skills/xtend-to-java/examples/01-template-with-for-loop.md b/.agents/skills/xtend-to-java/examples/01-template-with-for-loop.md new file mode 100644 index 000000000..d0790024c --- /dev/null +++ b/.agents/skills/xtend-to-java/examples/01-template-with-for-loop.md @@ -0,0 +1,55 @@ +# Example: template with `«FOR»` and `«SEPARATOR»` + +Demonstrates the `StringBuilder` conversion pattern for templates that emit repeated content with a separator. Touches rule 04 in detail. + +## Xtend input + +```xtend +def renderArgs(List args) ''' + («FOR a : args SEPARATOR ", "»«a.type» «a.name»«ENDFOR») +''' +``` + +## Java output — stream approach + +When the body is a simple expression, use `Collectors.joining()`: + +```java +public CharSequence renderArgs(final List args) { + final StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(args.stream() + .map(a -> a.getType() + " " + a.getName()) + .collect(Collectors.joining(", "))); + builder.append(")\n"); + return builder; +} +``` + +## Java output — boolean flag approach + +When the body has multi-statement logic, use a flag: + +```java +public CharSequence renderArgs(final List args) { + final StringBuilder builder = new StringBuilder(); + builder.append("("); + boolean first = true; + for (final Argument a : args) { + if (!first) { + builder.append(", "); + } + builder.append(a.getType()).append(" ").append(a.getName()); + first = false; + } + builder.append(")\n"); + return builder; +} +``` + +## What changed + +- The `«FOR … SEPARATOR ", "»` block became either a stream with `Collectors.joining(", ")` or a boolean flag. +- The `«a.type»` and `«a.name»` interpolations became `a.getType()` and `a.getName()`. +- The trailing newline (implicit `\n` at end of template `'''`) is preserved by explicit `"\n"`. +- **Whitespace was verified against `xtend-gen/`** — the template starts with `(` directly (no leading newline because the first `'''` line has content after the opening mark). diff --git a/.agents/skills/xtend-to-java/references/xtend-library-replacements.md b/.agents/skills/xtend-to-java/references/xtend-library-replacements.md new file mode 100644 index 000000000..176e50d38 --- /dev/null +++ b/.agents/skills/xtend-to-java/references/xtend-library-replacements.md @@ -0,0 +1,47 @@ +# Xtend library replacements + +When a converted file calls Xtend's runtime library, replace the call with a Java stdlib equivalent. +Use Guava **only** where it is genuinely more concise (marked with ★). + +| Xtend library call | Java replacement | +|---|---| +| `IterableExtensions.map(iter, fn)` | `iter.stream().map(fn).toList()` | +| `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.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))` | +| `IterableExtensions.exists(iter, fn)` | `iter.stream().anyMatch(fn)` | +| `IterableExtensions.forall(iter, fn)` | `iter.stream().allMatch(fn)` | +| `IterableExtensions.findFirst(iter, fn)` | `iter.stream().filter(fn).findFirst().orElse(null)` | +| `IterableExtensions.sortBy(iter, fn)` | `iter.stream().sorted(Comparator.comparing(fn)).toList()` | +| `IterableExtensions.sort(iter)` | `iter.stream().sorted().toList()` | +| `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))` | +| `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)` | +| `StringExtensions.toFirstLower(s)` | `Character.toLowerCase(s.charAt(0)) + s.substring(1)` | +| `CollectionLiterals.newArrayList(...)` | `new ArrayList<>(List.of(...))` | +| `CollectionLiterals.newHashSet(...)` | `new HashSet<>(Set.of(...))` | +| `CollectionLiterals.newHashMap(...)` | `new HashMap<>(Map.of(...))` | +| `ObjectExtensions.operator_doubleArrow(obj, fn)` | Inline — see [`rules/08-operator-overloads.md`](../rules/08-operator-overloads.md) §8.4 | +| `Functions.Function1` | `java.util.function.Function` | +| `Procedures.Procedure1` | `java.util.function.Consumer` | + +## Guava utilities to keep as-is (do not rewrite) + +These are already Guava or Xtext-idiomatic patterns that should stay: + +- `Iterables.filter(iter, Type.class)` — type-safe class filtering +- `Iterables.getFirst(iter, default)` — null-safe first element +- `Joiner.on(sep).skipNulls()` — null-skipping joins (no Java stdlib equivalent) +- `IteratorExtensions.toIterable(resource.getAllContents())` — standard Xtext pattern + +## When in doubt + +Check the `xtend-gen/` output to see what the Xtend compiler actually generated. +The generated code shows the exact static method calls Xtend resolved to. diff --git a/.agents/skills/xtend-to-java/rules/00-decisions.md b/.agents/skills/xtend-to-java/rules/00-decisions.md new file mode 100644 index 000000000..9bb3d2364 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/00-decisions.md @@ -0,0 +1,112 @@ +# Binding decisions + +These are non-negotiable project choices. Apply them to every conversion. + +## Target: Java 21 + +Use the language features available: pattern matching for `instanceof`, switch expressions, text blocks, records. + +## No `var` keyword — explicit types everywhere + +Neither Xtend's `val`/`var` nor Java 10's `var`/`final var`. Every local variable, field, and parameter declares its type explicitly. + +```java +// Good +final CheckCatalog catalog = EcoreUtil2.getContainerOfType(context, CheckCatalog.class); +List names = new ArrayList<>(); +String result = computeResult(); + +// Bad — never +final var catalog = EcoreUtil2.getContainerOfType(context, CheckCatalog.class); +var names = new ArrayList<>(); +var result = computeResult(); +``` + +## Explicit visibility on everything + +Classes, methods, fields. Xtend defaults to `public` for classes and methods and to package-private for fields; Java conversion makes each one explicit. + +## Java stdlib by default — Guava only where genuinely superior + +Use Java standard library for collections and streams. Keep Guava only where it is genuinely more concise or has no clean Java equivalent: + +| Use Java stdlib | Use Guava (exception) | +|---|---| +| `new ArrayList<>()` | `Iterables.filter(iter, Type.class)` — type-safe class filtering | +| `new HashSet<>()` | `Iterables.getFirst(iter, default)` — null-safe first element | +| `new HashMap<>()` | `Joiner.on(sep).skipNulls()` — null-skipping joins | +| `List.of(...)` / `Set.of(...)` / `Map.of(...)` | | +| `.stream().filter(...).toList()` | | +| `.stream().map(...).toList()` | | +| `String.join(sep, iter)` | | + +**Do not rewrite existing Guava code** that you're not migrating. But do not propagate Guava into newly migrated files where Java stdlib suffices. + +## String building — 4-tier decision tree + +Choose the most readable Java idiom based on the template pattern: + +| # | Template pattern | Java idiom | +|---|---|---| +| 1 | No interpolation, single line | String literal | +| 2 | No interpolation, multi-line | Text block | +| 3 | Interpolation, no control flow | `.formatted()` (text block if multi-line, literal if single-line) | +| 4 | Control flow (`«IF»`, `«FOR»`) | `StringBuilder` with explicit `if`/`for` | + +**Decision rules — in order:** +1. **No interpolation, single line** → string literal +2. **No interpolation, multi-line** → text block +3. **Interpolation, no control flow** → `.formatted()` (on text block if multi-line, on literal if single-line) +4. **Control flow** → `StringBuilder` — always + +**`.formatted()` limitations — fall back to concatenation ONLY when:** +- The interpolated expression is glued to adjacent text with no whitespace/delimiter boundary, making `%s` ambiguous (e.g., `"pre" + expr + "suf"` where `"pre%ssuf"` is confusing) +- The template contains literal `%` characters (would need escaping as `%%`) + +If the `%s` sits at a natural boundary (start of line, after a space, before a newline), **use `.formatted()`** — do NOT fall back to concatenation just because the expression is at the start or in the middle. + +## Never `String.format()` — always `.formatted()` + +```java +// Good +"Processing %s in %s".formatted(name, context); + +// Bad +String.format("Processing %s in %s", name, context); +``` + +## Parameterized Log4j2 logging + +Use `{}` placeholders. Never concatenation or `.formatted()` in log calls. + +```java +private static final Logger LOGGER = LogManager.getLogger(MyClass.class); + +// Good +LOGGER.info("Processing file: {}", path); + +// Bad +LOGGER.info("Processing file: " + path); +LOGGER.info("Processing file: %s".formatted(path)); +``` + +## `@SuppressWarnings("nls")` — conditional on project settings + +Add at class level **only when the module's effective JDT settings have `nonExternalizedStringLiteral=warning`**. + +To check: look at `/.settings/org.eclipse.jdt.core.prefs`. If absent (most modules), check the project's shared settings at `ddk-configuration/.settings/org.eclipse.jdt.core.prefs`. + +**Current DDK state:** all modules inherit `warning` from `ddk-configuration`, so all migrated classes currently need `@SuppressWarnings("nls")` at class level. If a test-specific settings profile is later added with `ignore` (as in ASMD), test classes would not need it. + +```java +@SuppressWarnings("nls") +public class MyMigratedClass extends AbstractBase { +``` + +## Formatting conventions + +- 2-space indentation (project convention). +- Opening brace on the same line. +- Spaces around operators. +- Blank line between methods. +- Keep the original method ordering from the Xtend file. diff --git a/.agents/skills/xtend-to-java/rules/01-imports-and-package.md b/.agents/skills/xtend-to-java/rules/01-imports-and-package.md new file mode 100644 index 000000000..5324acb0d --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/01-imports-and-package.md @@ -0,0 +1,45 @@ +# Imports, package, class declaration, semicolons + +## 1.1 Package + +Keep identical. Add a trailing semicolon if missing. + +## 1.2 Imports + +- `import com.foo.Bar` stays as `import com.foo.Bar;` +- `import static com.foo.Bar.*` stays as `import static com.foo.Bar.*;` +- `import static extension com.foo.Bar.*` becomes `import static com.foo.Bar.*;` — the `extension` keyword is dropped at the import; see [`rules/06-extension-methods.md`](./06-extension-methods.md) for the call-site rewrite. +- **Remove** imports of Xtend-only types that are no longer needed (e.g. `org.eclipse.xtend2.lib.StringConcatenation` when switching to `StringBuilder`). +- **Add** any imports the Java code now needs: `java.util.List`, `java.util.ArrayList`, `java.util.stream.*`, `java.util.Objects`, etc. +- **No wildcard imports.** Every import is explicit. 20 explicit imports beats one wildcard. +- **Remove unused imports.** + +## 1.3 Import order convention + +Follow the project's Eclipse import order — **standard/framework first, project-specific second**: + +```java +// Group 1: org.*, java.*, javax.*, junit.* (standard/framework) +import org.eclipse.xtext.testing.InjectWith; +import org.junit.jupiter.api.Test; +import java.util.List; + +// Blank line separator + +// Group 2: com.* (project-specific: com.avaloq.*, com.google.*, etc.) +import com.avaloq.tools.ddk.check.core.test.util.CheckTestUtil; +import com.google.inject.Inject; +``` + +Within each group, imports are sorted alphabetically. There is always exactly one blank line between the two groups. + +## 1.4 Class declaration + +- Xtend classes are `public` by default. Make this explicit in Java. + - `class Foo extends Bar` → `public class Foo extends Bar` +- Xtend `interface` stays as `interface` (already public by default in Java too). +- Add `{` / `}` braces as normal Java. + +## 1.5 Semicolons + +Add semicolons to all statements. Xtend allows omission; Java requires them. diff --git a/.agents/skills/xtend-to-java/rules/02-variables.md b/.agents/skills/xtend-to-java/rules/02-variables.md new file mode 100644 index 000000000..ef70f6bce --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/02-variables.md @@ -0,0 +1,41 @@ +# Variable declarations + +Always explicit types. Never `var`. See [`rules/00-decisions.md`](./00-decisions.md). + +## 2.1 `val` — final local variable + +```xtend +val catalog = EcoreUtil2.getContainerOfType(context, CheckCatalog.class) +``` +becomes +```java +final CheckCatalog catalog = EcoreUtil2.getContainerOfType(context, CheckCatalog.class); +``` + +## 2.2 `var` — mutable local variable + +```xtend +var skip = instance - 1 +``` +becomes +```java +int skip = instance - 1; +``` + +## 2.3 `val` field (class-level final) + +- `val String FOO = "bar"` → `private final String FOO = "bar";` +- `val static Logger LOGGER = ...` → `private static final Logger LOGGER = ...;` + +Always add explicit visibility (`private` unless a wider scope is genuinely needed). + +## 2.4 `var` field (class-level mutable) + +- `var String name` → `private String name;` + +Always add explicit visibility. + +## 2.5 No leading underscore on non-dispatch fields + +Non-dispatch private fields that got `_fieldName` from the converter: rename to `fieldName`. +The `_` prefix is reserved for dispatch methods only (see [`rules/09-misc-syntax.md`](./09-misc-syntax.md)). diff --git a/.agents/skills/xtend-to-java/rules/03-methods.md b/.agents/skills/xtend-to-java/rules/03-methods.md new file mode 100644 index 000000000..055c9bd29 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/03-methods.md @@ -0,0 +1,71 @@ +# Method declarations + +## 3.1 `def` methods + +- `def` means `public` by default. Add explicit `public`. +- `def private`, `def protected`, `def package` keep their visibility. +- Add explicit return type. If the Xtend method omits the return type, infer it from the method body (or from the override target). + +Example: +```xtend +def outputPath() { '.settings' } +``` +becomes +```java +public String outputPath() { + return ".settings"; +} +``` + +## 3.2 `override` keyword + +Replace `override` with `@Override` annotation plus an explicit visibility modifier. +**`@Override` on every override** — including interface method implementations. + +```xtend +override void doGenerate(...) { ... } +``` +becomes +```java +@Override +public void doGenerate(...) { + ... +} +``` + +For `override protected doGenerate()`: +```java +@Override +protected void doGenerate() { + ... +} +``` + +## 3.3 Return types and implicit returns + +Xtend returns the value of the last expression. In Java, **add explicit `return` statements** for every non-void return path. For `void` methods, no return needed. + +```xtend +def foo() { bar } +``` +becomes +```java +public SomeType foo() { + return bar; +} +``` + +## 3.4 Method parameters + +- Add `final` to parameters that are not reassigned (matches the project's existing Java code convention). +- `extension` parameters: see [`rules/06-extension-methods.md`](./06-extension-methods.md). + +## 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. diff --git a/.agents/skills/xtend-to-java/rules/04-templates.md b/.agents/skills/xtend-to-java/rules/04-templates.md new file mode 100644 index 000000000..0495bf537 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/04-templates.md @@ -0,0 +1,204 @@ +# Template expressions (guillemets) + +The most complex feature. Xtend templates use triple single quotes plus `«»` interpolation markers. + +## Template whitespace — the #1 source of migration bugs + +Xtend template expressions (`'''...'''`) have **smart whitespace handling** that is NOT obvious from +reading the Xtend source alone. You MUST check the `xtend-gen/` output to see the actual string values. + +The rules (from the Xtend docs): + +1. **Indentation relative to a control structure is stripped.** If a template starts with `'''` followed + by a newline, the indentation on subsequent lines relative to that opening mark is removed. + +2. **Lines containing only control structures (`«IF»`, `«FOR»`, `«ENDIF»`) produce no output.** + +3. **Newlines in appended strings get the current indentation prepended.** + +**What this means in practice:** +```xtend +def String example() {''' + check configuration «name» {'''.toString} +``` +produces `"check configuration testing {"` — **NOT** `"\n check configuration testing {"`. + +**The only reliable way to know what a template produces is to read the `xtend-gen/` output.** + +## 4.1 String building — choose the right idiom + +Apply the 4-tier decision tree from [`rules/00-decisions.md`](./00-decisions.md): + +### Tier 1 — Static single line → string literal + +```xtend +'''some static text''' +``` +becomes +```java +"some static text" +``` + +### Tier 2 — Static multi-line → text block + +```xtend +''' +line one +line two +''' +``` +becomes +```java +""" +line one +line two +""" +``` + +### Tier 3 — Interpolation without control flow → `.formatted()` + +```xtend +'''«type» «name»;''' +``` +becomes +```java +"%s %s;".formatted(type, name) +``` + +Multi-line with interpolation: +```xtend +''' +package «packageName»; + +public class «className» { +} +''' +``` +becomes +```java +""" +package %s; + +public class %s { +} +""".formatted(packageName, className) +``` + +**Fall back to concatenation ONLY when** `%s` is ambiguous or template has literal `%`: +```java +// OK — %s at natural boundary +"import %s;".formatted(imp) + +// Fallback — %s glued to text with no boundary +"{predicates." + predicate.getName() + "(parserContext)}?=>" +``` + +### Tier 4 — Control flow → `StringBuilder` + +```xtend +''' +«FOR k : properties.keySet» +«k»=«properties.get(k)» +«ENDFOR» +''' +``` +becomes +```java +final StringBuilder builder = new StringBuilder(); +for (final String k : properties.keySet()) { + builder.append(k).append("=").append(properties.get(k)).append("\n"); +} +return builder; +``` + +## 4.2 Template control flow patterns + +- `«IF condition»...«ENDIF»` → `if (condition) { builder.append(...); }` +- `«IF condition»...«ELSE»...«ENDIF»` → `if`/`else` +- `«ELSEIF condition»` → `else if (condition)` +- `«FOR item : collection»...«ENDFOR»` → `for (Type item : collection) { builder.append(...); }` +- `«FOR item : collection SEPARATOR sep»...«ENDFOR»` → use `StringJoiner`, `Collectors.joining(sep)`, or a boolean flag +- `«val x = expr»` inside a template → declare as a local before the relevant `.append()` + +### SEPARATOR pattern + +Stream approach (when body is a simple expression): +```java +builder.append(collection.stream() + .map(item -> item.getType() + " " + item.getName()) + .collect(Collectors.joining(", "))); +``` + +Boolean flag (when body is complex): +```java +boolean first = true; +for (final Argument a : args) { + if (!first) { + builder.append(", "); + } + builder.append(a.getType()).append(" ").append(a.getName()); + first = false; +} +``` + +## 4.3 Text block `\` escape — suppressing trailing newlines + +Java text blocks always end with a trailing newline before the closing `"""`. When the original +string must NOT end with `\n`, use a **line continuation** `\` on the last content line: + +```java +// String that ends with } not }\n +String s = """ + + check configuration mdlc + for com.avaloq.tools.dsl.labeldef.LabelDef { + catalog com.avaloq.tools.dsl.labeldef.validation.LabelDefCoreChecks { } + }\ +"""; +``` + +The `\` suppresses the line terminator. Place the closing `"""` at column 0 to prevent indent stripping. + +**When to use this pattern:** check the `xtend-gen/` output. If the original string starts with `\n` +and does NOT end with `\n`, the `\` escape is the correct approach. + +## 4.4 Return type + +Methods that return templates should return `CharSequence` (matches `StringBuilder`). +Return `String` only if every caller treats it as a `String` (and `.toString()` the builder). + +## 4.5 `«expression»` interpolation + +Convert each `«expr»` into the corresponding Java expression: +- Inside `.formatted()`: as `%s` argument +- Inside `StringBuilder`: as `.append(expr)` call + +Property access: `«catalog.name»` → `catalog.getName()` (see [`rules/09-misc-syntax.md`](./09-misc-syntax.md)). + +## 4.6 StringBuilder sizing + +PMD's `InsufficientStringBufferDeclaration` checks that the declared capacity is large enough for the appended content. Java's default of 16 is almost never enough and trips the rule; aggressive sizing also prevents the first internal resize. + +Pick the next power of two above the expected length: + +- Methods appending < 500 chars → `new StringBuilder(512)` +- Generator methods producing larger output → `new StringBuilder(2048)` +- For known sizes outside those ranges, size to the next power of two. + +## 4.7 MultipleStringLiterals — Checkstyle + +Checkstyle flags any string literal that appears 2+ times. Handle differently by context: + +**In test classes:** extract to `private static final String` constants: +```java +private static final String TEST_ORA_USER = "TEST.ORA USER"; +``` + +**In generator methods:** wrap the method body with suppression: +```java +// CHECKSTYLE:CONSTANTS-OFF +// ... method body with repeated keywords ... +// CHECKSTYLE:CONSTANTS-ON +``` + +Prefer constants over suppression whenever the repeated string has a meaningful name. diff --git a/.agents/skills/xtend-to-java/rules/05-control-flow.md b/.agents/skills/xtend-to-java/rules/05-control-flow.md new file mode 100644 index 000000000..2789f207d --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/05-control-flow.md @@ -0,0 +1,56 @@ +# Control flow + +## 5.1 Switch as expression + +```xtend +switch(x) { + case "a": doA() + case "b": doB() + default: doDefault() +} +``` +becomes a Java switch expression (preferred, Java 14+) or switch statement, depending on context. + +## 5.2 Switch with type guards + +```xtend +switch obj { + CheckCatalog: obj.name + Category case obj.name !== null: obj.label + default: "unknown" +} +``` +becomes +```java +if (obj instanceof CheckCatalog checkCatalog) { + return checkCatalog.getName(); +} else if (obj instanceof Category category && category.getName() != null) { + return category.getLabel(); +} else { + return "unknown"; +} +``` + +Use Java 21 pattern matching for `instanceof` wherever applicable. + +## 5.3 Template control flow + +Already covered in [`rules/04-templates.md`](./04-templates.md). Short reference: + +- `«IF» / «ELSEIF» / «ELSE» / «ENDIF»` → `if` / `else if` / `else` +- `«FOR x : xs»` → `for (Type x : xs)` +- `«FOR x : xs SEPARATOR sep»` → `Collectors.joining(sep)` or a boolean separator-flag + +## 5.4 If as expression + +Xtend `if` is an expression that returns a value. Java needs either a ternary or extraction. + +```xtend +val label = if (x !== null) x.name else "" +``` +becomes +```java +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. diff --git a/.agents/skills/xtend-to-java/rules/06-extension-methods.md b/.agents/skills/xtend-to-java/rules/06-extension-methods.md new file mode 100644 index 000000000..d8740001f --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/06-extension-methods.md @@ -0,0 +1,53 @@ +# Extension methods + +Xtend's "extension" mechanism dispatches a method call on the receiver to a separate type. Java has no equivalent — every extension call site is rewritten to an explicit static or instance call. + +**This is the #2 challenge after templates (81% of files use extensions).** + +## 6.1 `@Inject extension ClassName fieldName` + +Convert the field, then rewrite every call site: + +- Field: `@Inject extension MyHelper helper` → `@Inject private MyHelper helper;` +- Call site: `obj.extensionMethod(args)` → `helper.extensionMethod(obj, args)` (the implicit receiver `obj` moves to the first parameter). +- If the extension field had no name (`@Inject extension CheckGeneratorNaming`), invent one following the convention: camelCase class name starting lowercase (`checkGeneratorNaming`). + +Tip: The `xtend-gen/` output shows exactly how the Xtend compiler resolved every extension call — use it as the reference. + +## 6.2 `extension` method parameters + +A method with `def foo(extension IFormattableDocument document)` lets the body call `document`'s methods without a receiver. + +- Drop the `extension` keyword from the parameter. +- At call sites within the method body, calls that dispatched to the extension parameter become explicit calls on the parameter: + - `prepend(checkcatalog)[noSpace]` becomes `document.prepend(checkcatalog, (IHiddenRegionFormatter it) -> it.noSpace())` + +## 6.3 `static extension` imports + +```xtend +import static extension com.foo.Bar.* +``` +becomes +```java +import static com.foo.Bar.*; +``` + +At every call site: `obj.staticExtensionMethod(args)` becomes `Bar.staticExtensionMethod(obj, args)`. + +Example: +```xtend +import static extension org.eclipse.xtext.GrammarUtil.* +grammar.simpleName +``` +becomes +```java +GrammarUtil.getSimpleName(grammar) +``` + +## 6.4 `extension` keyword on `val`/`var` + +```xtend +val extension naming = contentAssistNaming +``` +- Drop the `extension` keyword. Keep as a plain local variable. +- Rewrite call sites within scope to explicit calls on the variable. diff --git a/.agents/skills/xtend-to-java/rules/07-lambdas.md b/.agents/skills/xtend-to-java/rules/07-lambdas.md new file mode 100644 index 000000000..dffe2a5c1 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/07-lambdas.md @@ -0,0 +1,34 @@ +# Lambdas + +## 7.1 `[...]` → `(...) -> {...}` + +- `[x | x.name]` → `x -> x.getName()` (parentheses optional for a single param) +- `[it | name]` → `it -> it.getName()` or a method reference +- `[ body ]` with no parameters but implicit `it` → `(it) -> { body }` if `it` is used, `() -> { body }` otherwise. +- Single-expression lambdas: no braces, no `return`. +- Multi-statement lambdas: braces and explicit `return`. + ```java + (x) -> { doSomething(); return x.getName(); } + ``` + +## 7.2 The implicit `it` parameter + +When a lambda uses properties/methods without a receiver, those reference the implicit `it` parameter. +**Name the parameter explicitly in Java.** + +- `[name]` on a `Function1` becomes `(Foo it) -> it.getName()` or `Foo::getName`. +- `checks.filter[name !== null]` becomes `checks.stream().filter(c -> c.getName() != null).toList()`. + +## 7.3 Procedure (void) vs Function (returning) + +Xtend uses the same `[...]` syntax for both. In Java, decide from context: + +- Returns nothing → `Consumer` / `BiConsumer` / project-specific procedure type +- Returns a value → `Function` / `BiFunction` / `Predicate` / etc. + +Xtext-specific case — `Procedure1` lambdas like `[noSpace]` become: +```java +(IHiddenRegionFormatter it) -> { it.noSpace(); } +// or simply +IHiddenRegionFormatter::noSpace +``` diff --git a/.agents/skills/xtend-to-java/rules/08-operator-overloads.md b/.agents/skills/xtend-to-java/rules/08-operator-overloads.md new file mode 100644 index 000000000..0bae08692 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/08-operator-overloads.md @@ -0,0 +1,61 @@ +# Operators and expressions + +## 8.1 Identity comparison + +- `===` (Xtend identity-equals) → `==` (Java) +- `!==` (Xtend identity-not-equals) → `!=` (Java) +- `==` in Xtend is `.equals()` — convert to `.equals()` or `Objects.equals()` (use `Objects.equals()` when either operand could be null). + +## 8.2 Null-safe navigation `?.` + +`obj?.method()` becomes an explicit null check: + +- Ternary: `obj != null ? obj.method() : null` +- For chained null-safe calls, nest ternaries or use local variables: + ```xtend + resource?.URI + ``` + → + ```java + final URI uri = resource != null ? resource.getURI() : null; + ``` +- If the `?.` result is compared against `null` (e.g., `resource?.URI !== null`), inline the AND: `resource != null && resource.getURI() != null`. + +## 8.3 Elvis `?:` + +`a ?: b` → `a != null ? a : b` (or `Objects.requireNonNullElse(a, b)` when the eager evaluation of `b` is fine). + +## 8.4 `=>` (with/apply) + +`obj => [ body ]` executes `body` with `obj` as `it`, then returns `obj`. + +```xtend +new Foo() => [bar = "baz"] +``` +becomes +```java +Foo foo = new Foo(); +foo.setBar("baz"); +// use foo +``` + +For non-trivial bodies, extract to a helper or rewrite as a sequence of statements. + +## 8.5 String concatenation `+` + +Same in Java. + +## 8.6 Range `..` + +`0..n` → `IntStream.rangeClosed(0, n)` or a `for`-loop, whichever fits the call site. + +## 8.7 Power `**` + +`a ** b` → `Math.pow(a, b)`. + +## 8.8 Collection operators + +- `list += element` → `list.add(element)` +- `list += otherList` → `list.addAll(otherList)` +- `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 new file mode 100644 index 000000000..4c5c62652 --- /dev/null +++ b/.agents/skills/xtend-to-java/rules/09-misc-syntax.md @@ -0,0 +1,132 @@ +# Miscellaneous syntax + +## 9.1 Type references + +- `typeof(X)` → `X.class`. Example: `typeof(CheckCatalog)` → `CheckCatalog.class`. +- Generic syntax: identical in Xtend and Java. Keep as-is. +- Type casting: `expr as Type` → `(Type) expr`, or Java 21 pattern matching with `instanceof`. + +## 9.2 Collection literals + +- `#["a", "b", "c"]` → `List.of("a", "b", "c")` (immutable) or `new ArrayList<>(List.of("a", "b", "c"))` (mutable). +- `#[]` (empty) → `List.of()` or `new ArrayList<>()`. +- `newArrayList(...)` → `new ArrayList<>(List.of(...))` or `new ArrayList<>()`. +- `#{"a", "b"}` → `Set.of("a", "b")` or `new HashSet<>(Set.of(...))`. +- `newHashSet(...)` → `new HashSet<>(Set.of(...))`. +- `newHashMap(...)` → `new HashMap<>(Map.of(...))`. + +## 9.3 Collection extension methods + +Most have direct Stream equivalents. See [`references/xtend-library-replacements.md`](../references/xtend-library-replacements.md). + +**Exception — keep Guava where genuinely superior:** +- `Iterables.filter(iter, Type.class)` — type-safe class filtering (Java streams need `.filter(Foo.class::isInstance).map(Foo.class::cast)`) + +## 9.4 `isNullOrEmpty` + +- `StringExtensions.isNullOrEmpty(s)` or `s.isNullOrEmpty` → `s == null || s.isEmpty()`. + +## 9.5 Property access syntax + +Xtend allows getter/setter property access: +- `obj.name` → `obj.getName()` (when `name` is not a public field). +- `obj.name = value` → `obj.setName(value)`. +- `obj.isActive` → `obj.isActive()` (or `obj.getIsActive()`; check the actual class). +- `field.final` → `field.isFinal()`, `field.static` → `field.isStatic()`. + +**Caveat**: not all dot-access is property access. If the receiver has an actual public field, keep field access. Check `xtend-gen/` if unsure. + +## 9.6 Active annotations + +### `@Data` + +Generates `equals()`, `hashCode()`, `toString()`, and getters for final fields. + +- Class with no superclass and no mutable state → convert to a Java **`record`**. + ```java + public record SemanticPredicate(String name, String message, String grammar, List keywords) {} + ``` +- Otherwise, manually add: all-args constructor, getters, `equals()`, `hashCode()`, `toString()`. + The `xtend-gen/` output shows exactly what was generated — copy from there. + +### `@Accessors` + +Generates getters (and setters for `var` fields). + +- `@Accessors boolean foo` → `getFoo()` + `setFoo(boolean)`. +- `@Accessors(PUBLIC_SETTER) String bar` → public setter only. +- `@Accessors(PROTECTED_GETTER) Foo baz` → protected getter only. + +Write the accessor methods explicitly with the specified visibility. + +### `@FinalFieldsConstructor` + +Generates a constructor taking all final fields as parameters. Write it manually. + +## 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. + +```xtend +def dispatch void format(CheckCatalog c, IFormattableDocument doc) { ... } +def dispatch void format(Category c, IFormattableDocument doc) { ... } +def dispatch void format(EObject obj, IFormattableDocument doc) { ... } +``` +becomes +```java +@SuppressWarnings({"checkstyle:MethodName", "PMD.UnusedFormalParameter"}) +public class MyFormatter extends AbstractFormatter { + + protected void _format(CheckCatalog c, IFormattableDocument doc) { ... } + protected void _format(Category c, IFormattableDocument doc) { ... } + protected void _format(EObject obj, IFormattableDocument doc) { ... } + + public void format(Object obj, IFormattableDocument doc) { + if (obj instanceof CheckCatalog c) { + _format(c, doc); return; + } else if (obj instanceof Category c) { + _format(c, doc); return; + } else if (obj instanceof EObject e) { + _format(e, doc); return; + } else { + throw new IllegalArgumentException("Unhandled parameter types: " + obj); + } + } +} +``` + +Rules: +- **Keep the `_` prefix** — the Xtext runtime resolves dispatch by name. +- **Suppress at class level**: `@SuppressWarnings({"checkstyle:MethodName", "PMD.UnusedFormalParameter"})` +- Order `instanceof` checks from most specific to least specific. +- If the original `dispatch` had `override`, add `@Override` to the **dispatcher**, not the `_` methods. +- The dispatcher parameter type should be the common supertype (often `Object` or `EObject`). +- If the parent class has dispatch methods with the same name, the dispatcher must call `super._methodName()` for types not handled locally. +- Use Java 21 pattern matching for `instanceof`. + +## 9.8 Special patterns + +- **`it` implicit parameter on methods**: when a method declares `Type it` as its first parameter, unqualified calls in the body refer to `it`. Rename the parameter (e.g. `exportModel`) and qualify every call accordingly (`exports` → `exportModel.getExports()`). +- **`this` vs receiver resolution**: in Xtend, an unqualified call may dispatch to `this`, to an `@Inject extension`, to a `static extension` import, or to `it`. Decide based on the type hierarchy and rewrite to one of: + 1. `this.method()` or just `method()` (own class / superclass) + 2. `field.method(obj)` (`@Inject extension` field) + 3. `ExtClass.method(obj)` (`static extension`) + 4. `it.method()` using the renamed parameter +- **Multiple return paths**: Xtend implicitly returns the last expression. Add explicit `return` on **every** non-void path. +- **`class` keyword as literal**: `SomeClass` used as a class literal → `SomeClass.class`. +- **Static method reference `::`**: `ClassName::methodName` → `ClassName.methodName()` (or keep as a Java method reference where the receiving API accepts one). +- **Pairs**: `key -> value` (in pair-construction context) → `Pair.of(key, value)` or `Map.entry(key, value)`. +- **`^keyword`** (escaped reserved word in Xtend): Drop the `^` — most Xtend escapes aren't Java keywords. + +## 9.9 Guice DI + +- `@Inject` fields stay as `@Inject`. Add `private` if missing. +- `@Inject ClassName fieldName` → `@Inject private ClassName fieldName;` +- For `@Inject extension`, see [`rules/06-extension-methods.md`](./06-extension-methods.md). + +## 9.10 Comments and Javadoc + +- Preserve **all** comments (Javadoc `/** */`, block `/* */`, line `//`) exactly. +- **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. diff --git a/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md b/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md new file mode 100644 index 000000000..6b4c672d4 --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/formatting-and-commit.md @@ -0,0 +1,72 @@ +# Formatting and commit + +## Copyright headers + +Every `.java` file must include the Avaloq copyright header. Files must start with: +```java +/******************************************************************************* + * 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 + *******************************************************************************/ +``` + +Verify before committing. + +## Format in Eclipse before pushing + +Before pushing to the remote, **always format the migrated Java files** using one of these methods: + +### Option 1 — Eclipse CLI formatter (preferred, reproducible) + +Run the Eclipse headless formatter from the command line: + +```powershell +$eclipse = "\eclipsec.exe" +$ini = "\eclipse-formatter.ini" # custom ini fixing the p2.mirrors bug +$config = "ddk-configuration\.settings\org.eclipse.jdt.core.prefs" +$ws = "$env:TEMP\eclipse-fmt-ws" # temp workspace to avoid conflicts + +& $eclipse --launcher.ini $ini -noSplash -data $ws ` + -application org.eclipse.jdt.core.JavaCodeFormatter -verbose ` + -config $config ... +``` + +**Important:** Use `eclipsec.exe` (console version) with `--launcher.ini` pointing to a custom ini file. The default `eclipse.ini` has a bug where `-Declipse.p2.mirrors` is placed before `-vmargs`, causing the formatter to fail silently. The custom `eclipse-formatter.ini` moves this property under `-vmargs`. + +**The CLI formatter does NOT organize imports.** Import order must be correct in your source from the start — follow the import order convention in [`rules/01-imports-and-package.md`](../rules/01-imports-and-package.md). + +### Option 2 — Eclipse IDE formatting + +Select all migrated files in Package Explorer → Source → Format. Then amend the commit. + +**⚠️ IDE save action warning:** "Organize Imports" or other IDE actions may trigger save actions that auto-convert string concatenation to text blocks. The auto-conversion can produce **wrong results**. Always review `git diff` after any IDE action. + +### Formatter safety notes + +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 message format + +``` +refactor: migrate Xtend to Java - +``` + +Example: `refactor: migrate Xtend to Java - com.avaloq.tools.ddk.check.core.test` + +PR title: same as commit message. diff --git a/.agents/skills/xtend-to-java/workflow/infrastructure-cleanup.md b/.agents/skills/xtend-to-java/workflow/infrastructure-cleanup.md new file mode 100644 index 000000000..f1683cc44 --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/infrastructure-cleanup.md @@ -0,0 +1,31 @@ +# Module infrastructure cleanup + +When Xtend is fully removed from a module, update these files. + +## Per-module files + +| File | Change | +|------|--------| +| **META-INF/MANIFEST.MF** | Remove `org.eclipse.xtend.lib` and `org.eclipse.xtext.xbase.lib` from `Require-Bundle` (keep `xtext.xbase.lib` if the module still uses Xtext runtime classes like `XbaseTypeComputer`) | +| **build.properties** | Remove `xtend-gen/` from `source..` entries | +| **.classpath** | Remove `` (including any nested ``) | +| **.project** | Remove `org.eclipse.xtext.ui.shared.xtextBuilder` from `` and `org.eclipse.xtext.ui.shared.xtextNature` from `` | +| **xtend-gen/** | Delete the entire directory (including its `.gitignore` marker) | + +## Verify: no leftover Xtend references + +```bash +grep -r "xtend" /META-INF/ /build.properties /.classpath /.project +``` + +## What stays put + +- **`.mwe2.launch` files** (e.g. `GenerateCheck.mwe2.launch`) — drive MWE2 src-gen regeneration, which is independent of Xtend. They remain after a module is off Xtend. +- **`com.avaloq.tools.ddk.workflow/`** — the MWE2 workflow bundle. Same reason. +- **`ddk-target/ddk.target`** — keep any Xtend SDK reference until the LAST module migrates; removing earlier breaks builds for modules still on Xtend. + +## If last Xtend module in the repository + +Also update: +- `ddk-parent/pom.xml` — remove `xtend-maven-plugin`, `xtend.version`, xtend-gen entries in `maven-clean-plugin` and `tycho-source-plugin` +- Root `.gitignore` — remove `/*/xtend-gen/*` and `!/*/xtend-gen/.gitignore` diff --git a/.agents/skills/xtend-to-java/workflow/known-pitfalls.md b/.agents/skills/xtend-to-java/workflow/known-pitfalls.md new file mode 100644 index 000000000..583c1e686 --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/known-pitfalls.md @@ -0,0 +1,31 @@ +# Known pitfalls + +Consolidated table of common mistakes and their fixes. Review before and after every migration. + +| Pitfall | What to do | +|---------|------------| +| **Template whitespace** | The #1 migration bug source. Xtend's `StringConcatenation` strips leading indentation relative to control structures. A template that *looks* like it has a leading newline and spaces often produces a flat string. The ONLY way to know is to read `xtend-gen/`. Never guess. | +| **"Trivial" files** | Small files are NOT safe to eyeball. A 5-line file with one template can have surprising whitespace. Read both references for every file. | +| **Extension method call sites** | `obj.helperMethod()` in Xtend becomes `helper.helperMethod(obj)` in Java. The `xtend-gen/` output shows how every extension call was resolved. | +| **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. | +| **`val` leaks** | Never use `var`. Use the explicit type. | +| **Invented Javadoc** | Never add Javadoc that wasn't in the original. This is a migration, not a rewrite. | +| **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. | +| **`BasicEList` in generic code** | Needs explicit type parameter — `new BasicEList()`. | +| **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. | +| **PMD missing type-resolution** | Always `compile` before `pmd:check` or you'll miss `MissingOverride`, `LooseCoupling` etc. | +| **`--fail-at-end` hides failures** | Check the final BUILD line, not intermediate output. | +| **Dispatch method names** | Keep underscores. Suppress with `@SuppressWarnings`. Never rename. | +| **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** | The project uses `org.*`/`java.*` first, blank line, then `com.*` second. Wrong order causes diff churn. | +| **Eclipse CLI formatter** | Does NOT organize imports — only code formatting. Import order must be correct from the start. | +| **IllegalCatch** | Do not suppress `PMD.AvoidCatchingGenericException`. Replace `catch (Exception e)` with specific exceptions. | +| **Rollback** | If already pushed: `git revert -m 1 `. If local only: `git reset --hard HEAD~1`. After reverting, build and test. | +| **`ByteArrayInputStream.close()`** | It's a no-op. Safe to remove entirely. | +| **`==` in Xtend** | Xtend `==` is `.equals()`, not identity. Convert to `.equals()` or `Objects.equals()`. Only `===`/`!==` are identity. | diff --git a/.agents/skills/xtend-to-java/workflow/multi-file-batch.md b/.agents/skills/xtend-to-java/workflow/multi-file-batch.md new file mode 100644 index 000000000..f893fd75b --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/multi-file-batch.md @@ -0,0 +1,68 @@ +# Multi-file batch workflow + +Use this when converting several `.xtend` files together. + +## Batching principles + +### Batch sizing by complexity + +Bucket files by complexity, not raw line count: + +| Bucket | Heuristic | +|---|---| +| **Trivial** | ≤30 lines, no complex features (no dispatch, no templates, no `@Data`/`@Accessors`) | +| **Easy** | ≤100 lines, basic features only (`val`, `override`, `@Inject`, simple templates) | +| **Medium** | 100–200 lines, **or** any file with templates + extension methods + switch | +| **Hard** | 200–400 lines with dispatch / complex templates / many extensions | +| **Very Hard** | 400+ lines and a combination of dispatch + templates + extensions | + +Aim for ~10 files per batch when all Trivial/Easy. Drop the count as complexity climbs — a single Very Hard file may be its own batch. + +### Leaf-bundle-first ordering + +Process modules bottom-up: + +1. Leaf modules first: test utilities, small standalone bundles. +2. Then mid-level: language cores, simple generators. +3. Heavy generators and dispatch-heavy code last: `xtext.generator`, `xtext.format`, large `xtext.export.*`. + +### Group by module + +Batch files within the **same module** together where possible — they share Maven module context, test setup, and helper classes. + +### Validation between batches + +After every batch: + +1. **Compile gate**: `mvn -pl : -am -DskipTests compile -f ./ddk-parent/pom.xml` — must pass. +2. **Test gate**: `mvn verify -f ./ddk-parent/pom.xml --batch-mode --fail-at-end` — must pass. +3. **Static analysis gate**: `mvn checkstyle:check pmd:check spotbugs:check -f ./ddk-parent/pom.xml` — must pass. + +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. + +Commit message format: +``` +refactor: migrate Xtend to Java - +``` +Example: `refactor: migrate Xtend to Java - com.avaloq.tools.ddk.check.core.test` + +**Never add co-authoring trailers** (`Co-authored-by`, `Signed-off-by`, etc.) to migration commits. + +### Rollback strategy + +If a batch fails any validation gate: + +1. **Do not force-commit.** +2. Revert: `git reset --hard HEAD~1` (or `git revert -m 1 ` if already pushed). +3. Diagnose the failure file-by-file. +4. **Pin** any individually-problematic file for separate investigation. Skip it; do the remaining files. +5. Re-attempt without the pinned files. Tackle pins as one-off conversions afterwards. + +## Per-file mechanics + +See [`workflow/one-file-conversion.md`](./one-file-conversion.md) for the per-file conversion steps. diff --git a/.agents/skills/xtend-to-java/workflow/one-file-conversion.md b/.agents/skills/xtend-to-java/workflow/one-file-conversion.md new file mode 100644 index 000000000..d313dc00a --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/one-file-conversion.md @@ -0,0 +1,26 @@ +# One-file conversion workflow + +Use this when converting a single `.xtend` to its `.java` counterpart. + +## Steps + +1. **Read the Xtend source in full.** Note its package, imports, and the constructs it uses. +2. **Read the `xtend-gen/` output in full.** Same package, same class name, `.java` extension. This is mandatory — no exceptions. See [`workflow/overview.md`](./overview.md) Step 3. +3. **Identify which rule files apply.** Template-heavy? Extension-heavy? Simple POJO? Load only those. +4. **Apply the binding decisions.** [`rules/00-decisions.md`](../rules/00-decisions.md) — Java 21, 4-tier string building, no `var`, explicit visibility, Java stdlib. +5. **Apply rules in order.** Walk [`rules/01-...`](../rules/01-imports-and-package.md) through [`rules/09-...`](../rules/09-misc-syntax.md) for the constructs you identified. +6. **Write the `.java` file.** Match `xtend-gen/` behavior exactly while using idiomatic Java. +7. **Run the validation checklist.** [`workflow/validation-checklist.md`](./validation-checklist.md). Every item must pass. +8. **Delete the `.xtend` file** as part of the same commit. Don't leave both. +9. **Verify the file compiles:** + ```bash + mvn -pl : -am -DskipTests compile -f ./ddk-parent/pom.xml > mvn-output.txt 2>&1 + ``` +10. **Run quality checks:** + ```bash + mvn -pl : -am checkstyle:check pmd:check -f ./ddk-parent/pom.xml > mvn-output.txt 2>&1 + ``` + +## Reference + +A worked end-to-end example is at [`examples/00-basic-generator.md`](../examples/00-basic-generator.md). diff --git a/.agents/skills/xtend-to-java/workflow/overview.md b/.agents/skills/xtend-to-java/workflow/overview.md new file mode 100644 index 000000000..ffc5024e6 --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/overview.md @@ -0,0 +1,186 @@ +# Conversion workflow overview + +This is the full end-to-end workflow for migrating Xtend files to Java in dsl-devkit. + +## Step 0 — Establish scope + +### Listing migration candidates + +To get a deterministic overview of remaining Xtend work, run one of these commands from the repository root. + +On Windows (PowerShell): + +```powershell +Get-ChildItem -Recurse -Filter "*.xtend" -Path "." | + Where-Object { $_.FullName -match "\\src\\" -and $_.FullName -notmatch "\\bin\\" } | + Group-Object { ($_.FullName -split '\\src\\')[0] | Split-Path -Leaf } | + ForEach-Object { + $totalLines = ($_.Group | ForEach-Object { (Get-Content $_.FullName | Measure-Object -Line).Lines } | Measure-Object -Sum).Sum + $type = if ($_.Name -match '\.test$') { 'test' } else { 'prod' } + [PSCustomObject]@{ Module = $_.Name; Type = $type; Files = $_.Count; Lines = $totalLines } + } | + Sort-Object Type, Lines | + Format-Table -AutoSize +``` + +On macOS / Linux (bash): + +```bash +find . -name "*.xtend" -path "*/src/*" -not -path "*/bin/*" \ + | awk -F'/src/' '{print $1}' \ + | sort | uniq -c | sort -n +``` + +Both list per-module Xtend file counts so you can plan slice scope. + +### Scoping the session + +Before touching any files, establish: + +1. **What files or modules are in scope for this session?** + Collect the list of `.xtend` source files to migrate. + +2. **What should the branch be called?** + Convention: `migrate/xtend-to-java/` (e.g., `migrate/xtend-to-java/check-core-test`). + +3. **Is there an existing migration branch with pre-converted files?** + If yes, you can pull already-converted `.java` files from it (Step 2 Option A). + +Do not proceed until you have a concrete file list and a branch name. + +--- + +## Step 1 — Cut the slice branch + +Always branch from **master** (or the project's main integration branch): + +```bash +SLICE=migrate/xtend-to-java/ +git fetch origin +git checkout -b "$SLICE" origin/master +``` + +For stacked multi-slice migrations, suffix the branch name with `-step-N` +(e.g. `migrate/xtend-to-java/check-core-step-1`, `...-step-2`) per the +project's stacked-PR convention. + +--- + +## Step 2 — Obtain the converted Java files + +### Option A — Pull converted files from a pre-converted branch + +```bash +git checkout -- ... +``` + +The converted files live at the same package path under `src/` with `.java` replacing `.xtend`. + +### Option B — Convert from scratch + +For each `.xtend` file, produce the Java equivalent by following Steps 3 and 4 below **in strict order**. +Do not write the Java file first and then vet it — read the references first, then write. + +--- + +## Step 3 — Read BOTH references BEFORE writing any Java + +> **HARD RULE — NO EXCEPTIONS, NO SHORTCUTS** +> +> You MUST read **both** the `.xtend` source AND the `xtend-gen/` Java output **in full** +> before writing a single line of migrated Java. This applies to **every file, every time** — +> even a 5-line utility class, even a file that "looks trivial", even if you think you +> already know what it does. +> +> **Why this is non-negotiable:** A 21-line Xtend file with a single template expression +> can produce completely different string output than what you'd guess from reading the +> Xtend source alone. Xtend's template whitespace rules are counter-intuitive — indentation +> is stripped, leading newlines are removed, and the only way to know the actual output is +> to read what the Xtend compiler produced in `xtend-gen/`. +> +> **There is no file small enough to skip this step.** + +### 3a. Read the Xtend source — understand intent and structure + +Read the original `.xtend` file **in full**. It tells you: +- The author's intent +- Which methods are dispatch methods +- Where extension methods, operator overloading, or `?.`/`?:` were used +- What the logical structure is + +### 3b. Read the `xtend-gen/` output — verify exact behavior + +Find the corresponding generated file under `xtend-gen/` (same package, same class name, `.java`). +**Read in full — no exceptions.** It tells you: +- The exact string output of every template expression (critical) +- Exactly which Java methods were generated for each dispatch variant +- The precise exception handling Xtend generated +- Which implicit null checks, type casts, and conversions Xtend inserted + +### 3c. Template whitespace — verify against `xtend-gen/` + +Xtend's template whitespace rules: + +1. **Indentation relative to a control structure is stripped.** If a template starts with `'''` followed + by a newline, indentation on subsequent lines relative to the opening mark is removed. + +2. **Lines containing only control structures produce no output.** + +3. **Newlines in appended strings get the current indentation prepended.** + +**The only reliable way to know what a template produces is to read the `xtend-gen/` output.** + +### 3d. Only NOW write the Java + +After reading both references, write Java that: +1. **Matches the `xtend-gen/` behavior exactly** for all string outputs, method signatures, and control flow +2. **Uses idiomatic Java** (text blocks, `.formatted()`, concatenation) instead of `StringConcatenation` +3. **Preserves the original Javadoc exactly** — never invent +4. **Follows the quality checklist** in [`workflow/validation-checklist.md`](./validation-checklist.md) + +--- + +## Step 4 — Apply the quality checklist + +See [`workflow/validation-checklist.md`](./validation-checklist.md) — all 30 rules must pass. + +--- + +## Step 5 — Build and verify + +Module-specific build first: +```bash +mvn -pl , -am verify -f ./ddk-parent/pom.xml > mvn-output.txt 2>&1 +``` + +**PMD needs compiled classes** for type-resolution rules (`MissingOverride`, `UnnecessaryCast`, +`LooseCoupling`, `UseCollectionIsEmpty`). Always compile first: +```bash +mvn clean compile pmd:check -f ./ddk-parent/pom.xml -pl -am > mvn-output.txt 2>&1 +``` + +Checkstyle works on source only: +```bash +mvn checkstyle:check -f ./ddk-parent/pom.xml -pl > mvn-output.txt 2>&1 +``` + +Full CI-equivalent: +```bash +mvn clean verify checkstyle:check pmd:pmd pmd:cpd pmd:check pmd:cpd-check spotbugs:check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end > mvn-output.txt 2>&1 +``` + +Always check the final `BUILD SUCCESS/FAILURE` line. With `--fail-at-end`, intermediate lines can +show `0 violations` while the build is still failing downstream. + +--- + +## Step 6 — Update module infrastructure + +When Xtend is fully removed from a module, update the module infrastructure. +See [`workflow/infrastructure-cleanup.md`](./infrastructure-cleanup.md). + +--- + +## Step 7 — Format, commit, and push + +See [`workflow/formatting-and-commit.md`](./formatting-and-commit.md). diff --git a/.agents/skills/xtend-to-java/workflow/validation-checklist.md b/.agents/skills/xtend-to-java/workflow/validation-checklist.md new file mode 100644 index 000000000..db5a25d46 --- /dev/null +++ b/.agents/skills/xtend-to-java/workflow/validation-checklist.md @@ -0,0 +1,134 @@ +# Validation checklist + +Run through this list before declaring a conversion done. Every item is a hard gate. + +--- + +## Quality rules (all 30 must pass) + +### Javadoc and comments + +| # | Rule | Requirement | +|---|------|-------------| +| 1 | Javadoc preservation | Copy Javadoc from Xtend source verbatim. Never generate/guess Javadoc that wasn't in the original. | +| 2 | `@throws` tags | Only add when method already has Javadoc AND migrated signature declares `throws`. Don't create Javadoc just for the tag. | + +### Types and variables + +| # | Rule | Requirement | +|---|------|-------------| +| 3 | No `val`/`var` | Always use explicit types. `var` is banned. | +| 4 | No unnecessary boxing | `Integer.valueOf(i)` only when signature requires boxed type. | +| 14 | LooseCoupling | Interface types (`List`, `Map`, `Set`, `EList`) in fields/params/returns, not `ArrayList`/`HashMap`/`BasicEList`. | +| 16 | No leading underscore on non-dispatch fields | Rename `_fieldName` → `fieldName`. | + +### String building + +| # | Rule | Requirement | +|---|------|-------------| +| 5 | String building idiom | Static single-line → literal; static multi-line → text block; interpolation without control flow → `.formatted()`; control flow → `StringBuilder`. | +| 6 | Text block `\` escape | Use `\` on last content line to suppress trailing `\n` when `xtend-gen/` shows the string doesn't end with newline. | +| 17 | MultipleStringLiterals | Tests: extract to `private static final String` constants. Generators: `CHECKSTYLE:CONSTANTS-OFF/ON`. | +| 20 | InsufficientStringBufferDeclaration | Size generously: 512 small methods, 2048 generators. | +| 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 `%`. | + +### Annotations and modifiers + +| # | Rule | Requirement | +|---|------|-------------| +| 7 | `@SuppressWarnings("nls")` | At class level when module's JDT settings have `nonExternalizedStringLiteral=warning`. Check `ddk-configuration/.settings/org.eclipse.jdt.core.prefs`. | +| 8 | `@Override` | On every override including interface implementations. | +| 15 | Dispatch methods | Keep `_` prefix. Suppress with `@SuppressWarnings({"checkstyle:MethodName", "PMD.UnusedFormalParameter"})`. | + +### Logging + +| # | Rule | Requirement | +|---|------|-------------| +| 9 | Parameterized Log4j2 | Use `{}` placeholders. Never concatenation or `.formatted()` in log calls. | + +### Imports + +| # | Rule | Requirement | +|---|------|-------------| +| 10 | No wildcard imports | Every import explicit. | +| 11 | Import order | `org.*`/`java.*`/`javax.*`/`junit.*` first, blank line, then `com.*`. Alphabetical within groups. | + +### Exception handling + +| # | Rule | Requirement | +|---|------|-------------| +| 12 | Preserved stack traces | `catch` blocks that re-throw must pass caught exception as cause. | +| 13 | try-with-resources | Any `AutoCloseable` — no manual `close()` in finally. | +| 18 | IllegalCatch | Catch specific exceptions, never generic `Exception`. | + +### Collections + +| # | Rule | Requirement | +|---|------|-------------| +| 19 | UseCollectionIsEmpty | `.isEmpty()` not `.size() == 0`. | + +### Extension methods and implicit behavior + +| # | Rule | Requirement | +|---|------|-------------| +| 24 | Extension methods | `obj.helperMethod()` → `helper.helperMethod(obj)`. Verify against `xtend-gen/`. | +| 25 | Implicit `it` in lambdas | Name the parameter explicitly in Java. | +| 26 | Implicit returns | Add explicit `return` statements. | +| 27 | Property access | `obj.name` → `obj.getName()`; `obj.name = x` → `obj.setName(x)`. | + +### Infrastructure and formatting + +| # | Rule | Requirement | +|---|------|-------------| +| 21 | Copyright headers | File starts with `/* Copyright (c) Avaloq Group AG ...` block. | +| 22 | Commit format | `refactor: migrate Xtend to Java - `. Single commit. | +| 28 | Infrastructure cleanup | Remove Xtend from MANIFEST.MF, build.properties, .classpath, .project; delete `xtend-gen/` directory (when module fully off Xtend). | + +--- + +## Per-file checklist (quick pass/fail) + +- [ ] All `val` converted to `final ExplicitType` (no `var` keyword). +- [ ] All `var` converted to `ExplicitType` (no `var` keyword). +- [ ] All `def` converted to Java methods with explicit visibility, return type, and `return` statements. +- [ ] All `override` converted to `@Override` annotation + visibility modifier. +- [ ] All `typeof(X)` converted to `X.class`. +- [ ] All `===` / `!==` converted to `==` / `!=`. +- [ ] All `==` (equality) converted to `.equals()` or `Objects.equals()`. +- [ ] All `?.` null-safe navigations converted to null checks. +- [ ] All `[...]` lambdas converted to `(...) -> {...}`. +- [ ] All `dispatch` methods converted to dispatcher pattern with `@SuppressWarnings`. +- [ ] All `extension` methods converted to explicit calls. +- [ ] All `static extension` imports converted to static calls at call site. +- [ ] All template expressions converted using the 4-tier decision tree. +- [ ] All `«IF» / «FOR» / «SEPARATOR»` converted to Java control flow. +- [ ] All `#[]` / `#{}` collection literals converted. +- [ ] All `=>` operator usages converted. +- [ ] 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 `::` 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 Javadoc preserved exactly. +- [ ] Copyright header preserved/added. +- [ ] Checked exceptions handled (`throws` clause or `try`/`catch` with specific types). +- [ ] The `.xtend` file is deleted; the `.java` file is added; both never coexist. + +--- + +## Build / test gates + +- [ ] `mvn -pl : -am -DskipTests compile -f ./ddk-parent/pom.xml` — passes. +- [ ] `mvn verify -f ./ddk-parent/pom.xml` — passes (or failures are known flakes). +- [ ] `mvn checkstyle:check pmd:check spotbugs:check -f ./ddk-parent/pom.xml` — passes. + +--- + +## Behaviour parity + +- [ ] Diff against the `xtend-gen/` output. The hand-converted version should be materially equivalent (often shorter, always more idiomatic). Any behaviour-shifting deviation must be intentional and called out in the commit message. diff --git a/.agents/sync.sh b/.agents/sync.sh new file mode 100755 index 000000000..55b9c7bed --- /dev/null +++ b/.agents/sync.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Mirror .agents/skills/ -> .claude/skills/ for Claude Code auto-discovery. +# macOS / Linux: single symlink. Windows (Git Bash): recursive copy. +# Idempotent. Re-run after any pull that changes .agents/skills/. + +set -eu + +# Anchor at repo root regardless of cwd, so `./sync.sh` from .agents/ +# and `./.agents/sync.sh` from root both behave identically. +cd "$(dirname "$0")/.." + +SKILL_SRC=".agents/skills" +SKILL_DST=".claude/skills" + +mkdir -p "$(dirname "$SKILL_DST")" + +case "$(uname -s)" in + Darwin|Linux) + if [ -L "$SKILL_DST" ]; then + if [ "$(readlink "$SKILL_DST")" = "../$SKILL_SRC" ]; then + echo "skills: symlink already correct, no-op" + exit 0 + fi + rm "$SKILL_DST" + elif [ -e "$SKILL_DST" ]; then + echo "skills: $SKILL_DST exists and is not the expected symlink; refusing to overwrite" >&2 + exit 1 + fi + ln -s "../$SKILL_SRC" "$SKILL_DST" + echo "skills: symlinked $SKILL_DST -> ../$SKILL_SRC" + ;; + MINGW*|CYGWIN*|MSYS*) + # Windows: recursive copy (Git symlinks unreliable without Developer Mode). + rm -rf "$SKILL_DST" + mkdir -p "$SKILL_DST" + cp -R "$SKILL_SRC"/. "$SKILL_DST"/ + echo "skills: copied $SKILL_SRC -> $SKILL_DST" + ;; + *) + echo "skills: unsupported OS $(uname -s)" >&2 + exit 1 + ;; +esac diff --git a/.gitignore b/.gitignore index 345e13fc0..b71c920bc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ # macOS .DS_Store + +# Local mirror of .agents/skills/ created by .agents/sync.sh — do not commit +# Match both the directory form (Windows copy) and the symlink form (macOS/Linux). +/.claude/skills +/.claude/skills/ diff --git a/AGENTS.md b/AGENTS.md index 80d0688d9..42c365335 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,16 @@ This document helps AI coding agents work effectively with the DSL DevKit codeba - **Tycho** - **Xtext/Xtend** +## Setup + +After cloning, run: + +```bash +./.agents/sync.sh +``` + +This creates a local mirror of `.agents/skills/` at `.claude/skills/` (gitignored) so Claude Code auto-discovers the project's skills. macOS/Linux use a symlink; Windows gets a recursive copy. Re-run after any pull that touches `.agents/skills/`. + ## Key Directories | Directory | Purpose |