Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .agents/skills/xtend-to-java/SKILL.md
Original file line number Diff line number Diff line change
@@ -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)
104 changes: 104 additions & 0 deletions .agents/skills/xtend-to-java/examples/00-basic-generator.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 55 additions & 0 deletions .agents/skills/xtend-to-java/examples/01-template-with-for-loop.md
Original file line number Diff line number Diff line change
@@ -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<Argument> 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<Argument> 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<Argument> 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).
Original file line number Diff line number Diff line change
@@ -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<String>`) 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<T>) () -> iter` (keep as-is for Xtext patterns) |
| `StringExtensions.isNullOrEmpty(s)` | `s == null \|\| s.isEmpty()` |
| `StringExtensions.toFirstUpper(s)` | `Character.toUpperCase(s.charAt(0)) + s.substring(1)` |
| `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.
Loading