From c2d0b101b06ab68c87deb0c6f9307dd1cd19501d Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:57:41 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 117 ++ skills/recipe-writer/SKILL.md | 1198 +++++++++++++++++ .../example-declarative-migration.yml | 257 ++++ .../examples/example-say-hello-recipe.java | 186 +++ .../examples/example-scanning-recipe.java | 294 ++++ .../examples/example-yaml-github-actions.java | 282 ++++ .../checklist-recipe-development.md | 198 +++ .../references/common-patterns.md | 356 +++++ .../references/java-lst-reference.md | 420 ++++++ .../references/jsonpath-patterns.md | 524 +++++++ .../references/trait-implementation-guide.md | 328 +++++ .../references/yaml-lst-reference.md | 736 ++++++++++ .../scripts/add_license_header.sh | 144 ++ skills/recipe-writer/scripts/init_recipe.py | 299 ++++ .../recipe-writer/scripts/validate_recipe.py | 331 +++++ .../templates/license-header.txt | 13 + .../templates/template-declarative-recipe.yml | 122 ++ .../templates/template-imperative-recipe.java | 200 +++ .../templates/template-recipe-test.java | 292 ++++ .../templates/template-refaster-template.java | 134 ++ 22 files changed, 6446 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/recipe-writer/SKILL.md create mode 100644 skills/recipe-writer/examples/example-declarative-migration.yml create mode 100644 skills/recipe-writer/examples/example-say-hello-recipe.java create mode 100644 skills/recipe-writer/examples/example-scanning-recipe.java create mode 100644 skills/recipe-writer/examples/example-yaml-github-actions.java create mode 100644 skills/recipe-writer/references/checklist-recipe-development.md create mode 100644 skills/recipe-writer/references/common-patterns.md create mode 100644 skills/recipe-writer/references/java-lst-reference.md create mode 100644 skills/recipe-writer/references/jsonpath-patterns.md create mode 100644 skills/recipe-writer/references/trait-implementation-guide.md create mode 100644 skills/recipe-writer/references/yaml-lst-reference.md create mode 100755 skills/recipe-writer/scripts/add_license_header.sh create mode 100755 skills/recipe-writer/scripts/init_recipe.py create mode 100755 skills/recipe-writer/scripts/validate_recipe.py create mode 100644 skills/recipe-writer/templates/license-header.txt create mode 100644 skills/recipe-writer/templates/template-declarative-recipe.yml create mode 100644 skills/recipe-writer/templates/template-imperative-recipe.java create mode 100644 skills/recipe-writer/templates/template-recipe-test.java create mode 100644 skills/recipe-writer/templates/template-refaster-template.java diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..16e77d7 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "openrewrite", + "description": "Claude Code plugin", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Scott", + "email": "scott.jungling@gmail.com" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7453100 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# openrewrite + +Claude Code plugin diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..d443b5a --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,117 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:sjungling/claude-plugins:plugins/openrewrite", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "59d3c9205611bca47a9101d92d65052375cf4ed2", + "treeHash": "c6d9af92a961ff7c5242b74ad3322250c29c8c6f973a6863f2e4e59effb0bc6c", + "generatedAt": "2025-11-28T10:28:23.454536Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "openrewrite", + "description": "Claude Code plugin", + "version": null + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "cbed6a9ebeb3cd9ddd3110880b2b808291888e41e198efd9fbf068289bb2964f" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "dd229ba7883bf4e7d96961bb61f4c40a1ffa477277633f1ce19133758a569dbd" + }, + { + "path": "skills/recipe-writer/SKILL.md", + "sha256": "ea021984f43c33e7006699e202d594b70f9c181eb78529e2b9e61bf30afeffad" + }, + { + "path": "skills/recipe-writer/references/yaml-lst-reference.md", + "sha256": "0caec5f36e881788ede3a812c1282b809f47408eb07c5a26772e792e695ed43e" + }, + { + "path": "skills/recipe-writer/references/checklist-recipe-development.md", + "sha256": "98966975f7e17b603d2fbb9c42935ad4f00162fc486841cf05b53a2b2013414e" + }, + { + "path": "skills/recipe-writer/references/java-lst-reference.md", + "sha256": "18e9a8c8c44371fd5833dc360674a0bb40ec4b235dd165b342b0ab374a5bc1b7" + }, + { + "path": "skills/recipe-writer/references/common-patterns.md", + "sha256": "3db2b41ff3a520267be613a4dc5b03c394cd301d5a5079e019c2fcae8a46af2b" + }, + { + "path": "skills/recipe-writer/references/trait-implementation-guide.md", + "sha256": "01cf52ae5563aa7e2bf7444382a73d656acd972e4bdf080db4c7465fbeee475c" + }, + { + "path": "skills/recipe-writer/references/jsonpath-patterns.md", + "sha256": "a0c9d1d442bacfae7f826453500e7ad972e91f27856b12e9a6a9d27d268447d4" + }, + { + "path": "skills/recipe-writer/examples/example-say-hello-recipe.java", + "sha256": "12b384c30934e1ff2d874cc6f65a10d0cbd699453cb079ebc379ec9d7949f120" + }, + { + "path": "skills/recipe-writer/examples/example-yaml-github-actions.java", + "sha256": "1bde1be463c3c592b932aec72adfd4ceb14b5407c17c30de323d8611db128e18" + }, + { + "path": "skills/recipe-writer/examples/example-scanning-recipe.java", + "sha256": "c87fe6c1dc09ceda8ba75e4099504eddd89da1db52cc17ee2bb79de96dc48809" + }, + { + "path": "skills/recipe-writer/examples/example-declarative-migration.yml", + "sha256": "ec94034dea27e0e647fe66f4dd6420c57fb1b64d926abe187232d81b69bdb388" + }, + { + "path": "skills/recipe-writer/scripts/validate_recipe.py", + "sha256": "c151691b679d3fed7acc0c48e72dd484288c895af1aeaf177de84bb5c13b7909" + }, + { + "path": "skills/recipe-writer/scripts/add_license_header.sh", + "sha256": "68e49928677590a3d2b3019b9db1241c904d46c6614eccbe2e700af6a86c4d04" + }, + { + "path": "skills/recipe-writer/scripts/init_recipe.py", + "sha256": "ca1799803b8ebc95a8f61cdf35d953b3129d715027a3c44d9eecd88c0c6a4b4f" + }, + { + "path": "skills/recipe-writer/templates/template-declarative-recipe.yml", + "sha256": "a149b6067cf18e294b97491acc3ff69ef4583adf345e933743f64409185f67c2" + }, + { + "path": "skills/recipe-writer/templates/template-recipe-test.java", + "sha256": "2d14970ad7fc2bed1116d597acc210b58f3f49449b43cfd175b840aa90e28f9c" + }, + { + "path": "skills/recipe-writer/templates/template-imperative-recipe.java", + "sha256": "62d03d6bc123da32a6b15a8db3769cbf550554933d248b38175901b76ac8bd6c" + }, + { + "path": "skills/recipe-writer/templates/license-header.txt", + "sha256": "a6715cfa80f257d614b735b70b703bea21b5ddbe006e1532c6354d4542d4a032" + }, + { + "path": "skills/recipe-writer/templates/template-refaster-template.java", + "sha256": "cc8cef2c45479f5cad81494013532a2cfe5813c1219910bc2c8aa15ffe2a07d6" + } + ], + "dirSha256": "c6d9af92a961ff7c5242b74ad3322250c29c8c6f973a6863f2e4e59effb0bc6c" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/recipe-writer/SKILL.md b/skills/recipe-writer/SKILL.md new file mode 100644 index 0000000..8c85116 --- /dev/null +++ b/skills/recipe-writer/SKILL.md @@ -0,0 +1,1198 @@ +--- +name: recipe-writer +description: Expert in test-first development of production-quality OpenRewrite recipes for automated code refactoring. Automatically activates when working with OpenRewrite recipe files, Java/YAML files in `src/main/java/**/rewrite/**` directories, writing tests implementing `RewriteTest`, or when users ask about recipe development, writing recipes, creating migrations, LST manipulation, JavaTemplate usage, visitor patterns, preconditions, scanning recipes, YAML recipes, GitHub Actions transformations, Kubernetes manifest updates, or code migration strategies. Guides recipe type selection (declarative/Refaster/imperative), visitor implementation, and test-driven development workflows. +--- + +# OpenRewrite Recipe Writing Expert + +## Overview + +Create production-quality OpenRewrite recipes using test-first development. This skill combines comprehensive coverage of all recipe types (declarative, Refaster, imperative) with deep domain expertise in Java and YAML transformations. + +**Core Principle:** Write tests first (RED), implement minimally (GREEN), apply OpenRewrite idioms (REFACTOR). + +## When to Use This Skill + +Explicitly invoke this skill for: + +- **Planning recipes** - Determining the best recipe type for a use case +- **Implementing recipes** - Writing recipe classes, visitors, and JavaTemplate code +- **Writing tests** - Creating comprehensive test coverage with RewriteTest +- **YAML transformations** - GitHub Actions, Kubernetes manifests, CI/CD configs +- **Java refactoring** - Code migrations, API updates, framework modernization +- **Debugging recipes** - Troubleshooting visitor behavior, type checking, or preconditions +- **Converting recipe types** - Analyzing if an imperative recipe can be declarative +- **Understanding OpenRewrite concepts** - Learning about LSTs, cursors, traits, or scanning patterns + +## When NOT to Use This Skill + +Do NOT invoke this skill for: + +- **General Java programming questions** - Use standard Java knowledge unless specifically about OpenRewrite LST manipulation + - ❌ "How do I parse JSON in Java?" + - ✅ "How do I parse Java code into LSTs?" +- **General YAML editing** - Use standard Edit tools for direct file modifications + - ❌ "Edit this YAML file to change the value" + - ✅ "Create a recipe to update GitHub Actions across all repositories" +- **Running OpenRewrite recipes** - This skill is for authoring recipes, not executing them + - ❌ "How do I run the Maven plugin?" + - ✅ "How do I test my recipe runs correctly?" +- **Build tool configuration** - Unless directly related to recipe publishing/distribution + - ❌ "How do I configure Gradle for my project?" + - ✅ "How do I publish my recipe to Maven Central?" +- **General refactoring questions** - Only use for OpenRewrite recipe implementation + - ❌ "What's the best way to refactor this code?" + - ✅ "What recipe type should I use for this refactoring?" +- **Reading/understanding existing code** - Unless analyzing recipe implementation + - ❌ "Explain what this Spring controller does" + - ✅ "Explain what this JavaIsoVisitor is doing" + +## Quick Examples + +Here are example requests that activate this skill: + +**Planning:** +- "I need to migrate from JUnit 4 to JUnit 5 - help me plan the recipe" +- "What's the best recipe type to replace all ArrayList with List?" +- "Should I use a declarative or imperative recipe for adding annotations?" +- "Help me create a recipe to update GitHub Actions to use Node 20" + +**Java Implementation:** +- "Write a recipe that adds @Deprecated to classes in com.example.old package" +- "Show me how to use JavaTemplate to add a method to a class" +- "Create a recipe that changes method return types from Optional to nullable" + +**YAML Implementation:** +- "Create a recipe to update all GitHub Actions checkout actions to v4" +- "Write a recipe to change Kubernetes image tags across all manifests" +- "Build a recipe to migrate Travis CI configs to GitHub Actions" + +**Debugging:** +- "My recipe isn't matching the expected classes - help debug" +- "Why is my JavaTemplate throwing a parse error?" +- "The recipe runs but doesn't make any changes - what's wrong?" +- "My YAML recipe isn't preserving comments - how do I fix it?" + +**Testing:** +- "Write tests for a ScanningRecipe that analyzes multiple files" +- "How do I test a recipe that requires external classpath dependencies?" +- "Show me how to test edge cases in my YAML recipe" + +## Recipe Type Selection + +Choose the appropriate recipe type based on your needs. + +### Decision Tree + +``` +Start here + ├─ Can I compose existing recipes? ───────────────────┐ + │ YES → Use Declarative YAML │ + │ NO ↓ │ + ├─ Is it a simple expression/statement replacement? ───┤ + │ YES → Use Refaster Template │ + │ NO ↓ │ + └─ Do I need custom logic or conditional changes? ─────┤ + YES → Use Imperative Java Recipe │ + │ +Still unsure? → Start with declarative, fall back to ─────┘ + imperative only when necessary +``` + +### Recipe Type Comparison + +| Type | Speed | Complexity | Use Cases | Examples | +|------|-------|------------|-----------|----------| +| **Declarative YAML** | Fastest | Lowest | Composing existing recipes | Framework migrations, standard refactorings | +| **Refaster Template** | Fast | Low-Medium | Expression/statement replacements | API updates, method call changes | +| **Imperative Java** | Slower | High | Complex transformations, conditional logic | Custom analysis, YAML LST manipulation | + +### Declarative YAML Recipes (Preferred) + +**Use when:** Composing existing recipes with configuration + +**Advantages:** +- No code required +- Simple and maintainable +- Fast execution +- Easy to understand + +**Example use case:** Combining framework migration steps + +```yaml +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.MyMigration +displayName: Migrate to Framework X +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: old.Type + newFullyQualifiedTypeName: new.Type + - com.yourorg.OtherRecipe +``` + +**Common declarative recipes:** +- Java: `ChangeType`, `ChangeMethodName`, `AddDependency`, `UpgradeDependencyVersion` +- YAML: `FindKey`, `FindValue`, `ChangeKey`, `ChangeValue`, `DeleteKey`, `MergeYaml`, `CopyValue` + +### Refaster Template Recipes + +**Use when:** Simple expression/statement replacements with type awareness + +**Advantages:** +- Faster than imperative recipes +- Type-aware matching +- Concise syntax +- Good for API migrations + +**Example use case:** Replace `StringUtils.equals()` with `Objects.equals()` + +```java +public class StringUtilsToObjects { + @BeforeTemplate + boolean before(String s1, String s2) { + return StringUtils.equals(s1, s2); + } + + @AfterTemplate + boolean after(String s1, String s2) { + return Objects.equals(s1, s2); + } +} +``` + +### Imperative Java Recipes + +**Use when:** Complex logic, conditional transformations, custom analysis, or YAML/LST manipulation + +**Advantages:** +- Full control over transformation logic +- Complex transformations possible +- Access to full LST structure +- Can implement custom matching + +**Example use case:** +- Add modifiers only to variables that aren't reassigned +- Transform YAML based on complex conditions +- Generate new files based on analysis +- Multi-file coordination with ScanningRecipe + +**Decision Rule:** If it can be declarative, make it declarative. Use Refaster for simple replacements. Use imperative only when necessary. + +## Test-First Development Workflow + +Follow the RED-GREEN-REFACTOR cycle for recipe development: + +``` +Phase 1: RED (Write Failing Tests) + ↓ +Phase 2: DECIDE (Select Recipe Type) + ↓ +Phase 3: GREEN (Minimal Implementation) + ↓ +Phase 4: REFACTOR (Apply OpenRewrite Idioms) + ↓ +Phase 5: DOCUMENT (Add Metadata & Examples) + ↓ +Phase 6: VALIDATE (Production Readiness) +``` + +### Phase 1: RED - Write Failing Tests + +Start with tests before writing any recipe code. This ensures you understand the transformation and can verify correctness. + +**For Java recipes:** + +```java +class YourRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new YourRecipe("parameter-value")); + } + + @Test + void makesExpectedChange() { + rewriteRun( + //language=java + java( + // Before + """ + package com.example; + class Before { } + """, + // After + """ + package com.example; + class After { } + """ + ) + ); + } + + @Test + void doesNotChangeWhenNotNeeded() { + rewriteRun( + //language=java + java( + """ + package com.example; + class AlreadyCorrect { } + """ + // No second argument = no change expected + ) + ); + } +} +``` + +**For YAML recipes:** + +```java +class YourYamlRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new YourYamlRecipe()); + } + + @Test + void updatesGitHubActionsCheckout() { + rewriteRun( + //language=yaml + yaml( + """ + jobs: + build: + steps: + - uses: actions/checkout@v2 + """, + """ + jobs: + build: + steps: + - uses: actions/checkout@v4 + """ + ) + ); + } +} +``` + +**Test Checklist:** +- [ ] Write happy path test (simplest transformation) +- [ ] Include edge cases (nulls, empty files, missing elements) +- [ ] Test no-op scenarios (recipe shouldn't change unrelated code) +- [ ] Test multi-document YAML if relevant +- [ ] Include real-world examples if domain is known +- [ ] Run tests to confirm RED state - tests must fail initially + +**Key Principle:** Start with simplest possible before/after. Add complexity incrementally. + +### Phase 2: DECIDE - Select Recipe Type + +Use the decision tree above to choose between declarative, Refaster, or imperative. + +**Ask yourself:** +1. Can this be done by composing existing recipes? → Declarative +2. Is it a simple expression/statement replacement? → Refaster +3. Does it need custom logic or LST manipulation? → Imperative + +**For YAML-specific decisions:** +- Simple value changes, key renames → Declarative (use `ChangeValue`, `ChangeKey`) +- Complex JsonPath matching with conditions → Imperative +- Multi-step YAML transformations → Declarative (compose multiple recipes) +- Dynamic YAML generation → Imperative + +### Phase 3: GREEN - Minimal Implementation + +Implement just enough to make tests pass. Don't optimize or refactor yet. + +**For declarative recipes:** + +1. Create YAML file in `src/main/resources/META-INF/rewrite/` +2. Compose existing recipes +3. Run tests to verify GREEN state + +**For imperative Java recipes:** + +Use templates from `./templates/` directory: +- `template-imperative-recipe.java` - Complete recipe structure +- `template-recipe-test.java` - Test structure + +**Automation:** Use `./scripts/init_recipe.py ` to generate boilerplate. + +**For YAML recipes, extend YamlIsoVisitor:** + +```java +public class YourYamlRecipe extends Recipe { + + @Override + public String getDisplayName() { + return "Your recipe display name"; + } + + @Override + public String getDescription() { + return "Description of transformation."; + } + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + entry = super.visitMappingEntry(entry, ctx); + + // Match specific key + if ("targetKey".equals(entry.getKey().getValue())) { + // Safe value access + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + if ("oldValue".equals(scalar.getValue())) { + return entry.withValue( + scalar.withValue("newValue") + ); + } + } + } + + return entry; + } + }; + } +} +``` + +**Verification:** Run tests to achieve GREEN state - all tests must pass. + +### Phase 4: REFACTOR - Apply OpenRewrite Idioms + +Now improve the recipe using OpenRewrite best practices. Don't skip GREEN to do this - refactoring comes AFTER tests pass. + +**Refactoring Checklist:** + +**1. Trait Usage** (Advanced) +- [ ] Can this recipe implement an existing trait? +- [ ] Should a new trait be created for reusable matching logic? +- [ ] Separate "what to find" (trait) from "what to do" (recipe) + +See `./references/trait-implementation-guide.md` for details. + +**2. Recipe Composition** +- [ ] Can parts be extracted into smaller, composable recipes? +- [ ] Are there opportunities for configurability (parameters)? +- [ ] Could this be split into search recipe + modification recipe? + +**3. OpenRewrite Conventions** +- [ ] Recipe has clear `displayName` and `description` (both support markdown) +- [ ] Parameters use `@Option` annotations with descriptions and examples +- [ ] Properly handles `null` values and missing elements +- [ ] Preserves formatting and comments where possible +- [ ] Uses `@Value` and `@EqualsAndHashCode(callSuper = false)` for immutability +- [ ] `getVisitor()` returns NEW instance (never cached) + +**4. Performance Considerations** +- [ ] Minimize LST traversals (don't visit more than necessary) +- [ ] Use preconditions to skip files that won't match +- [ ] Return original object if no changes made (identity check) + +**5. YAML-Specific Refactoring** +- [ ] Use JsonPath matching for complex patterns +- [ ] Handle multi-document YAML if relevant +- [ ] Preserve YAML anchors and aliases +- [ ] Test with real-world files (GitHub Actions, K8s, etc.) + +**Verification:** +- [ ] All tests still pass after refactoring +- [ ] Recipe follows OpenRewrite naming conventions +- [ ] Code is cleaner and more maintainable + +### Phase 5: DOCUMENT - Add Metadata & Examples + +Add comprehensive documentation to make the recipe discoverable and understandable. + +**Recipe Metadata (supports markdown):** + +```java +@Override +public String getDisplayName() { + return "Update GitHub Actions to `actions/checkout@v4`."; +} + +@Override +public String getDescription() { + return "Updates all uses of `actions/checkout@v2` and `actions/checkout@v3` to `actions/checkout@v4`.\n\n" + + "**Before:**\n```yaml\n- uses: actions/checkout@v2\n```\n\n" + + "**After:**\n```yaml\n- uses: actions/checkout@v4\n```"; +} +``` + +**Option Documentation:** + +```java +@Option( + displayName = "Old action reference", + description = "The old action reference to replace (e.g., `actions/checkout@v2`).", + example = "actions/checkout@v2" +) +String oldActionRef; +``` + +**Javadoc:** +- Add class-level Javadoc with use cases +- Show before/after transformations +- Document parameter effects +- Link to related recipes + +**Naming Conventions:** +- Display names: Sentence case, code in backticks, end with period +- Recipe names: `com.yourorg.VerbNoun` (e.g., `com.yourorg.UpdateGitHubActions`) + +### Phase 6: VALIDATE - Production Readiness + +Use the comprehensive checklist to ensure production quality. + +**Quick Validation:** +- [ ] All tests pass (GREEN state maintained) +- [ ] Recipe handles edge cases gracefully (no NPEs) +- [ ] Formatting/comments preserved in output +- [ ] Documentation is clear and includes examples +- [ ] Recipe is idempotent (same result on repeated runs) + +**Full Validation:** +See `./references/checklist-recipe-development.md` for 200+ validation items. + +**License Headers:** +Check for `{repository_root}/gradle/licenseHeader.txt`. If exists, use `./scripts/add_license_header.sh` to add headers. + +## Implementation Patterns + +Quick reference for common implementation patterns. + +### Java Recipes + +**Set Up Recipe Class:** + +```java +@Value +@EqualsAndHashCode(callSuper = false) +public class YourRecipe extends Recipe { + + @Option(displayName = "Parameter Name", + description = "Clear description.", + example = "com.example.Type") + String parameterName; + + @Override + public String getDisplayName() { + return "Your recipe display name."; + } + + @Override + public String getDescription() { + return "What this recipe does."; + } + + @Override + public TreeVisitor getVisitor() { + return new YourVisitor(); + } +} +``` + +**Implement Visitor:** + +```java +public class YourVisitor extends JavaIsoVisitor { + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + // ALWAYS call super to traverse the tree + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + // Check if change is needed (do no harm) + if (!shouldChange(cd)) { + return cd; + } + + // Make changes using JavaTemplate or LST methods + cd = makeChanges(cd); + + return cd; + } +} +``` + +**Use JavaTemplate:** + +```java +private final JavaTemplate template = JavaTemplate + .builder("public String hello() { return \"Hello from #{}!\"; }") + .build(); + +// In visitor method: +classDecl = template.apply( + new Cursor(getCursor(), classDecl.getBody()), + classDecl.getBody().getCoordinates().lastStatement(), + fullyQualifiedClassName +); +``` + +**Add Preconditions:** + +```java +@Override +public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + new UsesType<>("com.example.Type", true), + new UsesJavaVersion<>(17) + ), + new YourVisitor() + ); +} +``` + +### YAML Recipes + +**Basic YAML Visitor:** + +```java +public class YourYamlRecipe extends Recipe { + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + entry = super.visitMappingEntry(entry, ctx); + + // Match key + if ("targetKey".equals(entry.getKey().getValue())) { + // Safe value access + String value = entry.getValue() instanceof Yaml.Scalar ? + ((Yaml.Scalar) entry.getValue()).getValue() : null; + + if ("oldValue".equals(value)) { + return entry.withValue( + ((Yaml.Scalar) entry.getValue()).withValue("newValue") + ); + } + } + + return entry; + } + }; + } +} +``` + +**JsonPath Matching:** + +```java +public class GitHubActionsRecipe extends Recipe { + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + + private final JsonPathMatcher matcher = + new JsonPathMatcher("$.jobs.*.steps[*].uses"); + + @Override + public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) { + scalar = super.visitScalar(scalar, ctx); + + if (matcher.matches(getCursor())) { + String value = scalar.getValue(); + if (value != null && value.startsWith("actions/checkout@v2")) { + return scalar.withValue(value.replace("@v2", "@v4")); + } + } + + return scalar; + } + }; + } +} +``` + +**Common JsonPath Patterns:** + +See `./references/jsonpath-patterns.md` for comprehensive patterns including: +- GitHub Actions: `$.jobs.*.steps[*].uses`, `$.on.push.branches` +- Kubernetes: `$.spec.template.spec.containers[*].image`, `$.metadata.labels` +- Generic YAML: `$.databases.*.connection.host`, `$[?(@.enabled == true)]` + +### ScanningRecipe Pattern + +Use when you need to see all files before making changes, generate new files, or share data across files. + +```java +@Value +@EqualsAndHashCode(callSuper = false) +public class YourScanningRecipe extends ScanningRecipe { + + public static class YourAccumulator { + Map fileData = new HashMap<>(); + } + + @Override + public YourAccumulator getInitialValue(ExecutionContext ctx) { + return new YourAccumulator(); + } + + @Override + public TreeVisitor getScanner(YourAccumulator acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + // Collect data into accumulator + return tree; + } + }; + } + + @Override + public TreeVisitor getVisitor(YourAccumulator acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + // Use data from accumulator to make changes + return tree; + } + }; + } +} +``` + +For complete example, see `./examples/example-scanning-recipe.java`. + +## Testing Recipes + +### Test Structure + +Use the RewriteTest interface for all recipe tests. + +```java +class YourRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new YourRecipe("parameter-value")); + } + + @Test + void makesExpectedChange() { + rewriteRun( + //language=java + java( + // Before + """ + package com.example; + class Before { } + """, + // After + """ + package com.example; + class After { } + """ + ) + ); + } + + @Test + void doesNotChangeWhenNotNeeded() { + rewriteRun( + //language=java + java( + """ + package com.example; + class AlreadyCorrect { } + """ + // No second argument = no change expected + ) + ); + } +} +``` + +### Testing Best Practices + +- **Test both changes AND no-changes cases** - Ensure recipe doesn't modify unrelated code +- **Test edge cases** - Nulls, empty files, missing elements, multi-document YAML +- **Test harness runs multiple cycles** - Ensures idempotence automatically +- **Add `//language=XXX` comments** - Helps IDE syntax highlight test code +- **Use text blocks properly** - End `"""` delimiter one indent to right of open delimiter + +### YAML-Specific Testing + +**Multi-document YAML:** + +```java +@Test +void handlesMultiDocumentYaml() { + rewriteRun( + //language=yaml + yaml( + """ + --- + first: document + --- + second: document + """, + """ + --- + first: updated + --- + second: updated + """ + ) + ); +} +``` + +**Null value handling:** + +```java +@Test +void handlesNullValues() { + rewriteRun( + //language=yaml + yaml( + """ + key: null + another: + """ + // Should not crash or change + ) + ); +} +``` + +**Comment preservation:** + +```java +@Test +void preservesComments() { + rewriteRun( + //language=yaml + yaml( + """ + # Important comment + key: oldValue + """, + """ + # Important comment + key: newValue + """ + ) + ); +} +``` + +For more testing patterns, see `./references/testing-patterns.md`. + +## Advanced Features + +### OpenRewrite Traits + +Traits provide semantic abstractions over LST elements, wrapping them with domain-specific logic. + +**When to use traits:** +- You need reusable matching logic across multiple recipes +- You want to separate "what to find" from "what to do" +- You're working with complex LST patterns repeatedly + +**Basic trait structure:** + +```java +@Value +public class YourTrait implements Trait { + Cursor cursor; + + // Domain-specific accessor + public String getClassName() { + return getTree().getSimpleName(); + } + + // Nested Matcher class + public static class Matcher extends SimpleTraitMatcher { + @Override + protected @Nullable YourTrait test(Cursor cursor) { + J.ClassDeclaration cd = cursor.getValue(); + // Custom matching logic + if (matchesCondition(cd)) { + return new YourTrait(cursor); + } + return null; + } + } +} +``` + +**Using traits in recipes:** + +```java +@Override +public TreeVisitor getVisitor() { + return new YourTrait.Matcher().asVisitor((trait, ctx) -> { + String className = trait.getClassName(); + // Use semantic API instead of raw LST navigation + return trait.getTree(); + }); +} +``` + +**IMPORTANT:** Never use deprecated `Traits` utility classes. Always instantiate matchers directly: + +```java +// ❌ Old (deprecated): +Traits.literal() + +// ✅ New (preferred): +new Literal.Matcher() +``` + +For complete trait implementation guide, see `./references/trait-implementation-guide.md`. + +### Preconditions (Performance Optimization) + +Preconditions filter files before running the recipe, improving performance. + +**Common preconditions:** + +```java +// Only run on files using specific type +new UsesType<>("com.example.Type", true) + +// Only run on files with specific method +new UsesMethod<>("com.example.Type methodName(..)") + +// Only run on specific Java version +new UsesJavaVersion<>(17) + +// Only run on YAML files +new FindSourceFiles("**/*.yml", "**/*.yaml") +``` + +**Combining preconditions:** + +```java +@Override +public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.and( + new UsesType<>("com.example.Type", true), + new UsesJavaVersion<>(11) + ), + new YourVisitor() + ); +} +``` + +### JavaTemplate Deep Dive + +JavaTemplate compiles code snippets once and applies them to LST elements. + +**When to use:** +- Adding new code structures (methods, statements, expressions) +- Complex code generation that's tedious with LST methods + +**When NOT to use:** +- Modifying existing elements (use `.withX()` methods) +- Simple changes (reordering, renaming, removing) + +**Template syntax:** + +```java +// Untyped substitution (strings) +JavaTemplate.builder("System.out.println(#{})") + .build(); + +// Typed substitution (LST elements) +JavaTemplate.builder("return #{any(java.lang.String)}") + .build(); + +// With imports +JavaTemplate.builder("List list = new ArrayList<>()") + .imports("java.util.List", "java.util.ArrayList") + .build(); + +// With classpath +JavaTemplate.builder("@Deprecated(since = \"2.0\")") + .javaParser(JavaParser.fromJavaVersion().classpath("library-name")) + .build(); + +// Context-sensitive (references local scope) +JavaTemplate.builder("localVariable.method()") + .contextSensitive() + .build(); +``` + +**Applying templates:** + +```java +// Apply to statement position +method.withBody( + template.apply( + new Cursor(getCursor(), method.getBody()), + method.getBody().getCoordinates().lastStatement(), + args + ) +); +``` + +**Tips:** +- Context-free templates (default) are faster +- Use `.contextSensitive()` only when referencing local variables/methods +- Declare all imports explicitly +- Escape special characters in template strings + +### State Management + +**Within visitor (intra-visitor state):** + +```java +// Store state +getCursor().putMessage("key", value); + +// Retrieve state from cursor hierarchy +Object value = getCursor().getNearestMessage("key"); +``` + +**Between visitors (ScanningRecipe):** + +Use accumulator pattern - see ScanningRecipe section above. + +**Never:** +- Use ExecutionContext for visitor state +- Mutate recipe instance fields +- Use static variables + +### Multi-Module Projects + +Track data per-project, not globally: + +```java +public static class Accumulator { + // ✅ Per-project tracking + Map fileData = new HashMap<>(); + + // ❌ Don't assume single project + // boolean globalFlag; +} +``` + +## Troubleshooting + +Common issues and solutions when developing recipes. + +### Recipe Not Running on Expected Files + +**Symptoms:** Recipe doesn't execute on files you expect to change + +**Solutions:** +1. Check preconditions - might be too restrictive +2. Verify file matches visitor type (JavaIsoVisitor for Java, YamlIsoVisitor for YAML) +3. Add debug logging to visitor methods +4. Ensure the file contains the patterns you're looking for +5. For YAML: Check file extension (`.yml` vs `.yaml`) + +### JavaTemplate Parse Errors + +**Symptoms:** Template fails to compile or apply + +**Solutions:** +1. Check imports are declared with `.imports()` +2. Verify classpath includes all referenced types with `.javaParser()` +3. Use `.contextSensitive()` if referencing local scope +4. Escape special characters in template strings +5. Ensure placeholder syntax is correct: + - `#{}` for strings + - `#{any(Type)}` for LST elements + +### Tests Passing But Recipe Doesn't Work in Real Code + +**Symptoms:** RewriteTest passes but recipe fails on actual projects + +**Solutions:** +1. Add more realistic test cases with complex code +2. Test with external dependencies via parser configuration +3. Verify preconditions match real-world usage +4. Check for edge cases not covered in tests +5. Test with different Java versions if version-specific + +### Recipe Makes Changes But Not Idempotent + +**Symptoms:** Running recipe multiple times produces different results each time + +**Solutions:** +1. Ensure all checks use referential equality (return unchanged LST if no change needed) +2. Verify visitor doesn't accumulate state between invocations +3. Check that `getVisitor()` returns a NEW instance each time +4. Ensure recipe class is immutable (uses `@Value`) +5. Test with `rewriteRun()` which automatically runs multiple cycles + +### Type Information Not Available + +**Symptoms:** `TypeUtils.isOfClassType()` returns false when it should be true + +**Solutions:** +1. Ensure test configures classpath with required dependencies +2. Check that imports are present in the source file +3. Verify the type binding resolved correctly (check for `null` types) +4. Add explicit type attribution if working with dynamic code + +### YAML Recipe Not Matching Expected Elements + +**Symptoms:** YAML recipe doesn't find or transform expected YAML elements + +**Solutions:** +1. Check JsonPath pattern is correct - test with online JsonPath evaluator +2. Verify you're using correct LST visitor method (visitScalar vs visitMappingEntry) +3. Handle null values safely - YAML allows `key: null` and `key:` +4. Check for multi-document YAML (starts with `---`) +5. Verify you're calling `super.visitX()` to traverse tree + +### YAML Comments or Formatting Not Preserved + +**Symptoms:** Recipe changes YAML but loses comments or formatting + +**Solutions:** +1. Never create new LST elements - always use `.withX()` methods +2. Use `ListUtils` for list operations, never mutate directly +3. Return original element if no change needed (identity check) +4. Check you're not replacing entire nodes unnecessarily + +For more troubleshooting guidance, see `./references/troubleshooting-guide.md`. + +## Critical Best Practices + +### Do No Harm +- If unsure whether a change is safe, DON'T make it +- Make minimal, least invasive changes +- Respect existing formatting and comments +- Return unchanged LST if no change needed + +### Immutability & Idempotence +- Recipes must be immutable (no mutable state) +- Same input → same output, always +- Use `@Value` and `@EqualsAndHashCode(callSuper = false)` +- `getVisitor()` must return NEW instance each time + +### Never Mutate LSTs +```java +// WRONG +method.getArguments().remove(0); + +// CORRECT +method.withArguments(ListUtils.map(method.getArguments(), (i, arg) -> + i == 0 ? null : arg +)); +``` + +### Naming Conventions +- Display names: Sentence case, code in backticks, end with period +- Example: "Change type from `OldType` to `NewType`." +- Recipe names: `com.yourorg.VerbNoun` (e.g., `com.yourorg.ChangePackage`) + +## Accessing Bundled Resources + +This skill uses progressive disclosure to minimize token usage. Load resources on demand: + +### Templates (for boilerplate code) +- `templates/template-imperative-recipe.java` - Complete recipe class structure +- `templates/template-declarative-recipe.yml` - YAML recipe format +- `templates/template-refaster-template.java` - Refaster template structure +- `templates/template-recipe-test.java` - Test class using RewriteTest +- `templates/license-header.txt` - Standard license header + +### Examples (for working patterns) +- `examples/example-say-hello-recipe.java` - Simple recipe with JavaTemplate +- `examples/example-scanning-recipe.java` - Multi-file analysis pattern +- `examples/example-yaml-github-actions.java` - YAML domain example +- `examples/example-declarative-migration.yml` - Framework migration + +### References (for detailed guidance) +- `references/java-lst-reference.md` - Java LST structure and hierarchy +- `references/yaml-lst-reference.md` - YAML LST structure and hierarchy +- `references/jsonpath-patterns.md` - Domain-specific JsonPath patterns +- `references/trait-implementation-guide.md` - Advanced trait patterns +- `references/checklist-recipe-development.md` - 200+ validation items +- `references/common-patterns.md` - Copy-paste code snippets +- `references/testing-patterns.md` - Test patterns and edge cases +- `references/troubleshooting-guide.md` - Issue diagnosis and solutions + +### When to Load Resources + +| Resource Type | Typical Size | When to Load | +|--------------|-------------|--------------| +| SKILL.md | ~3,500 tokens | Always (auto) | +| Templates | ~500 tokens each | On demand (Read tool) | +| Examples | ~1,000 tokens each | On demand (Read tool) | +| References | ~1,500-3,000 tokens each | On demand (Read tool) | + +**Best practice:** Only read templates/examples when actively working on implementation. The SKILL.md content provides sufficient guidance for planning and decision-making. + +## Quick Reference + +### Key Classes + +| Class | Purpose | +|-------|---------| +| `Recipe` | Base class for all recipes | +| `JavaIsoVisitor` | Most common Java visitor type | +| `YamlIsoVisitor` | Most common YAML visitor type | +| `JavaTemplate` | Generate Java code snippets | +| `RewriteTest` | Testing interface | +| `ScanningRecipe` | Multi-file analysis pattern | +| `JsonPathMatcher` | Match YAML/JSON paths | + +### Key Methods + +| Method | Purpose | +|--------|---------| +| `getVisitor()` | Returns visitor instance (must be NEW) | +| `super.visitX()` | Traverse subtree | +| `.withX()` | Create modified LST copy (immutable) | +| `ListUtils.map()` | Transform lists without mutation | +| `doAfterVisit()` | Chain additional visitors | +| `maybeAddImport()` | Add import if not present | +| `maybeRemoveImport()` | Remove import if unused | +| `getCursor().putMessage()` | Store intra-visitor state | + +### Common Patterns + +For quick reference on frequently used patterns, see: +- `references/common-patterns.md` - Import management, visitor chaining, type checking +- `references/jsonpath-patterns.md` - GitHub Actions, Kubernetes, CI/CD patterns + +## Automation Scripts + +Use helper scripts for common tasks: + +- **`./scripts/init_recipe.py `** - Generate recipe boilerplate (class, test file, optional YAML) +- **`./scripts/validate_recipe.py [path]`** - Validate recipe structure, naming, Java compatibility +- **`./scripts/add_license_header.sh [file]`** - Add license headers from `gradle/licenseHeader.txt` + +## Token Budget Awareness + +This skill is optimized for token efficiency: + +- **SKILL.md**: ~3,500 tokens (loaded when skill activates) +- **Templates**: Load only when creating new recipes +- **Examples**: Load only when learning specific patterns +- **References**: Load only when you need deep dives or validation + +**Strategy:** Start with SKILL.md guidance. Load templates for boilerplate. Load references for troubleshooting or advanced features. + +## Remember + +- **Always start with tests (RED)** +- **Try declarative before Refaster before imperative** +- **Apply idioms during REFACTOR, not GREEN** +- **Document with markdown for clarity** +- **Validate production-readiness before completion** +- **Preserve formatting and comments** +- **Do no harm - when in doubt, don't change** diff --git a/skills/recipe-writer/examples/example-declarative-migration.yml b/skills/recipe-writer/examples/example-declarative-migration.yml new file mode 100644 index 0000000..bce117a --- /dev/null +++ b/skills/recipe-writer/examples/example-declarative-migration.yml @@ -0,0 +1,257 @@ +--- +# ============================================================================== +# Example: Framework Migration Recipe +# ============================================================================== +# This example demonstrates a realistic declarative recipe for migrating +# from one version of a framework to another, combining multiple recipes +# with configuration. +# +# Save this file in: src/main/resources/META-INF/rewrite/ + +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.MigrateToFrameworkX +displayName: Migrate to Framework X 2.0 +description: | + Migrates applications from Framework X 1.x to 2.0. This recipe performs the following steps: + - Updates dependency versions + - Migrates renamed packages and types + - Updates deprecated API usage + - Applies code formatting +tags: + - framework-x + - migration + - upgrade +estimatedEffortPerOccurrence: PT15M + +recipeList: + # Step 1: Update dependencies + - org.openrewrite.java.dependencies.ChangeDependency: + oldGroupId: com.example.frameworkx + oldArtifactId: frameworkx-core + newGroupId: com.example.frameworkx + newArtifactId: frameworkx-core + newVersion: 2.0.x + + - org.openrewrite.java.dependencies.AddDependency: + groupId: com.example.frameworkx + artifactId: frameworkx-new-module + version: 2.0.x + onlyIfUsing: com.example.frameworkx.NewFeature + configuration: implementation + + # Step 2: Package and type migrations + - org.openrewrite.java.ChangePackage: + oldPackageName: com.example.frameworkx.old + newPackageName: com.example.frameworkx.v2 + recursive: true + + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: com.example.frameworkx.OldConfig + newFullyQualifiedTypeName: com.example.frameworkx.v2.NewConfig + + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: com.example.frameworkx.OldClient + newFullyQualifiedTypeName: com.example.frameworkx.v2.Client + + # Step 3: Method migrations + - org.openrewrite.java.ChangeMethodName: + methodPattern: com.example.frameworkx.v2.Client execute(..) + newMethodName: run + + # Step 4: Apply custom recipes (if you have any) + # - com.yourorg.CustomFrameworkXMigration + + # Step 5: Format the code + - org.openrewrite.java.format.AutoFormat + +--- +# ============================================================================== +# Example: Security Fixes Recipe +# ============================================================================== +# This recipe applies common security fixes across a codebase + +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.ApplySecurityFixes +displayName: Apply security best practices +description: Applies a collection of security-related fixes and improvements to the codebase. +tags: + - security + - SAST + +recipeList: + # Use secure random number generation + - org.openrewrite.java.security.SecureRandom + + # Fix SQL injection vulnerabilities + - org.openrewrite.java.security.UseFileCreateTempFile + + # Apply static analysis security fixes + - org.openrewrite.staticanalysis.CommonStaticAnalysis + + # Remove unused imports + - org.openrewrite.java.format.RemoveUnusedImports + +--- +# ============================================================================== +# Example: Testing Framework Migration +# ============================================================================== +# Migrates from JUnit 4 to JUnit 5 + +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.MigrateToJUnit5 +displayName: Migrate to JUnit 5 +description: Migrates JUnit 4 tests to JUnit 5 (Jupiter). +tags: + - junit + - testing + - junit5 + +preconditions: + - org.openrewrite.java.search.UsesType: + fullyQualifiedTypeName: org.junit.Test + +recipeList: + # Update dependencies + - org.openrewrite.java.dependencies.RemoveDependency: + groupId: junit + artifactId: junit + + - org.openrewrite.java.dependencies.AddDependency: + groupId: org.junit.jupiter + artifactId: junit-jupiter + version: 5.9.x + onlyIfUsing: org.junit.Test + configuration: testImplementation + + # Migrate annotations and assertions + - org.openrewrite.java.testing.junit5.JUnit4to5Migration + + # Format + - org.openrewrite.java.format.AutoFormat + +--- +# ============================================================================== +# Example: Code Quality Improvements +# ============================================================================== +# A collection of code quality improvements + +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.ImproveCodeQuality +displayName: Improve code quality +description: Applies a comprehensive set of code quality improvements. +tags: + - code-quality + - refactoring + - best-practices + +recipeList: + # Static analysis + - org.openrewrite.staticanalysis.CommonStaticAnalysis + + # Finalize local variables that aren't reassigned + - org.openrewrite.staticanalysis.FinalizeLocalVariables + + # Add missing @Override annotations + - org.openrewrite.staticanalysis.MissingOverrideAnnotation + + # Simplify boolean expressions + - org.openrewrite.staticanalysis.SimplifyBooleanExpression + + # Remove unnecessary null checks + - org.openrewrite.staticanalysis.UnnecessaryNullCheckBeforeInstanceOf + + # Format code + - org.openrewrite.java.format.AutoFormat + +--- +# ============================================================================== +# Example: Simple Type Replacement +# ============================================================================== +# A focused recipe that just replaces one type with another + +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.ReplaceStringUtils +displayName: Replace Apache Commons StringUtils with Spring StringUtils +description: Replaces usage of Apache Commons StringUtils with Spring Framework's StringUtils. + +recipeList: + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: org.apache.commons.lang3.StringUtils + newFullyQualifiedTypeName: org.springframework.util.StringUtils + + - org.openrewrite.java.dependencies.AddDependency: + groupId: org.springframework + artifactId: spring-core + version: 6.0.x + onlyIfUsing: org.springframework.util.StringUtils + configuration: implementation + +--- +# ============================================================================== +# TEST EXAMPLE +# ============================================================================== +# Here's how to test a declarative recipe: +# +# ```java +# package com.yourorg; +# +# import org.junit.jupiter.api.Test; +# import org.openrewrite.java.JavaParser; +# import org.openrewrite.test.RecipeSpec; +# import org.openrewrite.test.RewriteTest; +# +# import static org.openrewrite.java.Assertions.java; +# +# class MigrateToFrameworkXTest implements RewriteTest { +# +# @Override +# public void defaults(RecipeSpec spec) { +# spec +# // Load the recipe from resources +# .recipeFromResources("com.yourorg.MigrateToFrameworkX") +# // Add dependencies needed to compile the "before" code +# .parser(JavaParser.fromJavaVersion() +# .classpath("frameworkx-core-1.0")); +# } +# +# @Test +# void migratesFrameworkXCode() { +# rewriteRun( +# java( +# """ +# package com.example; +# +# import com.example.frameworkx.old.OldConfig; +# +# class Example { +# OldConfig config; +# } +# """, +# """ +# package com.example; +# +# import com.example.frameworkx.v2.NewConfig; +# +# class Example { +# NewConfig config; +# } +# """ +# ) +# ); +# } +# } +# ``` + +# ============================================================================== +# TIPS FOR DECLARATIVE RECIPES +# ============================================================================== +# 1. Keep them focused - one migration or fix per recipe +# 2. Use meaningful names that describe what the recipe does +# 3. Document the purpose and steps in the description +# 4. Add tags for searchability +# 5. Use preconditions to avoid running on irrelevant files +# 6. Order matters - recipes run sequentially +# 7. Consider adding AutoFormat at the end +# 8. Test each recipe thoroughly +# 9. Wrap string values in quotes if they contain special characters +# 10. Use estimatedEffortPerOccurrence to help users understand the impact diff --git a/skills/recipe-writer/examples/example-say-hello-recipe.java b/skills/recipe-writer/examples/example-say-hello-recipe.java new file mode 100644 index 0000000..a6585bc --- /dev/null +++ b/skills/recipe-writer/examples/example-say-hello-recipe.java @@ -0,0 +1,186 @@ +package com.yourorg; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.NonNull; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.J; + +/** + * A simple recipe that adds a hello() method to a specified class. + * + * This example demonstrates: + * - Basic recipe structure with @Value and @EqualsAndHashCode + * - Using @Option for configurable parameters + * - Using JavaTemplate for code generation + * - Checking preconditions before making changes + * - Following the "do no harm" principle + * + * Based on the official OpenRewrite tutorial: + * https://docs.openrewrite.org/authoring-recipes/writing-a-java-refactoring-recipe + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class SayHelloRecipe extends Recipe { + + @Option( + displayName = "Fully Qualified Class Name", + description = "A fully qualified class name indicating which class to add a hello() method to.", + example = "com.yourorg.FooBar" + ) + @NonNull + String fullyQualifiedClassName; + + @JsonCreator + public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) { + this.fullyQualifiedClassName = fullyQualifiedClassName; + } + + @Override + public String getDisplayName() { + return "Say 'Hello'"; + } + + @Override + public String getDescription() { + return "Adds a \"hello\" method to the specified class."; + } + + @Override + public TreeVisitor getVisitor() { + // Always return a new instance - never cache visitors + return new JavaIsoVisitor() { + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + // Step 1: Traverse the subtree first + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + // Step 2: Check if this is the class we're looking for + // Do no harm: return unchanged if this isn't the target class + if (cd.getType() == null || !cd.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) { + return cd; + } + + // Step 3: Check if the class already has a hello() method + // Do no harm: don't add if it already exists + boolean helloMethodExists = cd.getBody().getStatements().stream() + .filter(statement -> statement instanceof J.MethodDeclaration) + .map(J.MethodDeclaration.class::cast) + .anyMatch(methodDeclaration -> "hello".equals(methodDeclaration.getName().getSimpleName())); + + if (helloMethodExists) { + return cd; + } + + // Step 4: Add the hello() method using JavaTemplate + // The template uses #{} for parameter substitution + J.Block body = JavaTemplate.apply( + "public String hello() { return \"Hello from #{}!\"; }", + new Cursor(getCursor(), cd.getBody()), + cd.getBody().getCoordinates().lastStatement(), + fullyQualifiedClassName + ); + + return cd.withBody(body); + } + }; + } +} + +// ============================================================ +// TEST CLASS +// ============================================================ + +/* +package com.yourorg; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class SayHelloRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SayHelloRecipe("com.yourorg.FooBar")); + } + + @DocumentExample + @Test + void addsHelloToFooBar() { + rewriteRun( + java( + """ + package com.yourorg; + + class FooBar { + } + """, + """ + package com.yourorg; + + class FooBar { + public String hello() { + return "Hello from com.yourorg.FooBar!"; + } + } + """ + ) + ); + } + + @Test + void doesNotChangeExistingHello() { + rewriteRun( + java( + """ + package com.yourorg; + + class FooBar { + public String hello() { return ""; } + } + """ + ) + ); + } + + @Test + void doesNotChangeOtherClasses() { + rewriteRun( + java( + """ + package com.yourorg; + + class OtherClass { + } + """ + ) + ); + } +} +*/ + +// ============================================================ +// YAML USAGE +// ============================================================ + +/* +Save this in src/main/resources/META-INF/rewrite/say-hello.yml: + +--- +type: specs.openrewrite.org/v1beta/recipe +name: com.yourorg.SayHelloToFooBar +displayName: Say Hello to FooBar +description: Adds a hello() method to the FooBar class. +recipeList: + - com.yourorg.SayHelloRecipe: + fullyQualifiedClassName: com.yourorg.FooBar +*/ diff --git a/skills/recipe-writer/examples/example-scanning-recipe.java b/skills/recipe-writer/examples/example-scanning-recipe.java new file mode 100644 index 0000000..76731e2 --- /dev/null +++ b/skills/recipe-writer/examples/example-scanning-recipe.java @@ -0,0 +1,294 @@ +package com.yourorg; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +import java.util.HashMap; +import java.util.Map; + +/** + * A ScanningRecipe that marks classes only if the project uses a specific type. + * + * This example demonstrates: + * - ScanningRecipe with accumulator pattern + * - Three-phase execution: scan, generate (optional), edit + * - Tracking per-project information with Map + * - Using accumulator data to inform editing decisions + * - Proper handling of multi-module projects + * + * Use ScanningRecipe when you need to: + * - See all files before making changes + * - Generate new files based on analysis + * - Share data across multiple files + * - Make decisions based on project-wide information + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class ScanningRecipeExample extends ScanningRecipe { + + /** + * The accumulator stores data collected during the scanning phase. + * This data is then available during the editing phase. + * + * Important: For multi-module projects, track data per JavaProject. + */ + public static class Accumulator { + // Track which projects use the target type + Map projectUsesTargetType = new HashMap<>(); + + // You can store any data structure you need + // Map> projectClasses = new HashMap<>(); + // Map> projectMethods = new HashMap<>(); + } + + @Override + public String getDisplayName() { + return "Mark classes in projects using target type"; + } + + @Override + public String getDescription() { + return "Marks classes with SearchResult only if the project uses a specific type."; + } + + /** + * Initialize the accumulator before scanning begins. + */ + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + /** + * Phase 1: SCANNING + * + * The scanner visits all source files and collects information + * into the accumulator. No changes are made in this phase. + */ + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new JavaIsoVisitor() { + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + // Get the JavaProject marker to track per-project data + JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null); + if (project == null) { + return cu; + } + + // Initialize project tracking if needed + acc.projectUsesTargetType.putIfAbsent(project, false); + + // Scan for the target type in this file + // In this example, we're looking for usage of java.util.Optional + cu.getImports().stream() + .filter(imp -> imp.getTypeName().equals("java.util.Optional")) + .findFirst() + .ifPresent(imp -> acc.projectUsesTargetType.put(project, true)); + + // Continue scanning subtree + return super.visitCompilationUnit(cu, ctx); + } + + @Override + public J.Import visitImport(J.Import import_, ExecutionContext ctx) { + // You can also scan at finer granularity + // Example: track specific imports, method calls, etc. + return import_; + } + }; + } + + /** + * Phase 2: GENERATING (optional) + * + * This phase is used to generate new source files based on the + * accumulated data. Return null if no new files are needed. + * + * Uncomment to implement: + */ + /* + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + List newFiles = new ArrayList<>(); + + // Example: Generate a file for each project that uses the target type + for (Map.Entry entry : acc.projectUsesTargetType.entrySet()) { + if (entry.getValue()) { + JavaProject project = entry.getKey(); + // Create a new source file + // newFiles.add(createNewFile(project)); + } + } + + return newFiles; + } + */ + + /** + * Phase 3: EDITING + * + * The editor visits all source files again and makes changes + * based on the data collected in the scanning phase. + */ + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new JavaIsoVisitor() { + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + // Get the project for this file + JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null); + if (project == null) { + return cu; + } + + // Check the accumulator to see if this project uses the target type + Boolean usesTargetType = acc.projectUsesTargetType.get(project); + if (usesTargetType == null || !usesTargetType) { + // Don't make changes in projects that don't use the target type + return cu; + } + + // This project uses the target type, continue with editing + return super.visitCompilationUnit(cu, ctx); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + // Get the project + JavaProject project = getCursor() + .firstEnclosing(J.CompilationUnit.class) + .getMarkers() + .findFirst(JavaProject.class) + .orElse(null); + + if (project == null) { + return cd; + } + + // Only mark classes in projects that use the target type + Boolean usesTargetType = acc.projectUsesTargetType.get(project); + if (usesTargetType != null && usesTargetType) { + // Mark this class with a SearchResult + return SearchResult.found(cd, "Found in project using Optional"); + } + + return cd; + } + }; + } +} + +// ============================================================ +// SIMPLIFIED EXAMPLE: COUNT CLASSES +// ============================================================ + +/* +Here's a simpler ScanningRecipe that counts classes per project: + +@Value +@EqualsAndHashCode(callSuper = false) +public class CountClasses extends ScanningRecipe { + + public static class Accumulator { + Map classCounts = new HashMap<>(); + } + + @Override + public String getDisplayName() { + return "Count classes per project"; + } + + @Override + public String getDescription() { + return "Counts the number of classes in each project."; + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) { + JavaProject project = getCursor() + .firstEnclosing(J.CompilationUnit.class) + .getMarkers() + .findFirst(JavaProject.class) + .orElse(null); + + if (project != null) { + acc.classCounts.merge(project, 1, Integer::sum); + } + + return cd; + } + }; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + // Print the results + return new JavaIsoVisitor() { + private boolean printed = false; + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + if (!printed) { + System.out.println("Class counts by project:"); + acc.classCounts.forEach((project, count) -> + System.out.println(project.getProjectName() + ": " + count + " classes") + ); + printed = true; + } + return cu; + } + }; + } +} +*/ + +// ============================================================ +// KEY TAKEAWAYS +// ============================================================ + +/* +1. ScanningRecipe has THREE phases: + - Scan: Collect data (no changes) + - Generate: Create new files (optional) + - Edit: Make changes based on collected data + +2. Always track per-project data with Map + - Don't assume single project per repository + - Get JavaProject from markers + +3. Accumulator is shared across all phases + - Use it to pass data from scan to edit + - Keep it simple and focused + +4. Scanner makes NO changes + - Only collect information + - Mark files or store data + +5. Editor uses accumulator data + - Make informed decisions + - Can access all collected information + +6. When multiple ScanningRecipes are in a recipe list: + - All scanners run first + - Then all generators run + - Then all editors run + - Scanners see state before ANY edits + - But scanners DO see generated files +*/ diff --git a/skills/recipe-writer/examples/example-yaml-github-actions.java b/skills/recipe-writer/examples/example-yaml-github-actions.java new file mode 100644 index 0000000..1b2cd4c --- /dev/null +++ b/skills/recipe-writer/examples/example-yaml-github-actions.java @@ -0,0 +1,282 @@ +package com.example.rewrite; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.yaml.JsonPathMatcher; +import org.openrewrite.yaml.YamlIsoVisitor; +import org.openrewrite.yaml.tree.Yaml; + +/** + * Example recipe demonstrating YAML manipulation for GitHub Actions workflows. + * + * This recipe updates GitHub Actions checkout action from v2/v3 to v4. + * + * Before: + * ```yaml + * jobs: + * build: + * steps: + * - uses: actions/checkout@v2 + * ``` + * + * After: + * ```yaml + * jobs: + * build: + * steps: + * - uses: actions/checkout@v4 + * ``` + * + * Key Concepts Demonstrated: + * 1. YamlIsoVisitor for YAML LST manipulation + * 2. JsonPathMatcher for targeted YAML element matching + * 3. Safe value access with type checking + * 4. Preserving formatting and comments + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class UpdateGitHubActionsCheckout extends Recipe { + + @Override + public String getDisplayName() { + return "Update GitHub Actions to `actions/checkout@v4`."; + } + + @Override + public String getDescription() { + return "Updates all uses of `actions/checkout@v2` and `actions/checkout@v3` to `actions/checkout@v4`.\n\n" + + "**Before:**\n```yaml\n- uses: actions/checkout@v2\n```\n\n" + + "**After:**\n```yaml\n- uses: actions/checkout@v4\n```"; + } + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + + // JsonPath to match the 'uses' field in GitHub Actions steps + private final JsonPathMatcher matcher = + new JsonPathMatcher("$.jobs.*.steps[*].uses"); + + @Override + public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) { + // Always call super to traverse the tree + scalar = super.visitScalar(scalar, ctx); + + // Check if this scalar matches our JsonPath + if (matcher.matches(getCursor())) { + String value = scalar.getValue(); + + // Safe null check + if (value != null) { + // Update v2 to v4 + if (value.startsWith("actions/checkout@v2")) { + return scalar.withValue(value.replace("@v2", "@v4")); + } + // Update v3 to v4 + if (value.startsWith("actions/checkout@v3")) { + return scalar.withValue(value.replace("@v3", "@v4")); + } + } + } + + return scalar; + } + }; + } +} + +/** + * Additional YAML recipe examples demonstrating other common patterns: + */ + +/** + * Example: Update Kubernetes container image tags + * + * Before: + * ```yaml + * spec: + * containers: + * - image: myapp:1.0.0 + * ``` + * + * After: + * ```yaml + * spec: + * containers: + * - image: myapp:2.0.0 + * ``` + */ +@Value +@EqualsAndHashCode(callSuper = false) +class UpdateKubernetesImageTag extends Recipe { + + String oldTag; + String newTag; + + @Override + public String getDisplayName() { + return "Update Kubernetes image tag."; + } + + @Override + public String getDescription() { + return "Updates Kubernetes container image tags."; + } + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + + private final JsonPathMatcher matcher = + new JsonPathMatcher("$.spec.template.spec.containers[*].image"); + + @Override + public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) { + scalar = super.visitScalar(scalar, ctx); + + if (matcher.matches(getCursor())) { + String value = scalar.getValue(); + if (value != null && value.endsWith(":" + oldTag)) { + return scalar.withValue( + value.substring(0, value.lastIndexOf(":")) + ":" + newTag + ); + } + } + + return scalar; + } + }; + } +} + +/** + * Example: Change key name in YAML + * + * Before: + * ```yaml + * oldKey: value + * ``` + * + * After: + * ```yaml + * newKey: value + * ``` + */ +@Value +@EqualsAndHashCode(callSuper = false) +class ChangeYamlKey extends Recipe { + + String oldKey; + String newKey; + + @Override + public String getDisplayName() { + return "Change YAML key name."; + } + + @Override + public String getDescription() { + return "Renames a YAML key while preserving its value."; + } + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + entry = super.visitMappingEntry(entry, ctx); + + // Check if this entry has the old key + if (oldKey.equals(entry.getKey().getValue())) { + // Replace the key while preserving everything else + return entry.withKey( + ((Yaml.Scalar) entry.getKey()).withValue(newKey) + ); + } + + return entry; + } + }; + } +} + +/** + * Example: Update value based on key match + * + * Before: + * ```yaml + * database: + * host: localhost + * ``` + * + * After: + * ```yaml + * database: + * host: prod-db.example.com + * ``` + */ +@Value +@EqualsAndHashCode(callSuper = false) +class UpdateYamlValue extends Recipe { + + String keyPath; + String oldValue; + String newValue; + + @Override + public String getDisplayName() { + return "Update YAML value."; + } + + @Override + public String getDescription() { + return "Updates a YAML value at a specific key path."; + } + + @Override + public TreeVisitor getVisitor() { + return new YamlIsoVisitor() { + + private final JsonPathMatcher matcher = new JsonPathMatcher(keyPath); + + @Override + public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) { + scalar = super.visitScalar(scalar, ctx); + + if (matcher.matches(getCursor())) { + String value = scalar.getValue(); + if (oldValue.equals(value)) { + return scalar.withValue(newValue); + } + } + + return scalar; + } + }; + } +} + +/** + * Common JsonPath Patterns for YAML Recipes: + * + * GitHub Actions: + * - $.jobs.*.steps[*].uses - All 'uses' in steps + * - $.on.push.branches - Push trigger branches + * - $.jobs.*.runs-on - Runner configuration + * - $.jobs.*.strategy.matrix - Matrix strategy + * + * Kubernetes: + * - $.spec.template.spec.containers[*].image - Container images + * - $.metadata.labels - Labels + * - $.spec.replicas - Replica count + * - $.spec.template.spec.containers[*].env[*] - Environment variables + * + * Generic YAML: + * - $.databases.*.connection.host - Nested configuration + * - $[?(@.enabled == true)] - Conditional matching + * - $..*[?(@.type == 'service')] - Deep search with condition + * + * See references/jsonpath-patterns.md for comprehensive examples. + */ diff --git a/skills/recipe-writer/references/checklist-recipe-development.md b/skills/recipe-writer/references/checklist-recipe-development.md new file mode 100644 index 0000000..cda8266 --- /dev/null +++ b/skills/recipe-writer/references/checklist-recipe-development.md @@ -0,0 +1,198 @@ +# OpenRewrite Recipe Development Checklist + +Use this checklist to ensure you've covered all important aspects of recipe development. + +## Planning Phase + +### Recipe Type Selection +- [ ] Determined if recipe can be declarative (preferred) +- [ ] Evaluated if Refaster template would work for simple replacements +- [ ] Confirmed imperative recipe is necessary for complex logic +- [ ] Identified which LST elements need to be visited +- [ ] Reviewed existing recipes to avoid duplication + +### Requirements Gathering +- [ ] Clearly defined what the recipe should change +- [ ] Identified what should NOT be changed +- [ ] Documented expected input and output +- [ ] Listed any dependencies or external types needed +- [ ] Determined if multi-file analysis is required (ScanningRecipe) + +## Implementation Phase + +### Recipe Class Structure +- [ ] Used `@Value` and `@EqualsAndHashCode(callSuper = false)` for immutability +- [ ] All fields are final (via Lombok or manual implementation) +- [ ] Added `@JsonCreator` constructor with `@JsonProperty` annotations +- [ ] Defined `@Option` fields with clear descriptions and examples +- [ ] Implemented `getDisplayName()` with sentence-case name +- [ ] Implemented `getDescription()` with clear, period-ending description +- [ ] `getVisitor()` returns NEW instance (never cached) + +### Visitor Implementation +- [ ] Chose correct visitor type (JavaIsoVisitor vs JavaVisitor) +- [ ] Called `super.visitX()` in overridden visit methods +- [ ] Checked for null before accessing type information +- [ ] Implemented "do no harm" - return unchanged LST when unsure +- [ ] Used `.withX()` methods instead of mutating LSTs +- [ ] Used `ListUtils` instead of stream operations on LST collections +- [ ] Avoided creating new lists unnecessarily + +### JavaTemplate Usage (if applicable) +- [ ] Used typed substitution `#{any(Type)}` for LST elements +- [ ] Used untyped substitution `#{}` for strings +- [ ] Declared all imports with `.imports()` +- [ ] Declared static imports with `.staticImports()` +- [ ] Configured parser with `.javaParser()` if referencing external types +- [ ] Added classpath dependencies or stubs as needed +- [ ] Used context-free templates (default) when possible +- [ ] Only used `.contextSensitive()` when necessary + +### Advanced Features (if applicable) +- [ ] Added preconditions with `Preconditions.check()` +- [ ] Used `UsesType` or `UsesMethod` to filter applicable files +- [ ] Implemented cursor messaging for intra-visitor communication +- [ ] For ScanningRecipe: defined accumulator with `Map` +- [ ] For ScanningRecipe: implemented `getInitialValue()` +- [ ] For ScanningRecipe: implemented `getScanner()` (no changes, only collect) +- [ ] For ScanningRecipe: implemented `getVisitor()` (uses accumulator data) +- [ ] For ScanningRecipe: implemented `generate()` if creating new files + +### Imports and Dependencies +- [ ] Used `maybeAddImport()` for new types +- [ ] Used `maybeRemoveImport()` for removed types +- [ ] Chained visitors with `doAfterVisit()` when needed + +## Testing Phase + +### Test Structure +- [ ] Created test class implementing `RewriteTest` +- [ ] Implemented `defaults(RecipeSpec)` with recipe configuration +- [ ] Added `@DocumentExample` to primary test + +### Test Coverage +- [ ] Test for expected changes (before → after) +- [ ] Test for no changes when not applicable (before only) +- [ ] Test for edge cases and boundary conditions +- [ ] Test with multiple files +- [ ] Test that recipe doesn't change already-correct code +- [ ] Test with different parameter values (if applicable) +- [ ] Test with different Java versions (if version-specific) +- [ ] Added classpath dependencies with `spec.parser()` + +### Test Quality +- [ ] Test names clearly describe what is being tested +- [ ] Used meaningful package and class names in test code +- [ ] Included comments explaining complex test scenarios +- [ ] Verified tests pass (including multi-cycle verification) +- [ ] Checked that recipe is idempotent (runs produce same result) + +## Code Quality Phase + +### Best Practices +- [ ] Recipe follows "do no harm" principle +- [ ] Recipe makes minimal, least invasive changes +- [ ] Recipe is immutable (no mutable state) +- [ ] Recipe is idempotent (same input → same output) +- [ ] Recipe never mutates LSTs +- [ ] Recipe respects existing formatting +- [ ] Used referential equality checks (same object = no change) + +### Naming Conventions +- [ ] Display name uses sentence case +- [ ] Display name uses backticks around code elements +- [ ] Display name ends with period (if complete sentence) +- [ ] Description is clear and concise +- [ ] Recipe class name follows `VerbNoun` pattern +- [ ] Package name follows `com.yourorg.category` pattern + +### Performance +- [ ] Added preconditions to skip irrelevant files +- [ ] Used context-free JavaTemplates when possible +- [ ] Avoided unnecessary LST allocations +- [ ] Recipe completes work in single cycle (no `causesAnotherCycle`) +- [ ] For ScanningRecipe: minimized accumulator size + +### Multi-Module Support +- [ ] For ScanningRecipe: tracked data per `JavaProject` +- [ ] Did not assume single project per repository +- [ ] Retrieved JavaProject from markers correctly + +## Documentation Phase + +### Code Documentation +- [ ] Added class-level JavaDoc describing the recipe +- [ ] Documented all `@Option` fields clearly +- [ ] Added inline comments for complex logic +- [ ] Included usage example in JavaDoc + +### External Documentation +- [ ] Created YAML file in `src/main/resources/META-INF/rewrite/` (if distributing) +- [ ] Added recipe to catalog/index (if applicable) +- [ ] Documented any known limitations +- [ ] Added tags for categorization + +## Distribution Phase + +### Build Configuration +- [ ] Recipe compiles with Java 8 target (or appropriate version) +- [ ] Added `-parameters` compiler flag for Jackson +- [ ] Included rewrite-recipe-bom for dependency management +- [ ] Tests run successfully with build tool + +### Publishing +- [ ] Published to local Maven repository for testing (`publishToMavenLocal`) +- [ ] Tested recipe in separate project +- [ ] Configured publishing to artifact repository (if applicable) +- [ ] Tagged release in version control + +## Final Verification + +### Smoke Testing +- [ ] Ran recipe on sample project +- [ ] Verified changes are correct +- [ ] Verified no unwanted changes were made +- [ ] Checked that formatting is preserved +- [ ] Ran recipe multiple times (idempotence check) + +### Common Pitfalls Avoided +- [ ] Did not mutate LSTs directly +- [ ] Did not cache visitor instances +- [ ] Did not use ExecutionContext for visitor state +- [ ] Did not forget to call `super.visitX()` +- [ ] Did not create unnecessary list allocations +- [ ] Did not forget imports in JavaTemplate +- [ ] Did not skip null checks on type information +- [ ] Did not assume single project per repository + +## Recipe-Specific Checklists + +### For Declarative Recipes +- [ ] Saved in `src/main/resources/META-INF/rewrite/` +- [ ] Used `type: specs.openrewrite.org/v1beta/recipe` +- [ ] All recipe names are fully qualified +- [ ] All parameters are correctly indented +- [ ] String values with special characters are quoted +- [ ] Recipe list is in logical order +- [ ] Tested with `spec.recipeFromResources()` + +### For Refaster Template Recipes +- [ ] Created template class with `@RecipeDescriptor` +- [ ] Implemented `@BeforeTemplate` methods +- [ ] Implemented single `@AfterTemplate` method +- [ ] Verified generated recipe works correctly +- [ ] Recipe name uses generated form (e.g., `RecipesName`) + +## Notes + +Remember: +- **Do no harm**: If unsure, don't change +- **Minimize changes**: Make only necessary modifications +- **Immutability**: Recipes and LSTs must not be mutated +- **Test thoroughly**: Both positive and negative cases +- **Document clearly**: Future maintainers will thank you + +For more information, see: +- SKILL.md in this skill directory +- OpenRewrite documentation in this repository +- Examples in examples/ directory diff --git a/skills/recipe-writer/references/common-patterns.md b/skills/recipe-writer/references/common-patterns.md new file mode 100644 index 0000000..e51eca6 --- /dev/null +++ b/skills/recipe-writer/references/common-patterns.md @@ -0,0 +1,356 @@ +# OpenRewrite Recipe Common Patterns + +Quick reference for frequently used code patterns in recipe development. + +## Import Management + +### Adding Imports +```java +maybeAddImport("java.util.List"); +maybeAddImport("java.util.Collections", "emptyList"); // Static import +``` + +### Removing Imports +```java +maybeRemoveImport("old.package.Type"); +``` + +## Visitor Patterns + +### Chaining Visitors +```java +doAfterVisit(new OtherRecipe().getVisitor()); +``` + +### Cursor Messaging (Intra-visitor State) +```java +// Store state +getCursor().putMessage("key", value); + +// Retrieve state +Object value = getCursor().getNearestMessage("key"); +``` + +## Type Checking + +### Check Class Type +```java +if (methodInvocation.getType() != null && + TypeUtils.isOfClassType(methodInvocation.getType(), "com.example.Type")) { + // Type matches +} +``` + +### Check Method Type +```java +if (TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) { + // Method matches +} +``` + +### Check Assignability +```java +if (TypeUtils.isAssignableTo("java.util.Collection", someType)) { + // Type is assignable to Collection +} +``` + +## LST Manipulation + +### Modifying Lists (Never Mutate!) +```java +// WRONG - Mutates the list +method.getArguments().remove(0); + +// CORRECT - Creates new list with ListUtils +method.withArguments(ListUtils.map(method.getArguments(), (i, arg) -> + i == 0 ? null : arg // null removes the element +)); +``` + +### Adding to Lists +```java +// Add at end +classDecl.withModifiers( + ListUtils.concat(classDecl.getModifiers(), newModifier) +); + +// Add at beginning +classDecl.withModifiers( + ListUtils.concat(newModifier, classDecl.getModifiers()) +); +``` + +### Replacing in Lists +```java +classDecl.withModifiers( + ListUtils.map(classDecl.getModifiers(), mod -> + shouldReplace(mod) ? newModifier : mod + ) +); +``` + +## JavaTemplate Patterns + +### Simple Template +```java +JavaTemplate template = JavaTemplate + .builder("new Expression()") + .build(); + +// Apply +expression = template.apply(getCursor(), expression.getCoordinates().replace()); +``` + +### Template with Imports +```java +JavaTemplate template = JavaTemplate + .builder("Collections.emptyList()") + .imports("java.util.Collections") + .build(); +``` + +### Template with Parameters +```java +JavaTemplate template = JavaTemplate + .builder("new #{any(String)}(#{})") + .build(); + +// Apply with parameters +expression = template.apply( + getCursor(), + expression.getCoordinates().replace(), + typeName, // #{any(String)} + constructorArg // #{} +); +``` + +### Template with External Classpath +```java +JavaTemplate template = JavaTemplate + .builder("new CustomType()") + .imports("com.external.CustomType") + .javaParser(JavaParser.fromJavaVersion() + .classpath("external-library")) + .build(); +``` + +### Context-Sensitive Template +```java +// Use ONLY when referencing local variables/methods +JavaTemplate template = JavaTemplate + .builder("localVariable.toString()") + .contextSensitive() + .build(); +``` + +## Preconditions + +### Single Precondition +```java +@Override +public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesType<>("com.example.Type", true), + new YourVisitor() + ); +} +``` + +### Multiple Preconditions (AND) +```java +return Preconditions.check( + Preconditions.and( + new UsesType<>("com.example.Type", true), + new UsesMethod<>("com.example.Type methodName(..)") + ), + new YourVisitor() +); +``` + +### Multiple Preconditions (OR) +```java +return Preconditions.check( + Preconditions.or( + new UsesType<>("com.example.TypeA", true), + new UsesType<>("com.example.TypeB", true) + ), + new YourVisitor() +); +``` + +### Java Version Check +```java +return Preconditions.check( + new UsesJavaVersion<>(17), + new YourVisitor() +); +``` + +## ScanningRecipe Patterns + +### Basic Accumulator Structure +```java +public static class Accumulator { + // Use per-project tracking for multi-module support + Map> projectData = new HashMap<>(); +} +``` + +### Scanner (First Pass - Collect Only) +```java +@Override +public TreeVisitor getScanner(Accumulator acc) { + return new JavaIsoVisitor<>() { + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + // Collect data - DO NOT MODIFY LST + JavaProject project = cu.getMarkers() + .findFirst(JavaProject.class) + .orElse(null); + + if (project != null) { + acc.projectData + .computeIfAbsent(project, k -> new HashSet<>()) + .add(someData); + } + + return cu; // Return unchanged + } + }; +} +``` + +### Visitor (Second Pass - Modify Using Accumulator) +```java +@Override +public TreeVisitor getVisitor(Accumulator acc) { + return new JavaIsoVisitor<>() { + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + JavaProject project = cu.getMarkers() + .findFirst(JavaProject.class) + .orElse(null); + + if (project == null) { + return cu; + } + + // Use accumulator data to make decisions + Set projectData = acc.projectData.get(project); + if (projectData != null && projectData.contains(someCondition)) { + // Make changes + } + + return cu; + } + }; +} +``` + +## Formatting Preservation + +### Use Space.format() +```java +// Preserve formatting when adding elements +Space.format("\n" + indent) +``` + +### Copy Formatting from Existing Elements +```java +newElement = newElement.withPrefix(existingElement.getPrefix()); +``` + +## Testing Patterns + +### Multi-file Tests +```java +rewriteRun( + java( + """ + package com.example; + class First { } + """, + """ + package com.example; + class First { /* modified */ } + """ + ), + java( + """ + package com.example; + class Second { } + """ + // No second arg = no change expected + ) +); +``` + +### Tests with Parser Configuration +```java +@Override +public void defaults(RecipeSpec spec) { + spec.recipe(new YourRecipe()) + .parser(JavaParser.fromJavaVersion() + .classpath("external-library")); +} +``` + +### Tests with Different Java Versions +```java +rewriteRun( + spec -> spec.parser(JavaParser.fromJavaVersion().version("11")), + java( + // Java 11 specific code + ) +); +``` + +## Error Handling + +### Safe Type Access +```java +// Always check for null +if (element.getType() != null) { + // Safe to use type +} +``` + +### Safe Method Type Access +```java +JavaType.Method methodType = method.getMethodType(); +if (methodType != null && methodType.getDeclaringType() != null) { + // Safe to use +} +``` + +## Performance Optimization + +### Referential Equality Check +```java +J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + +if (!shouldChange(cd)) { + return cd; // Same object = no work downstream +} +``` + +### Early Return Pattern +```java +// Check cheapest conditions first +if (element.getName() == null) { + return element; +} + +if (element.getType() == null) { + return element; +} + +// Then check more expensive conditions +if (!TypeUtils.isOfClassType(element.getType(), targetType)) { + return element; +} + +// Finally, make changes +return makeChanges(element); +``` diff --git a/skills/recipe-writer/references/java-lst-reference.md b/skills/recipe-writer/references/java-lst-reference.md new file mode 100644 index 0000000..f058341 --- /dev/null +++ b/skills/recipe-writer/references/java-lst-reference.md @@ -0,0 +1,420 @@ +# Java LST Structure Reference + +Complete reference for OpenRewrite's Java Lossless Semantic Tree (LST) structure. + +## Overview + +The Java LST represents Java code as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance. + +## Type Hierarchy + +``` +org.openrewrite.java.tree.J +├── J.CompilationUnit (root of Java file) +├── J.ClassDeclaration (class definitions) +├── J.MethodDeclaration (method definitions) +├── J.MethodInvocation (method calls) +├── J.VariableDeclarations (variable declarations) +├── J.Block (code blocks) +├── J.If (if statements) +├── J.ForLoop (for loops) +├── J.WhileLoop (while loops) +├── J.Try (try-catch blocks) +├── J.Import (import statements) +├── J.Annotation (annotations) +├── J.Binary (binary operations: +, -, *, /, &&, ||, etc.) +├── J.Literal (primitive literals) +├── J.Identifier (variable/type names) +├── J.NewClass (object instantiation) +├── J.Return (return statements) +├── J.Assignment (assignments) +└── ... (many more) +``` + +## Core Types + +### J.CompilationUnit + +The root element of a Java source file. + +```java +public interface CompilationUnit extends JavaSourceFile, J { + List getImports(); + List getClasses(); + Space getEof(); + // ... other methods +} +``` + +**Usage:** +```java +@Override +public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { + // Visit entire file + cu = super.visitCompilationUnit(cu, ctx); + + // Access package declaration + String packageName = cu.getPackageDeclaration() != null ? + cu.getPackageDeclaration().getExpression().printTrimmed() : null; + + // Access imports + List imports = cu.getImports(); + + // Access classes + List classes = cu.getClasses(); + + return cu; +} +``` + +--- + +### J.ClassDeclaration + +Represents class, interface, enum, or record declarations. + +```java +public interface ClassDeclaration extends Statement, TypedTree { + List getLeadingAnnotations(); + List getModifiers(); + Kind getKind(); // Class, Interface, Enum, Record, Annotation + Identifier getName(); + @Nullable TypeParameters getTypeParameters(); + @Nullable TypeTree getExtends(); + @Nullable Container getImplements(); + Block getBody(); + JavaType.FullyQualified getType(); +} +``` + +**Usage:** +```java +@Override +public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + classDecl = super.visitClassDeclaration(classDecl, ctx); + + // Get class name + String className = classDecl.getSimpleName(); + + // Get fully qualified name + if (classDecl.getType() != null) { + String fqn = classDecl.getType().getFullyQualifiedName(); + } + + // Check if interface + if (classDecl.getKind() == J.ClassDeclaration.Kind.Type.Interface) { + // ... + } + + // Access methods + List statements = classDecl.getBody().getStatements(); + for (Statement statement : statements) { + if (statement instanceof J.MethodDeclaration) { + J.MethodDeclaration method = (J.MethodDeclaration) statement; + // Process method + } + } + + return classDecl; +} +``` + +--- + +### J.MethodDeclaration + +Represents method declarations. + +```java +public interface MethodDeclaration extends Statement, TypedTree { + List getLeadingAnnotations(); + List getModifiers(); + @Nullable TypeParameters getTypeParameters(); + @Nullable TypeTree getReturnTypeExpression(); + Identifier getName(); + List getParameters(); + @Nullable Container getThrows(); + @Nullable Block getBody(); + JavaType.Method getMethodType(); +} +``` + +**Usage:** +```java +@Override +public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + method = super.visitMethodDeclaration(method, ctx); + + // Get method name + String methodName = method.getSimpleName(); + + // Get parameters + List params = method.getParameters(); + + // Get return type + TypeTree returnType = method.getReturnTypeExpression(); + + // Get method body + J.Block body = method.getBody(); + + // Check if method matches signature + if (method.getMethodType() != null && + TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) { + // Method matches + } + + return method; +} +``` + +--- + +### J.MethodInvocation + +Represents method calls. + +```java +public interface MethodInvocation extends Expression, TypedTree, MethodCall { + @Nullable Expression getSelect(); // Object being called on + Identifier getName(); + List getArguments(); + JavaType.Method getMethodType(); +} +``` + +**Usage:** +```java +@Override +public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + method = super.visitMethodInvocation(method, ctx); + + // Get method name + String methodName = method.getSimpleName(); + + // Get arguments + List args = method.getArguments(); + + // Check if calling specific method + if (method.getMethodType() != null && + TypeUtils.isOfType(method.getMethodType(), "java.lang.String", "equals")) { + // This is a String.equals() call + } + + // Get select (object being called on) + Expression select = method.getSelect(); + + return method; +} +``` + +--- + +### J.VariableDeclarations + +Represents variable declarations. + +```java +public interface VariableDeclarations extends Statement, TypedTree { + List getLeadingAnnotations(); + List getModifiers(); + @Nullable TypeTree getTypeExpression(); + List getVariables(); +} +``` + +**Usage:** +```java +@Override +public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + multiVariable = super.visitVariableDeclarations(multiVariable, ctx); + + // Get type + TypeTree type = multiVariable.getTypeExpression(); + + // Get all variables declared + for (J.VariableDeclarations.NamedVariable var : multiVariable.getVariables()) { + String varName = var.getSimpleName(); + Expression initializer = var.getInitializer(); + // Process variable + } + + return multiVariable; +} +``` + +--- + +### J.Import + +Represents import statements. + +```java +public interface Import extends Statement { + boolean isStatic(); + FieldAccess getQualid(); +} +``` + +**Usage:** +```java +@Override +public J.Import visitImport(J.Import _import, ExecutionContext ctx) { + _import = super.visitImport(_import, ctx); + + // Get fully qualified name + String fqn = _import.getQualid().printTrimmed(); + + // Check if static import + if (_import.isStatic()) { + // Static import + } + + return _import; +} +``` + +--- + +### J.Annotation + +Represents annotations. + +```java +public interface Annotation extends Expression { + NameTree getAnnotationType(); + @Nullable Container getArguments(); +} +``` + +**Usage:** +```java +@Override +public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + annotation = super.visitAnnotation(annotation, ctx); + + // Get annotation type + String annotationType = annotation.getAnnotationType().printTrimmed(); + + // Check specific annotation + if ("org.junit.Test".equals(annotationType)) { + // This is a @Test annotation + } + + // Get arguments + if (annotation.getArguments() != null) { + List args = annotation.getArguments().getElements(); + } + + return annotation; +} +``` + +--- + +## Common Patterns + +### Type Checking + +```java +// Check if method invocation is of specific type +if (method.getMethodType() != null && + TypeUtils.isOfClassType(method.getMethodType().getDeclaringType(), "com.example.Class")) { + // Method is declared in com.example.Class +} + +// Check method signature +if (TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) { + // Method matches +} +``` + +### Safe Value Access + +```java +// Always check for null before accessing type information +if (classDecl.getType() != null) { + String fqn = classDecl.getType().getFullyQualifiedName(); +} + +// Check for null on optional elements +if (method.getBody() != null) { + List statements = method.getBody().getStatements(); +} +``` + +### Modifying LST Elements + +```java +// Always use .withX() methods - never mutate +classDecl = classDecl.withName(classDecl.getName().withSimpleName("NewName")); + +// Use ListUtils for list operations +classDecl = classDecl.withModifiers( + ListUtils.concat(classDecl.getModifiers(), newModifier) +); + +// Remove from list +method = method.withArguments( + ListUtils.map(method.getArguments(), (i, arg) -> + i == indexToRemove ? null : arg + ) +); +``` + +### Import Management + +```java +// Add import if not present +maybeAddImport("java.util.List"); + +// Add static import +maybeAddImport("java.util.Collections", "emptyList"); + +// Remove import +maybeRemoveImport("old.package.Type"); +``` + +### Visitor Chaining + +```java +// Chain another visitor after this one +doAfterVisit(new SomeOtherRecipe().getVisitor()); +``` + +## Visit Method Reference + +Common visit methods to override: + +| LST Element | Visit Method | Common Use | +|-------------|--------------|------------| +| `J.CompilationUnit` | `visitCompilationUnit()` | Entire file operations | +| `J.ClassDeclaration` | `visitClassDeclaration()` | Class modifications | +| `J.MethodDeclaration` | `visitMethodDeclaration()` | Method modifications | +| `J.MethodInvocation` | `visitMethodInvocation()` | Method call changes | +| `J.VariableDeclarations` | `visitVariableDeclarations()` | Variable operations | +| `J.Block` | `visitBlock()` | Code block operations | +| `J.If` | `visitIf()` | Conditional logic | +| `J.ForLoop` | `visitForLoop()` | Loop transformations | +| `J.Import` | `visitImport()` | Import management | +| `J.Annotation` | `visitAnnotation()` | Annotation operations | +| `J.Binary` | `visitBinary()` | Binary operations | +| `J.Literal` | `visitLiteral()` | Literal values | +| `J.Assignment` | `visitAssignment()` | Assignment operations | +| `J.Return` | `visitReturn()` | Return statements | +| `J.NewClass` | `visitNewClass()` | Object instantiation | + +## Best Practices + +1. **Always call super** - `super.visitX()` traverses the subtree +2. **Return modified copies** - Never mutate LST elements directly +3. **Use `.withX()` methods** - For all modifications +4. **Handle null cases** - Check for null before accessing type information +5. **Preserve formatting** - LST methods maintain formatting automatically +6. **Use ListUtils** - For all list operations (never mutate directly) +7. **Check types safely** - Use TypeUtils methods with null checks + +## See Also + +- `references/common-patterns.md` - Code patterns for common operations +- `references/troubleshooting-guide.md` - Solutions to common issues +- `templates/template-imperative-recipe.java` - Complete recipe template diff --git a/skills/recipe-writer/references/jsonpath-patterns.md b/skills/recipe-writer/references/jsonpath-patterns.md new file mode 100644 index 0000000..6fa398a --- /dev/null +++ b/skills/recipe-writer/references/jsonpath-patterns.md @@ -0,0 +1,524 @@ +# JsonPath Patterns for YAML Recipes + +Comprehensive collection of JsonPath patterns for common YAML structures including GitHub Actions, Kubernetes, CI/CD configs, and generic YAML. + +## Overview + +JsonPath provides a query language for navigating YAML/JSON structures. In OpenRewrite, use `JsonPathMatcher` to match specific locations in YAML files. + +```java +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); +if (matcher.matches(getCursor())) { + // This element matches the path +} +``` + +## JsonPath Syntax Quick Reference + +| Syntax | Meaning | Example | +|--------|---------|---------| +| `$` | Root element | `$` | +| `.` | Child element | `$.jobs` | +| `..` | Recursive descent | `$..uses` | +| `*` | Wildcard (any element) | `$.jobs.*` | +| `[*]` | Array wildcard | `$.steps[*]` | +| `[n]` | Array index | `$.steps[0]` | +| `[start:end]` | Array slice | `$.steps[0:3]` | +| `[?(@.key)]` | Filter expression | `$[?(@.name)]` | + +--- + +## GitHub Actions Patterns + +### Workflow-Level Patterns + +```java +// Root workflow properties +"$.name" // Workflow name +"$.on" // Trigger configuration +"$.env" // Workflow-level environment variables +"$.permissions" // Workflow-level permissions +"$.concurrency" // Concurrency configuration +"$.defaults" // Default settings + +// Specific triggers +"$.on.push" // Push trigger +"$.on.pull_request" // Pull request trigger +"$.on.workflow_dispatch" // Manual trigger +"$.on.schedule[*]" // Scheduled triggers (cron) + +// Trigger details +"$.on.push.branches[*]" // Push branch filters +"$.on.push.paths[*]" // Push path filters +"$.on.pull_request.types[*]" // PR event types +``` + +### Job-Level Patterns + +```java +// All jobs +"$.jobs" // Jobs object +"$.jobs.*" // Any job (wildcard) +"$.jobs.build" // Specific job named 'build' + +// Job properties +"$.jobs.*.name" // Job display name +"$.jobs.*.runs-on" // Runner specification +"$.jobs.*.needs" // Job dependencies +"$.jobs.*.if" // Job conditions +"$.jobs.*.timeout-minutes" // Job timeout +"$.jobs.*.strategy" // Matrix/parallel strategy +"$.jobs.*.env" // Job-level environment variables +"$.jobs.*.permissions" // Job-level permissions +"$.jobs.*.container" // Container configuration +"$.jobs.*.services" // Service containers + +// Matrix strategy +"$.jobs.*.strategy.matrix" // Matrix configuration +"$.jobs.*.strategy.matrix.os[*]" // OS variations +"$.jobs.*.strategy.matrix.node[*]" // Node.js versions +"$.jobs.*.strategy.fail-fast" // Fail-fast setting +"$.jobs.*.strategy.max-parallel" // Parallel limit +``` + +### Step-Level Patterns + +```java +// All steps +"$.jobs.*.steps" // Steps array +"$.jobs.*.steps[*]" // Any step in any job +"$.jobs.build.steps[*]" // Steps in 'build' job +"$.jobs.*.steps[0]" // First step of each job + +// Step properties +"$.jobs.*.steps[*].name" // Step name +"$.jobs.*.steps[*].uses" // Action reference +"$.jobs.*.steps[*].run" // Shell command +"$.jobs.*.steps[*].with" // Action inputs +"$.jobs.*.steps[*].env" // Step environment variables +"$.jobs.*.steps[*].if" // Step condition +"$.jobs.*.steps[*].continue-on-error" // Error handling + +// Action inputs (with block) +"$.jobs.*.steps[*].with.node-version" // Specific input +"$.jobs.*.steps[*].with.*" // Any input +``` + +### Complete GitHub Actions Examples + +```java +// Find all uses of actions/checkout +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); +if (matcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + if (scalar.getValue().startsWith("actions/checkout@")) { + // Found checkout action + } + } +} + +// Find all jobs running on ubuntu-latest +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.runs-on"); +if (matcher.matches(getCursor()) && "runs-on".equals(entry.getKey().getValue())) { + // Process runner specification +} + +// Find all node-version specifications in matrix +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.strategy.matrix.node[*]"); +if (matcher.matches(getCursor())) { + // Process node version entry +} + +// Find environment variables at any level +JsonPathMatcher matcher = new JsonPathMatcher("$..env.*"); +if (matcher.matches(getCursor())) { + // Found an environment variable +} +``` + +--- + +## Kubernetes Patterns + +### Pod/Deployment Patterns + +```java +// Metadata +"$.metadata.name" // Resource name +"$.metadata.namespace" // Namespace +"$.metadata.labels.*" // Any label +"$.metadata.annotations.*" // Any annotation + +// Spec +"$.spec.replicas" // Replica count +"$.spec.selector" // Pod selector +"$.spec.template" // Pod template + +// Pod template +"$.spec.template.metadata.labels.*" // Pod labels +"$.spec.template.spec.containers[*]" // All containers +"$.spec.template.spec.initContainers[*]" // Init containers +"$.spec.template.spec.volumes[*]" // Volumes + +// Container details +"$.spec.template.spec.containers[*].name" // Container name +"$.spec.template.spec.containers[*].image" // Container image +"$.spec.template.spec.containers[*].ports[*]" // Container ports +"$.spec.template.spec.containers[*].env[*]" // Environment variables +"$.spec.template.spec.containers[*].resources" // Resource limits +"$.spec.template.spec.containers[*].volumeMounts[*]" // Volume mounts +``` + +### Service Patterns + +```java +"$.spec.type" // Service type (ClusterIP, NodePort, LoadBalancer) +"$.spec.selector.*" // Service selector +"$.spec.ports[*]" // Service ports +"$.spec.ports[*].port" // Port number +"$.spec.ports[*].targetPort" // Target port +"$.spec.ports[*].protocol" // Protocol (TCP/UDP) +``` + +### ConfigMap/Secret Patterns + +```java +"$.data.*" // Any data entry +"$.data.config\\.yaml" // Specific data key +"$.stringData.*" // String data entries +"$.binaryData.*" // Binary data entries +``` + +### Ingress Patterns + +```java +"$.spec.rules[*]" // Ingress rules +"$.spec.rules[*].host" // Host pattern +"$.spec.rules[*].http.paths[*]" // Path rules +"$.spec.rules[*].http.paths[*].path" // Path pattern +"$.spec.rules[*].http.paths[*].backend" // Backend service +"$.spec.tls[*]" // TLS configuration +``` + +### Complete Kubernetes Examples + +```java +// Update container images for nginx +JsonPathMatcher matcher = new JsonPathMatcher("$.spec.template.spec.containers[*].image"); +if (matcher.matches(getCursor()) && "image".equals(entry.getKey().getValue())) { + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + if (scalar.getValue().startsWith("nginx:")) { + // Update nginx version + } + } +} + +// Find all resource limits +JsonPathMatcher matcher = new JsonPathMatcher("$.spec.template.spec.containers[*].resources.limits.memory"); +if (matcher.matches(getCursor())) { + // Process memory limit +} + +// Find all environment variables across all containers +JsonPathMatcher matcher = new JsonPathMatcher("$..containers[*].env[*].name"); +if (matcher.matches(getCursor())) { + // Process environment variable +} +``` + +--- + +## CI/CD Configuration Patterns + +### GitLab CI + +```java +// Job-level +"$.*.script[*]" // Script commands in any job +"$.*.stage" // Job stage +"$.*.image" // Docker image +"$.*.services[*]" // Service containers +"$.*.before_script[*]" // Before script commands +"$.*.after_script[*]" // After script commands +"$.*.variables.*" // Job variables +"$.*.cache" // Cache configuration +"$.*.artifacts" // Artifacts configuration + +// Pipeline-level +"$.stages[*]" // Pipeline stages +"$.variables.*" // Global variables +"$.default" // Default settings +``` + +### CircleCI + +```java +// Jobs +"$.jobs.*" // Any job +"$.jobs.*.docker[*]" // Docker images +"$.jobs.*.docker[*].image" // Docker image +"$.jobs.*.steps[*]" // Job steps +"$.jobs.*.environment.*" // Job environment + +// Workflows +"$.workflows.*" // Any workflow +"$.workflows.*.jobs[*]" // Workflow jobs +``` + +### Travis CI + +```java +"$.language" // Language +"$.os" // Operating system +"$.dist" // Distribution +"$.script[*]" // Build script +"$.before_install[*]" // Before install +"$.install[*]" // Install commands +"$.before_script[*]" // Before script +"$.after_success[*]" // After success +"$.matrix.include[*]" // Matrix builds +``` + +### Jenkins (Declarative Pipeline) + +```java +"$.pipeline.agent" // Agent specification +"$.pipeline.stages[*]" // Pipeline stages +"$.pipeline.stages[*].stage" // Stage name +"$.pipeline.stages[*].steps[*]" // Stage steps +"$.pipeline.environment.*" // Environment variables +"$.pipeline.post" // Post actions +``` + +--- + +## Generic YAML Patterns + +### Common Structures + +```java +// Root level +"$.*" // Any root-level property +"$.version" // Version field +"$.name" // Name field +"$.description" // Description field + +// Nested structures +"$.config.*" // Any config property +"$.settings.*" // Any settings property +"$.metadata.*" // Any metadata property + +// Arrays +"$.items[*]" // Any item in array +"$.items[0]" // First item +"$.items[-1]" // Last item (not supported in all implementations) + +// Deep search +"$..name" // Any 'name' at any level +"$..version" // Any 'version' at any level +``` + +### Package Manager Configs + +```java +// package.json (npm) +"$.scripts.*" // npm scripts +"$.dependencies.*" // Dependencies +"$.devDependencies.*" // Dev dependencies +"$.peerDependencies.*" // Peer dependencies + +// composer.json (PHP) +"$.require.*" // PHP dependencies +"$.require-dev.*" // Dev dependencies +"$.autoload.psr-4.*" // PSR-4 autoload + +// Gemfile (Ruby) - typically not YAML but similar patterns +"$.dependencies[*]" // Gem dependencies +``` + +### Docker Compose + +```java +"$.version" // Compose file version +"$.services.*" // Any service +"$.services.*.image" // Service image +"$.services.*.build" // Build configuration +"$.services.*.ports[*]" // Port mappings +"$.services.*.environment.*" // Environment variables +"$.services.*.volumes[*]" // Volume mounts +"$.services.*.depends_on[*]" // Dependencies +"$.networks.*" // Network definitions +"$.volumes.*" // Volume definitions +``` + +### Ansible Playbooks + +```java +"$[*].hosts" // Target hosts +"$[*].tasks[*]" // All tasks +"$[*].tasks[*].name" // Task names +"$[*].tasks[*].when" // Task conditions +"$[*].vars.*" // Variables +"$[*].roles[*]" // Roles +``` + +--- + +## Advanced Patterns + +### Recursive Descent + +Find elements at any depth in the structure: + +```java +// Find all 'version' keys anywhere in document +JsonPathMatcher matcher = new JsonPathMatcher("$..version"); + +// Find all 'env' objects anywhere +JsonPathMatcher matcher = new JsonPathMatcher("$..env"); + +// Find all arrays named 'items' anywhere +JsonPathMatcher matcher = new JsonPathMatcher("$..items[*]"); +``` + +### Multiple Matchers + +Use multiple matchers for precise targeting: + +```java +// Match steps that use actions AND are in build job +JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.build.steps[*].uses"); +JsonPathMatcher anyStepMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); + +if (stepMatcher.matches(getCursor()) || anyStepMatcher.matches(getCursor())) { + // Process action reference +} +``` + +### Combining with Key Checks + +```java +// Match 'uses' key within steps +JsonPathMatcher pathMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); + +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + // Check both path AND key name + if (pathMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { + // This is definitely a 'uses' field in a step + } + return super.visitMappingEntry(entry, ctx); +} +``` + +--- + +## Pattern Selection Guide + +| Use Case | Pattern Type | Example | +|----------|--------------|---------| +| Exact path | Explicit path | `$.jobs.build.steps[0]` | +| Any child | Wildcard | `$.jobs.*` | +| Any array item | Array wildcard | `$.steps[*]` | +| Any depth | Recursive descent | `$..version` | +| Conditional | With key check | `pathMatcher.matches() && "key".equals(key)` | + +--- + +## Common Mistakes + +### 1. Forgetting Key Name Check + +```java +// ❌ WRONG - matches parent path, not the specific key +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); +if (matcher.matches(getCursor())) { + // This matches the STEP, not the 'uses' key +} + +// ✅ CORRECT - check both path and key +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); +if (matcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { + // Now we're definitely at the 'uses' key +} +``` + +### 2. Wrong Visitor Method + +```java +// ❌ WRONG - visitMapping() doesn't work for sequences +JsonPathMatcher matcher = new JsonPathMatcher("$.steps[*]"); +public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) { + // Won't match array items +} + +// ✅ CORRECT - use visitSequenceEntry() +JsonPathMatcher matcher = new JsonPathMatcher("$.steps[*]"); +public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) { + if (matcher.matches(getCursor())) { + // Process sequence entry + } +} +``` + +### 3. Overly Broad Patterns + +```java +// ❌ TOO BROAD - matches 'uses' anywhere +JsonPathMatcher matcher = new JsonPathMatcher("$..uses"); + +// ✅ BETTER - specific to GitHub Actions steps +JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); +``` + +--- + +## Testing JsonPath Patterns + +```java +// In your test, verify the path matches +@Test +void testJsonPathMatching() { + rewriteRun( + spec -> spec.recipe(new YourRecipe()), + yaml( + """ + jobs: + build: + steps: + - uses: actions/checkout@v2 + """, + """ + jobs: + build: + steps: + - uses: actions/checkout@v3 + """ + ) + ); +} +``` + +--- + +## Quick Reference Table + +| YAML Structure | JsonPath Pattern | Visitor Method | +|----------------|------------------|----------------| +| Root property | `$.property` | `visitMappingEntry` | +| Nested property | `$.parent.child` | `visitMappingEntry` | +| Any property | `$.*` or `$.parent.*` | `visitMappingEntry` | +| Array item | `$.array[*]` | `visitSequenceEntry` | +| Nested array | `$.parent.array[*]` | `visitSequenceEntry` | +| Any depth | `$..property` | `visitMappingEntry` | +| Multiple levels | `$.a.*.b.*.c` | `visitMappingEntry` | + +--- + +## Additional Resources + +- JsonPath Specification: https://goessner.net/articles/JsonPath/ +- JsonPath Evaluator (online tool): https://jsonpath.com/ +- OpenRewrite JsonPathMatcher JavaDoc: https://docs.openrewrite.org/ diff --git a/skills/recipe-writer/references/trait-implementation-guide.md b/skills/recipe-writer/references/trait-implementation-guide.md new file mode 100644 index 0000000..3df0716 --- /dev/null +++ b/skills/recipe-writer/references/trait-implementation-guide.md @@ -0,0 +1,328 @@ +# OpenRewrite Traits Guide + +This is a reference guide for understanding and using OpenRewrite Traits in recipe development. + +## What are Traits? + +Traits are semantic wrappers around LST elements that implement the `Trait` interface. They encapsulate domain-specific logic for identifying and accessing properties of tree elements, providing a higher-level abstraction over raw LST navigation. + +## IMPORTANT - Deprecated Traits Utility Classes + +Older OpenRewrite code used utility classes like `org.openrewrite.java.trait.Traits`, `org.openrewrite.gradle.trait.Traits`, etc. These are now deprecated. Always instantiate matchers directly: + +```java +// ❌ Old (deprecated): +Traits.literal() +Traits.methodAccess("...") + +// ✅ New (preferred): +new Literal.Matcher() +new MethodAccess.Matcher("...") +``` + +## Core Trait Interface + +The Trait interface is minimal and only requires implementing `getCursor()`: + +```java +public interface Trait { + Cursor getCursor(); + + default T getTree() { + return getCursor().getValue(); + } +} +``` + +## Example Trait Implementation + +Here's a complete trait implementation pattern (implementation details like Lombok are optional): + +```java +// Using Lombok for convenience (optional - you can implement manually) +@Value +public class YamlScalar implements Trait { + Cursor cursor; // Required for getCursor() + + // Optional: Additional fields for caching computed values + @Nullable String cachedValue; + + // Domain-specific accessor methods + public @Nullable String getValue() { + if (cachedValue != null) { + return cachedValue; + } + Yaml.Block block = getTree(); + return block instanceof Yaml.Scalar + ? ((Yaml.Scalar) block).getValue() + : null; + } + + // Static utility methods for shared logic + public static boolean isScalar(Cursor cursor) { + return cursor.getValue() instanceof Yaml.Scalar; + } + + // Modification methods return new trait instances + public YamlScalar withValue(String newValue) { + Yaml.Scalar scalar = (Yaml.Scalar) getTree(); + Yaml.Scalar updated = scalar.withValue(newValue); + return new YamlScalar(cursor.withValue(updated), null); + } + + // Matcher nested as static inner class + public static class Matcher extends SimpleTraitMatcher { + // Optional: Configuration fields for filtering + @Nullable + protected String requiredValue; + + @Override + protected @Nullable YamlScalar test(Cursor cursor) { + Object value = cursor.getValue(); + if (!(value instanceof Yaml.Block)) { + return null; + } + + // Complex matching logic with guards + if (!isScalar(cursor)) { + return null; + } + + Yaml.Scalar scalar = (Yaml.Scalar) value; + + // Apply filters if configured + if (requiredValue != null && + !requiredValue.equals(scalar.getValue())) { + return null; + } + + // Return trait with cached data + return new YamlScalar(cursor, scalar.getValue()); + } + + // Configuration methods for the matcher + public Matcher withRequiredValue(String value) { + this.requiredValue = value; + return this; + } + } +} +``` + +## Important Implementation Notes + +1. **Required Interface**: Only `getCursor()` is required by the Trait interface +2. **Implementation Flexibility**: You can use Lombok, manual implementation, or any pattern you prefer +3. **Matcher as Inner Class**: By convention, nest the Matcher as a static inner class +4. **Additional Fields**: Traits can have fields beyond cursor to cache expensive computations +5. **Static Utilities**: Include static helper methods for validation and shared logic +6. **Matcher Fields**: Matchers can have configuration fields for filtering behavior +7. **test() Complexity**: Real `test()` methods often contain substantial validation logic, not just simple instanceof checks + +## TraitMatcher API Methods + +The `TraitMatcher` interface provides several powerful methods for finding traits in the LST. Understanding these methods is essential for effective trait usage. The `SimpleTraitMatcher` base class implements all these methods using a single abstract `test(Cursor)` method. + +### Core API Methods + +```java +// Test if cursor matches and return trait instance +Optional get(Cursor cursor) + +// Like get() but throws NoSuchElementException if no match +U require(Cursor cursor) + +// Stream of all matching traits in ancestor chain (up the tree) +Stream higher(Cursor cursor) + +// Stream of all matching traits in descendants (down the tree) +Stream lower(Cursor cursor) + +// Stream of all matching traits in entire source file +Stream lower(SourceFile sourceFile) + +// Convert matcher to TreeVisitor for use in recipes +

TreeVisitor asVisitor(VisitFunction2 visitor) +``` + +### How SimpleTraitMatcher Implements These Methods + +`SimpleTraitMatcher` provides default implementations of all TraitMatcher methods by calling your abstract `test(Cursor)` method: + +- `get(Cursor)` - Calls `test(cursor)` and wraps result in Optional +- `require(Cursor)` - Calls `test(cursor)` and throws if null +- `higher(Cursor)` - Walks up cursor stack, calling `test()` on each ancestor +- `lower(Cursor)` - Visits all descendants, calling `test()` on each node +- `asVisitor()` - Creates a TreeVisitor that calls `test()` on every visited node + +This means you only need to implement `test(Cursor)` to get all these capabilities. + +## Example 1: Finding All Traits in a File + +Use `lower()` to find all matching traits in a source file or subtree: + +```java +// Create matcher for GitHub Actions steps +ActionStep.Matcher stepMatcher = new ActionStep.Matcher(); + +// In your visitor, find all ActionStep traits in the entire file +@Override +public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { + // Get stream of all ActionStep traits in this file + Stream allSteps = stepMatcher.lower(documents); + + // Process the stream - e.g., collect all action references + List actionRefs = allSteps + .map(step -> step.getActionRef()) + .filter(ref -> ref != null) + .collect(Collectors.toList()); + + // Log found actions + for (String ref : actionRefs) { + System.out.println("Found action: " + ref); + } + + return super.visitDocuments(documents, ctx); +} +``` + +## Example 2: Checking Parent Context with higher() + +Use `higher()` to search up the ancestor chain to check if you're within a specific context: + +```java +// Trait for GitHub Actions permissions blocks +PermissionsScope.Matcher permissionsMatcher = new PermissionsScope.Matcher(); + +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + // Check if we're inside a permissions block + Optional permissionsScope = + permissionsMatcher.higher(getCursor()).findFirst(); + + if (permissionsScope.isPresent() && "contents".equals(entry.getKey().getValue())) { + // We found a "contents" key inside a permissions block + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + if ("write".equals(scalar.getValue())) { + return SearchResult.found(entry, + "Found write permission to contents"); + } + } + } + + return super.visitMappingEntry(entry, ctx); +} +``` + +## Example 3: Using asVisitor() in Recipes + +The `asVisitor()` method is the primary way to use traits in recipes: + +```java +@Override +public TreeVisitor getVisitor() { + ActionStep.Matcher matcher = new ActionStep.Matcher(); + + // Convert matcher to visitor with VisitFunction2 + return matcher.asVisitor((step, ctx) -> { + String ref = step.getActionRef(); + if (ref != null && ref.contains("@v2")) { + return step.withActionRef(ref.replace("@v2", "@v3")).getTree(); + } + return step.getTree(); + }); +} +``` + +## When to Create a Trait + +A trait is the best choice when: + +1. **You want to provide shared functionality encapsulating several possible LST types** + - Example: A `YamlValue` trait that works with both `Yaml.Scalar`, `Yaml.Sequence`, and `Yaml.Mapping` + - The trait provides a unified interface regardless of the underlying concrete type + - Allows recipes to work generically with different YAML structures + +2. **You want to provide functionality specific to a subset of an individual LST type** + - Example: An `ActionStep` trait for `Yaml.Mapping.Entry` elements that have a `uses` key + - Not all mapping entries are action steps, only those matching specific criteria + - The trait represents a semantic concept within a broader LST type + +Additional scenarios where traits add value: +- **Semantic abstraction** over complex LST patterns (e.g., "a workflow trigger") +- **Reusable matching logic** across multiple recipes +- **Domain-specific accessors** that hide LST complexity +- **Composable filtering** with matcher chains + +## When to Use Utility Interfaces Instead + +Use utility interfaces for: +- **Simple helper methods** for visitors (e.g., safe scalar extraction) +- **Stateless operations** that don't need cursor context +- **Common patterns** that don't warrant full trait machinery +- **Mixins** to share functionality across visitor classes + +## Domain-Specific Matcher Base Classes + +For traits within a specific domain (e.g., YAML, Gradle, Maven), consider creating an intermediate matcher base class that provides shared utilities: + +```java +public abstract class YamlTraitMatcher> + extends SimpleTraitMatcher { + + // Shared utility for all YAML trait matchers + protected boolean withinMapping(Cursor cursor) { + return cursor.firstEnclosing(Yaml.Mapping.class) != null; + } + + // Common validation logic + protected boolean isValidYamlContext(Cursor cursor) { + SourceFile sourceFile = cursor.firstEnclosing(SourceFile.class); + return sourceFile instanceof Yaml.Documents; + } + + // Helper for finding parent entries + protected @Nullable Yaml.Mapping.Entry getParentEntry(Cursor cursor) { + return cursor.firstEnclosing(Yaml.Mapping.Entry.class); + } +} +``` + +Then your specific trait matchers extend this base class: + +```java +public static class Matcher extends YamlTraitMatcher { + @Override + protected @Nullable ActionStep test(Cursor cursor) { + // Can use withinMapping(), isValidYamlContext(), etc. here + if (!isValidYamlContext(cursor)) { + return null; + } + // ... rest of matching logic + return null; + } +} +``` + +## Trait Best Practices Summary + +**Core Requirements:** +- Implement `Trait` interface with `getCursor()` method +- Nest a `Matcher` as a static inner class extending `SimpleTraitMatcher` +- Override `test(Cursor)` in your matcher to implement matching logic + +**Instantiation Pattern:** +```java +// ✅ Correct - Direct instantiation +YamlScalar.Matcher matcher = new YamlScalar.Matcher(); + +// ❌ Wrong - Using deprecated Traits utility +Traits.yamlScalar() // Don't use - deprecated +``` + +**Common Patterns:** +- Use `higher()` to search ancestor chain +- Use `lower()` to search descendants +- Use `asVisitor()` in recipe `getVisitor()` methods +- Extend domain-specific matchers like `YamlTraitMatcher` for shared utilities diff --git a/skills/recipe-writer/references/yaml-lst-reference.md b/skills/recipe-writer/references/yaml-lst-reference.md new file mode 100644 index 0000000..c290016 --- /dev/null +++ b/skills/recipe-writer/references/yaml-lst-reference.md @@ -0,0 +1,736 @@ +# YAML LST Structure Reference + +Complete reference for OpenRewrite's YAML Lossless Semantic Tree (LST) structure. + +## Overview + +The YAML LST represents YAML documents as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance. + +## Type Hierarchy + +``` +org.openrewrite.yaml.tree.Yaml +├── Yaml.Documents (root container) +├── Yaml.Document (single document in multi-doc file) +├── Yaml.Mapping (key-value pairs, similar to JSON object) +│ └── Yaml.Mapping.Entry (single key-value pair) +├── Yaml.Sequence (arrays/lists) +│ └── Yaml.Sequence.Entry (single array item) +├── Yaml.Scalar (primitive values: strings, numbers, booleans) +│ └── Yaml.Scalar.Key (key in a key-value pair) +└── Yaml.Anchor (YAML anchors and aliases) +``` + +## Core Types + +### Yaml.Documents + +The root element containing one or more YAML documents. + +```java +public interface Documents extends Yaml { + List getDocuments(); + Documents withDocuments(List documents); +} +``` + +**Usage:** +```java +@Override +public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { + // Process all documents in the file + List modified = ListUtils.map( + documents.getDocuments(), + doc -> (Yaml.Document) visit(doc, ctx) + ); + return documents.withDocuments(modified); +} +``` + +--- + +### Yaml.Document + +A single YAML document (files can contain multiple documents separated by `---`). + +```java +public interface Document extends Yaml { + Block getBlock(); // Root block (usually Mapping or Sequence) + Document withBlock(Block block); + boolean isExplicit(); // True if document starts with --- +} +``` + +**Usage:** +```java +@Override +public Yaml.Document visitDocument(Yaml.Document document, ExecutionContext ctx) { + if (document.getBlock() instanceof Yaml.Mapping) { + Yaml.Mapping root = (Yaml.Mapping) document.getBlock(); + // Process root mapping + Yaml.Mapping modified = (Yaml.Mapping) visit(root, ctx); + if (modified != root) { + return document.withBlock(modified); + } + } + return super.visitDocument(document, ctx); +} +``` + +--- + +### Yaml.Mapping + +Represents a YAML mapping (key-value pairs), equivalent to JSON objects or Python dictionaries. + +```java +public interface Mapping extends Block { + List getEntries(); + Mapping withEntries(List entries); + String getAnchor(); // YAML anchor if present +} +``` + +**Example YAML:** +```yaml +name: my-workflow +on: + push: + branches: [main] +jobs: + build: + runs-on: ubuntu-latest +``` + +**Navigation:** +```java +@Override +public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) { + for (Yaml.Mapping.Entry entry : mapping.getEntries()) { + String key = entry.getKey().getValue(); + Block value = entry.getValue(); + + if ("jobs".equals(key) && value instanceof Yaml.Mapping) { + Yaml.Mapping jobsMapping = (Yaml.Mapping) value; + // Process each job + for (Yaml.Mapping.Entry jobEntry : jobsMapping.getEntries()) { + String jobName = jobEntry.getKey().getValue(); + // Process job... + } + } + } + return super.visitMapping(mapping, ctx); +} +``` + +--- + +### Yaml.Mapping.Entry + +A single key-value pair within a mapping. + +```java +public interface Entry extends Yaml { + Yaml.Scalar.Key getKey(); + Block getValue(); // Can be Scalar, Mapping, or Sequence + Entry withKey(Yaml.Scalar.Key key); + Entry withValue(Block value); +} +``` + +**Key Methods:** +- `getKey().getValue()` - Get the key as a string (always safe, no cast needed) +- `getValue()` - Get the value (requires type check and cast) +- `withKey()` - Create new entry with different key +- `withValue()` - Create new entry with different value + +**Usage:** +```java +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + String keyName = entry.getKey().getValue(); // Safe access + + // Check value type before processing + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + String value = scalar.getValue(); + + // Modify value + if ("old-value".equals(value)) { + return entry.withValue(scalar.withValue("new-value")); + } + } else if (entry.getValue() instanceof Yaml.Mapping) { + Yaml.Mapping nested = (Yaml.Mapping) entry.getValue(); + // Process nested mapping + } else if (entry.getValue() instanceof Yaml.Sequence) { + Yaml.Sequence sequence = (Yaml.Sequence) entry.getValue(); + // Process sequence + } + + return super.visitMappingEntry(entry, ctx); +} +``` + +--- + +### Yaml.Sequence + +Represents a YAML sequence (array/list). + +```java +public interface Sequence extends Block { + List getEntries(); + Sequence withEntries(List entries); + String getAnchor(); +} +``` + +**Example YAML:** +```yaml +branches: + - main + - develop + - feature/* + +# Or inline style: +branches: [main, develop, feature/*] +``` + +**Navigation:** +```java +@Override +public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) { + List entries = sequence.getEntries(); + + // Iterate through sequence items + for (Yaml.Sequence.Entry entry : entries) { + if (entry.getBlock() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); + String value = scalar.getValue(); + // Process value... + } else if (entry.getBlock() instanceof Yaml.Mapping) { + Yaml.Mapping mapping = (Yaml.Mapping) entry.getBlock(); + // Process mapping item... + } + } + + return super.visitSequence(sequence, ctx); +} +``` + +**Adding Items:** +```java +@Override +public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) { + // Check if 'main' branch exists + boolean hasMain = false; + for (Yaml.Sequence.Entry entry : sequence.getEntries()) { + if (entry.getBlock() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); + if ("main".equals(scalar.getValue())) { + hasMain = true; + break; + } + } + } + + if (!hasMain) { + // Create new scalar for 'main' + Yaml.Scalar mainScalar = new Yaml.Scalar( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + Yaml.Scalar.Style.PLAIN, + null, + "main" + ); + + // Wrap in sequence entry + Yaml.Sequence.Entry mainEntry = new Yaml.Sequence.Entry( + Tree.randomId(), + Space.format("\n - "), // Proper indentation + Markers.EMPTY, + mainScalar, + false + ); + + // Add to sequence + return sequence.withEntries( + ListUtils.concat(sequence.getEntries(), mainEntry) + ); + } + + return super.visitSequence(sequence, ctx); +} +``` + +--- + +### Yaml.Sequence.Entry + +A single item in a sequence. + +```java +public interface Entry extends Yaml { + Block getBlock(); // The actual value + Entry withBlock(Block block); + boolean isTrailingCommaPrefix(); +} +``` + +**Usage:** +```java +@Override +public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) { + if (entry.getBlock() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); + String value = scalar.getValue(); + + // Update version references + if (value.contains("@v2")) { + String updated = value.replace("@v2", "@v3"); + return entry.withBlock(scalar.withValue(updated)); + } + } + + return super.visitSequenceEntry(entry, ctx); +} +``` + +--- + +### Yaml.Scalar + +Represents primitive values (strings, numbers, booleans, null). + +```java +public interface Scalar extends Block { + Style getStyle(); // PLAIN, SINGLE_QUOTED, DOUBLE_QUOTED, etc. + String getAnchor(); + String getValue(); + Scalar withValue(String value); + Scalar withStyle(Style style); + + enum Style { + PLAIN, + SINGLE_QUOTED, + DOUBLE_QUOTED, + LITERAL, + FOLDED + } +} +``` + +**Example YAML:** +```yaml +plain: value +single: 'value' +double: "value" +number: 42 +boolean: true +null_value: null +multiline: | + Line 1 + Line 2 +``` + +**Usage:** +```java +// Reading scalar values +if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + String value = scalar.getValue(); + Yaml.Scalar.Style style = scalar.getStyle(); + + // Modify value while preserving style + Yaml.Scalar updated = scalar.withValue("new-value"); + return entry.withValue(updated); +} + +// Creating new scalars +Yaml.Scalar newScalar = new Yaml.Scalar( + Tree.randomId(), // Unique ID + Space.EMPTY, // Prefix whitespace + Markers.EMPTY, // Markers for search results, etc. + Yaml.Scalar.Style.PLAIN, // Quoting style + null, // Anchor + "value" // Actual value +); +``` + +--- + +### Yaml.Scalar.Key + +Special scalar type used for keys in mappings. + +```java +public interface Key extends Yaml { + String getValue(); + Key withValue(String value); +} +``` + +**Usage:** +```java +// Keys are always accessible via entry.getKey() +String keyName = entry.getKey().getValue(); // No casting needed! + +// Rename a key +if ("old-key".equals(entry.getKey().getValue())) { + Yaml.Scalar.Key newKey = entry.getKey().withValue("new-key"); + return entry.withKey(newKey); +} +``` + +--- + +## Navigation Patterns + +### Cursor Navigation + +The `Cursor` provides context about the current position in the tree. + +```java +// Get parent elements +Cursor parent = getCursor().getParent(); +Cursor grandparent = getCursor().getParent(2); + +// Check parent type +if (parent != null && parent.getValue() instanceof Yaml.Mapping) { + Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue(); + // Process parent... +} + +// Get all ancestors of a type +Iterator mappings = getCursor().getPathAsStream() + .filter(p -> p instanceof Yaml.Mapping) + .map(p -> (Yaml.Mapping) p) + .iterator(); +``` + +### Finding Siblings + +```java +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + // Get parent mapping to access siblings + Cursor parent = getCursor().getParent(); + if (parent != null && parent.getValue() instanceof Yaml.Mapping) { + Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue(); + + // Find sibling entries + for (Yaml.Mapping.Entry sibling : parentMapping.getEntries()) { + if (sibling != entry) { + String siblingKey = sibling.getKey().getValue(); + // Check sibling... + } + } + } + + return super.visitMappingEntry(entry, ctx); +} +``` + +### Path-Based Navigation with JsonPath + +```java +// Match specific paths in YAML structure +JsonPathMatcher jobMatcher = new JsonPathMatcher("$.jobs.*"); +JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.*.steps[*]"); +JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); + +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { + // This is a 'uses' field within a step + // Process it... + } + return super.visitMappingEntry(entry, ctx); +} +``` + +--- + +## Type Checking and Casting + +### Safe Type Checking Pattern + +```java +Block value = entry.getValue(); + +if (value instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) value; + String stringValue = scalar.getValue(); + // Process scalar... + +} else if (value instanceof Yaml.Mapping) { + Yaml.Mapping mapping = (Yaml.Mapping) value; + // Process mapping... + +} else if (value instanceof Yaml.Sequence) { + Yaml.Sequence sequence = (Yaml.Sequence) value; + // Process sequence... + +} else { + // Handle other types or null +} +``` + +### Null Safety + +```java +// Keys never need null checking (always present) +String key = entry.getKey().getValue(); // Safe + +// Values might be null or unexpected types +Block value = entry.getValue(); +if (value == null) { + // Handle null value (represents 'key:' with no value) +} + +// Scalar values can be null string +if (value instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) value; + String stringValue = scalar.getValue(); + if (stringValue == null || "null".equals(stringValue)) { + // Handle YAML null + } +} +``` + +--- + +## Creating New Elements + +### Creating Scalars + +```java +// Plain scalar +Yaml.Scalar plain = new Yaml.Scalar( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + Yaml.Scalar.Style.PLAIN, + null, + "value" +); + +// Quoted scalar +Yaml.Scalar quoted = new Yaml.Scalar( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + Yaml.Scalar.Style.DOUBLE_QUOTED, + null, + "value with spaces" +); +``` + +### Creating Mapping Entries + +```java +// Create key +Yaml.Scalar.Key key = new Yaml.Scalar.Key( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + "key-name" +); + +// Create value +Yaml.Scalar value = new Yaml.Scalar( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + Yaml.Scalar.Style.PLAIN, + null, + "value" +); + +// Create entry +Yaml.Mapping.Entry newEntry = new Yaml.Mapping.Entry( + Tree.randomId(), + Space.format("\n "), // Indentation + Markers.EMPTY, + key, + Space.format(" "), // Space after colon + value +); +``` + +### Creating Sequences + +```java +// Create sequence items +List entries = new ArrayList<>(); + +entries.add(new Yaml.Sequence.Entry( + Tree.randomId(), + Space.format("\n - "), + Markers.EMPTY, + new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY, + Yaml.Scalar.Style.PLAIN, null, "item1"), + false +)); + +entries.add(new Yaml.Sequence.Entry( + Tree.randomId(), + Space.format("\n - "), + Markers.EMPTY, + new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY, + Yaml.Scalar.Style.PLAIN, null, "item2"), + false +)); + +// Create sequence +Yaml.Sequence sequence = new Yaml.Sequence( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + null, // anchor + entries +); +``` + +--- + +## Common Pitfalls + +### 1. Not Calling Super Methods + +```java +// ❌ WRONG - tree traversal stops +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + if ("target".equals(entry.getKey().getValue())) { + return entry.withValue(newValue); + } + return entry; // ❌ Should call super +} + +// ✅ CORRECT +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + if ("target".equals(entry.getKey().getValue())) { + return entry.withValue(newValue); + } + return super.visitMappingEntry(entry, ctx); // ✅ +} +``` + +### 2. Mutating Instead of Creating New Objects + +```java +// ❌ WRONG - LST is immutable +Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); +scalar.setValue("new-value"); // ❌ This doesn't exist + +// ✅ CORRECT +Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); +Yaml.Scalar updated = scalar.withValue("new-value"); +return entry.withValue(updated); +``` + +### 3. Forgetting Type Checks + +```java +// ❌ WRONG - may throw ClassCastException +Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + +// ✅ CORRECT +if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + // Process safely +} +``` + +### 4. Incorrect Whitespace/Indentation + +```java +// ❌ WRONG - no indentation +Space.EMPTY // Results in key:value on same line as parent + +// ✅ CORRECT - proper YAML indentation +Space.format("\n ") // Newline + 2-space indent +``` + +### 5. Not Returning Original When Unchanged + +```java +// ❌ WRONG - creates unnecessary tree copies +return entry.withValue(entry.getValue()); + +// ✅ CORRECT - return original if unchanged +if (shouldModify) { + return entry.withValue(newValue); +} +return super.visitMappingEntry(entry, ctx); // Returns original +``` + +--- + +## Complete Example: Multi-Level Navigation + +```java +/** + * Find all GitHub Actions steps using deprecated actions + * and update them to newer versions + */ +@Override +public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + // Match: $.jobs.*.steps[*].uses + JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); + + if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { + if (entry.getValue() instanceof Yaml.Scalar) { + Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); + String actionRef = scalar.getValue(); + + // Navigate to parent step to check conditions + Cursor stepCursor = getCursor().getParent(2); + if (stepCursor != null && stepCursor.getValue() instanceof Yaml.Mapping) { + Yaml.Mapping step = (Yaml.Mapping) stepCursor.getValue(); + + // Check if step has 'if' condition + boolean hasCondition = false; + for (Yaml.Mapping.Entry stepEntry : step.getEntries()) { + if ("if".equals(stepEntry.getKey().getValue())) { + hasCondition = true; + break; + } + } + + // Only update unconditional steps + if (!hasCondition && actionRef.contains("@v2")) { + String updated = actionRef.replace("@v2", "@v3"); + return entry.withValue(scalar.withValue(updated)); + } + } + } + } + + return super.visitMappingEntry(entry, ctx); +} +``` + +--- + +## Reference Chart + +| LST Type | Represents | Common Methods | Notes | +|----------|-----------|----------------|-------| +| `Documents` | File root | `getDocuments()` | Container for multiple docs | +| `Document` | Single doc | `getBlock()` | May have `---` separator | +| `Mapping` | Key-value pairs | `getEntries()` | Like JSON object | +| `Mapping.Entry` | One key-value | `getKey()`, `getValue()` | Basic building block | +| `Sequence` | Array/list | `getEntries()` | Ordered collection | +| `Sequence.Entry` | Array item | `getBlock()` | Wraps actual value | +| `Scalar` | Primitive value | `getValue()`, `getStyle()` | String, number, bool, null | +| `Scalar.Key` | Mapping key | `getValue()` | Always string, no cast needed | + +## Additional Resources + +- OpenRewrite YAML LST JavaDoc: https://docs.openrewrite.org/reference/yaml-lossless-semantic-trees +- YAML Specification: https://yaml.org/spec/1.2/spec.html +- OpenRewrite Visitor Pattern: https://docs.openrewrite.org/concepts-and-explanations/visitors diff --git a/skills/recipe-writer/scripts/add_license_header.sh b/skills/recipe-writer/scripts/add_license_header.sh new file mode 100755 index 0000000..985a9a5 --- /dev/null +++ b/skills/recipe-writer/scripts/add_license_header.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# License Header Script +# +# Adds or updates license headers in Java source files based on the project's +# license template file (gradle/licenseHeader.txt). +# +# Usage: +# ./add_license_header.sh +# ./add_license_header.sh src/main/java/com/example/MyRecipe.java +# +# Features: +# - Checks for gradle/licenseHeader.txt in repository root +# - Substitutes ${year} with current year +# - Preserves existing package/import statements +# - Skips files that already have the correct header +# +# Exit codes: +# 0 - Success (header added or already present) +# 1 - License template file not found +# 2 - Invalid arguments or file not found + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get current year +CURRENT_YEAR=$(date +%Y) + +# Function to find repository root +find_repo_root() { + local dir="$PWD" + while [[ "$dir" != "/" ]]; do + if [[ -d "$dir/.git" ]] || [[ -f "$dir/build.gradle" ]] || [[ -f "$dir/build.gradle.kts" ]]; then + echo "$dir" + return 0 + fi + dir=$(dirname "$dir") + done + echo "$PWD" + return 1 +} + +# Function to show usage +usage() { + echo "Usage: $0 " + echo "" + echo "Add or update license header in a Java source file." + echo "" + echo "Example:" + echo " $0 src/main/java/com/example/MyRecipe.java" + exit 2 +} + +# Check arguments +if [[ $# -ne 1 ]]; then + usage +fi + +JAVA_FILE="$1" + +# Validate Java file exists +if [[ ! -f "$JAVA_FILE" ]]; then + echo -e "${RED}Error: File not found: $JAVA_FILE${NC}" >&2 + exit 2 +fi + +# Validate it's a Java file +if [[ ! "$JAVA_FILE" =~ \.java$ ]]; then + echo -e "${RED}Error: Not a Java file: $JAVA_FILE${NC}" >&2 + exit 2 +fi + +# Find repository root and license header file +REPO_ROOT=$(find_repo_root) +LICENSE_HEADER_FILE="$REPO_ROOT/gradle/licenseHeader.txt" + +# Check if license header template exists +if [[ ! -f "$LICENSE_HEADER_FILE" ]]; then + echo -e "${YELLOW}Warning: License header template not found at: $LICENSE_HEADER_FILE${NC}" >&2 + echo -e "${YELLOW}Skipping license header addition.${NC}" >&2 + exit 1 +fi + +# Read license header template and substitute ${year} +LICENSE_HEADER=$(sed "s/\${year}/$CURRENT_YEAR/g" "$LICENSE_HEADER_FILE") + +# Create a temporary file for the new content +TEMP_FILE=$(mktemp) +trap "rm -f $TEMP_FILE" EXIT + +# Check if file already has a license header (starts with /* or //) +FIRST_LINE=$(head -n 1 "$JAVA_FILE") + +if [[ "$FIRST_LINE" =~ ^/\* ]] || [[ "$FIRST_LINE" =~ ^// ]]; then + # File has some kind of header comment + # Extract everything after the header comment + + # Find the end of the comment block + if [[ "$FIRST_LINE" =~ ^/\* ]]; then + # Multi-line comment - find the closing */ + LINE_NUM=$(grep -n "\*/" "$JAVA_FILE" | head -n 1 | cut -d: -f1) + if [[ -n "$LINE_NUM" ]]; then + # Extract content after the comment block + tail -n +$((LINE_NUM + 1)) "$JAVA_FILE" > "$TEMP_FILE.body" + else + # No closing found, treat whole file as body + cp "$JAVA_FILE" "$TEMP_FILE.body" + fi + else + # Single-line comments - skip all leading // lines + LINE_NUM=$(grep -n -m 1 "^[^/]" "$JAVA_FILE" | head -n 1 | cut -d: -f1) + if [[ -n "$LINE_NUM" ]]; then + tail -n +$LINE_NUM "$JAVA_FILE" > "$TEMP_FILE.body" + else + # All lines are comments, preserve the file + cp "$JAVA_FILE" "$TEMP_FILE.body" + fi + fi + + # Write new header + body + echo "$LICENSE_HEADER" > "$TEMP_FILE" + echo "" >> "$TEMP_FILE" # Add blank line after header + cat "$TEMP_FILE.body" >> "$TEMP_FILE" + rm -f "$TEMP_FILE.body" + + echo -e "${GREEN}✓ Updated license header in: $JAVA_FILE${NC}" +else + # No header comment found, prepend the license header + echo "$LICENSE_HEADER" > "$TEMP_FILE" + echo "" >> "$TEMP_FILE" # Add blank line after header + cat "$JAVA_FILE" >> "$TEMP_FILE" + + echo -e "${GREEN}✓ Added license header to: $JAVA_FILE${NC}" +fi + +# Replace original file with new content +mv "$TEMP_FILE" "$JAVA_FILE" + +exit 0 diff --git a/skills/recipe-writer/scripts/init_recipe.py b/skills/recipe-writer/scripts/init_recipe.py new file mode 100755 index 0000000..2acdaa6 --- /dev/null +++ b/skills/recipe-writer/scripts/init_recipe.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +""" +OpenRewrite Recipe Initialization Script + +Generates boilerplate code for new OpenRewrite YAML recipes including: +- Recipe class file +- Test file +- Optional declarative YAML recipe + +Usage: + python init_recipe.py --name MyRecipe --package com.example.rewrite --description "Recipe description" +""" + +import argparse +import os +import sys +from datetime import datetime +from pathlib import Path + + +def read_license_header(): + """Read license header from gradle/licenseHeader.txt if it exists.""" + license_path = Path.cwd() + while license_path != license_path.parent: + license_file = license_path / "gradle" / "licenseHeader.txt" + if license_file.exists(): + with open(license_file, 'r') as f: + content = f.read() + # Substitute ${year} with current year + content = content.replace("${year}", str(datetime.now().year)) + return content + "\n" + license_path = license_path.parent + return "" + + +def to_snake_case(name): + """Convert PascalCase to snake_case.""" + result = [] + for i, char in enumerate(name): + if char.isupper() and i > 0: + result.append('_') + result.append(char.lower()) + return ''.join(result) + + +def generate_recipe_class(name, package, description, license_header): + """Generate the recipe class file content.""" + return f"""{license_header}package {package}; + +import org.openrewrite.*; +import org.openrewrite.yaml.YamlIsoVisitor; +import org.openrewrite.yaml.tree.Yaml; + +public class {name} extends Recipe {{ + + @Override + public String getDisplayName() {{ + return "{name}"; + }} + + @Override + public String getDescription() {{ + return "{description}"; + }} + + @Override + public TreeVisitor getVisitor() {{ + return new YamlIsoVisitor() {{ + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{ + // TODO: Implement recipe logic + return super.visitMappingEntry(entry, ctx); + }} + }}; + }} +}} +""" + + +def generate_parameterized_recipe_class(name, package, description, license_header): + """Generate a parameterized recipe class file content.""" + return f"""{license_header}package {package}; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.yaml.YamlIsoVisitor; +import org.openrewrite.yaml.tree.Yaml; + +@Value +@EqualsAndHashCode(callSuper = false) +public class {name} extends Recipe {{ + + @Option( + displayName = "Parameter name", + description = "Description of the parameter", + example = "example-value" + ) + String parameterName; + + @Override + public String getDisplayName() {{ + return "{name}"; + }} + + @Override + public String getDescription() {{ + return "{description}"; + }} + + @Override + public TreeVisitor getVisitor() {{ + return new YamlIsoVisitor() {{ + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{ + // TODO: Implement recipe logic using parameterName + return super.visitMappingEntry(entry, ctx); + }} + }}; + }} +}} +""" + + +def generate_test_class(name, package, license_header): + """Generate the test class file content.""" + return f"""{license_header}package {package}; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.yaml.Assertions.yaml; + +class {name}Test implements RewriteTest {{ + + @Override + public void defaults(RecipeSpec spec) {{ + spec.recipe(new {name}()); + }} + + @Test + void basicTransformation() {{ + rewriteRun( + yaml( + \"\"\" + # Before YAML + key: old-value + \"\"\", + \"\"\" + # After YAML + key: new-value + \"\"\" + ) + ); + }} + + @Test + void doesNotChangeUnrelatedYaml() {{ + rewriteRun( + yaml( + \"\"\" + unrelated: value + \"\"\" + ) + ); + }} + + @Test + void handlesEdgeCases() {{ + rewriteRun( + yaml( + \"\"\" + # Empty value + key: + # Null value + key2: null + \"\"\" + ) + ); + }} +}} +""" + + +def generate_declarative_recipe(name, package, description): + """Generate declarative YAML recipe content.""" + return f"""--- +type: specs.openrewrite.org/v1beta/recipe +name: {package}.{name} +displayName: {name} +description: {description} +recipeList: + - org.openrewrite.yaml.search.FindKey: + keyPath: $.some.path + - org.openrewrite.yaml.ChangeValue: + keyPath: $.some.path + value: newValue +""" + + +def create_file(path, content): + """Create a file with the given content.""" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, 'w') as f: + f.write(content) + print(f"Created: {path}") + + +def main(): + parser = argparse.ArgumentParser( + description="Initialize a new OpenRewrite YAML recipe with boilerplate code" + ) + parser.add_argument( + "--name", + required=True, + help="Recipe class name (PascalCase, e.g., UpdateGitHubActions)" + ) + parser.add_argument( + "--package", + required=True, + help="Package name (e.g., com.example.rewrite)" + ) + parser.add_argument( + "--description", + required=True, + help="Recipe description" + ) + parser.add_argument( + "--parameterized", + action="store_true", + help="Generate parameterized recipe with @Option annotation" + ) + parser.add_argument( + "--declarative", + action="store_true", + help="Also generate declarative YAML recipe template" + ) + parser.add_argument( + "--output-dir", + default=".", + help="Output directory (default: current directory)" + ) + + args = parser.parse_args() + + # Validate recipe name + if not args.name[0].isupper(): + print("Error: Recipe name must start with uppercase letter (PascalCase)") + sys.exit(1) + + # Read license header + license_header = read_license_header() + if license_header: + print(f"Found license header (will use year {datetime.now().year})") + + # Convert package to path + package_path = args.package.replace('.', '/') + base_path = Path(args.output_dir) + + # Generate file paths + recipe_path = base_path / "src" / "main" / "java" / package_path / f"{args.name}.java" + test_path = base_path / "src" / "test" / "java" / package_path / f"{args.name}Test.java" + + # Generate recipe class + if args.parameterized: + recipe_content = generate_parameterized_recipe_class( + args.name, args.package, args.description, license_header + ) + else: + recipe_content = generate_recipe_class( + args.name, args.package, args.description, license_header + ) + + # Generate test class + test_content = generate_test_class(args.name, args.package, license_header) + + # Create files + create_file(recipe_path, recipe_content) + create_file(test_path, test_content) + + # Generate declarative recipe if requested + if args.declarative: + yaml_name = to_snake_case(args.name) + yaml_path = base_path / "src" / "main" / "resources" / "META-INF" / "rewrite" / f"{yaml_name}.yml" + yaml_content = generate_declarative_recipe(args.name, args.package, args.description) + create_file(yaml_path, yaml_content) + + print("\n✓ Recipe initialization complete!") + print("\nNext steps:") + print("1. Write failing tests in the test file") + print("2. Implement the recipe logic") + print("3. Run tests to verify: ./gradlew test") + if args.declarative: + print("4. Choose between imperative (Java) or declarative (YAML) approach") + + +if __name__ == "__main__": + main() diff --git a/skills/recipe-writer/scripts/validate_recipe.py b/skills/recipe-writer/scripts/validate_recipe.py new file mode 100755 index 0000000..eb18964 --- /dev/null +++ b/skills/recipe-writer/scripts/validate_recipe.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +Validate OpenRewrite recipe structure, naming conventions, and Java compatibility. + +This script checks: +1. Recipe class structure (extends Recipe, has required annotations) +2. Required methods (getDisplayName, getDescription, getVisitor) +3. Naming conventions (package, class name, display name) +4. Java compatibility (can compile with Java 8) +5. YAML recipe format (for declarative recipes) + +Usage: + python validate_recipe.py + python validate_recipe.py --java-version 8 + python validate_recipe.py --no-compile +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import List, Tuple + + +class Colors: + """ANSI color codes for terminal output""" + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + RESET = '\033[0m' + BOLD = '\033[1m' + + +def print_success(message: str): + print(f"{Colors.GREEN}✓{Colors.RESET} {message}") + + +def print_error(message: str): + print(f"{Colors.RED}✗{Colors.RESET} {message}") + + +def print_warning(message: str): + print(f"{Colors.YELLOW}⚠{Colors.RESET} {message}") + + +def print_info(message: str): + print(f"{Colors.BLUE}ℹ{Colors.RESET} {message}") + + +def check_java_file_structure(content: str, file_path: Path) -> List[str]: + """Validate Java recipe file structure""" + errors = [] + + # Check for Recipe class + if not re.search(r'class\s+\w+\s+extends\s+Recipe', content): + errors.append("Recipe class must extend Recipe") + + # Check for @Value annotation (for immutability) + if '@Value' not in content: + print_warning(f"Consider using @Value annotation for immutability") + + # Check for @EqualsAndHashCode + if '@EqualsAndHashCode' not in content: + print_warning("Consider using @EqualsAndHashCode(callSuper = false)") + + # Check for required methods + if 'getDisplayName()' not in content: + errors.append("Recipe must override getDisplayName()") + + if 'getDescription()' not in content: + errors.append("Recipe must override getDescription()") + + if 'getVisitor()' not in content: + errors.append("Recipe must override getVisitor()") + + # Check for proper return in getVisitor + if 'getVisitor()' in content: + visitor_match = re.search(r'public\s+TreeVisitor<\?.*?>\s+getVisitor\([^)]*\)\s*{([^}]+)}', content, re.DOTALL) + if visitor_match: + visitor_body = visitor_match.group(1) + if 'new ' not in visitor_body: + print_warning("getVisitor() should return a NEW instance (no caching)") + + # Check display name ends with period + display_name_match = re.search(r'getDisplayName\(\)\s*{\s*return\s*"([^"]+)"', content) + if display_name_match: + display_name = display_name_match.group(1) + if not display_name.endswith('.') and not display_name.endswith('!') and not display_name.endswith('?'): + print_warning(f"Display name should end with a period: '{display_name}'") + + # Check for @Option annotations on parameters + option_count = content.count('@Option') + if option_count > 0: + # Check that options have example + for match in re.finditer(r'@Option\([^)]+\)', content): + option = match.group(0) + if 'example' not in option: + print_warning("@Option should include an example parameter") + + return errors + + +def check_naming_conventions(content: str, file_path: Path) -> List[str]: + """Check naming conventions""" + errors = [] + + # Extract package name + package_match = re.search(r'package\s+([\w.]+);', content) + if package_match: + package = package_match.group(1) + if package.startswith('com.yourorg') or package.startswith('com.example'): + print_warning(f"Update placeholder package name: {package}") + + # Extract class name + class_match = re.search(r'class\s+(\w+)\s+extends\s+Recipe', content) + if class_match: + class_name = class_match.group(1) + + # Check naming convention (VerbNoun pattern) + if not re.match(r'^[A-Z][a-z]+[A-Z]', class_name): + print_warning(f"Recipe class name should follow VerbNoun pattern: {class_name}") + + # Check file name matches class name + expected_filename = f"{class_name}.java" + if file_path.name != expected_filename: + errors.append(f"File name {file_path.name} does not match class name {expected_filename}") + + return errors + + +def check_java8_compatibility_patterns(content: str) -> List[str]: + """Check for Java 8 incompatible patterns""" + warnings = [] + + # Check for var keyword + if re.search(r'\bvar\b', content): + warnings.append("Found 'var' keyword - not available in Java 8") + + # Check for text blocks + if '"""' in content: + warnings.append("Found text blocks (triple quotes) - not available in Java 8") + + # Check for switch expressions + if re.search(r'switch\s*\([^)]+\)\s*{[^}]*->', content): + warnings.append("Found switch expression - not available in Java 8") + + # Check for pattern matching + if re.search(r'instanceof\s+\w+\s+\w+\s+&&', content): + warnings.append("Found pattern matching in instanceof - not available in Java 8") + + # Check for record keyword + if re.search(r'\brecord\s+\w+', content): + warnings.append("Found record - not available in Java 8") + + return warnings + + +def compile_with_javac(file_path: Path, java_version: int = 8) -> Tuple[bool, str]: + """Try to compile the file with javac""" + try: + # Create a temporary directory for compilation + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + result = subprocess.run( + ['javac', '-source', str(java_version), '-target', str(java_version), + '-d', tmpdir, str(file_path)], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return True, "" + else: + return False, result.stderr + + except FileNotFoundError: + return False, "javac not found in PATH" + except subprocess.TimeoutExpired: + return False, "Compilation timed out" + except Exception as e: + return False, str(e) + + +def validate_yaml_recipe(content: str, file_path: Path) -> List[str]: + """Validate YAML recipe format""" + errors = [] + + # Check for required fields + if 'type: specs.openrewrite.org/v1beta/recipe' not in content: + errors.append("YAML recipe must have 'type: specs.openrewrite.org/v1beta/recipe'") + + if not re.search(r'^name:\s+[\w.]+', content, re.MULTILINE): + errors.append("YAML recipe must have 'name' field") + + if not re.search(r'^displayName:', content, re.MULTILINE): + errors.append("YAML recipe must have 'displayName' field") + + if not re.search(r'^description:', content, re.MULTILINE): + errors.append("YAML recipe must have 'description' field") + + if not re.search(r'^recipeList:', content, re.MULTILINE): + errors.append("YAML recipe must have 'recipeList' field") + + # Check naming convention + name_match = re.search(r'^name:\s+([\w.]+)', content, re.MULTILINE) + if name_match: + name = name_match.group(1) + if not re.match(r'^[\w.]+\.[\w.]+$', name): + print_warning(f"Recipe name should be fully qualified: {name}") + if 'yourorg' in name.lower() or 'example' in name.lower(): + print_warning(f"Update placeholder recipe name: {name}") + + return errors + + +def validate_recipe(file_path: Path, java_version: int = 8, skip_compile: bool = False) -> bool: + """Validate a recipe file""" + print(f"\n{Colors.BOLD}Validating: {file_path}{Colors.RESET}\n") + + if not file_path.exists(): + print_error(f"File not found: {file_path}") + return False + + # Read file content + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + except Exception as e: + print_error(f"Error reading file: {e}") + return False + + errors = [] + warnings = [] + + # Determine file type + if file_path.suffix == '.java': + print_info("Checking Java recipe structure...") + errors.extend(check_java_file_structure(content, file_path)) + + print_info("Checking naming conventions...") + errors.extend(check_naming_conventions(content, file_path)) + + print_info(f"Checking Java {java_version} compatibility...") + java8_warnings = check_java8_compatibility_patterns(content) + warnings.extend(java8_warnings) + + # Try to compile if not skipped + if not skip_compile: + print_info("Attempting compilation...") + success, compile_error = compile_with_javac(file_path, java_version) + if not success: + if "javac not found" in compile_error: + print_warning("javac not found - skipping compilation check") + else: + errors.append(f"Compilation failed:\n{compile_error}") + else: + print_success("Compilation successful") + + elif file_path.suffix in ['.yml', '.yaml']: + print_info("Checking YAML recipe format...") + errors.extend(validate_yaml_recipe(content, file_path)) + + else: + print_error(f"Unsupported file type: {file_path.suffix}") + return False + + # Print results + print() + if errors: + print(f"{Colors.RED}{Colors.BOLD}ERRORS:{Colors.RESET}") + for error in errors: + print_error(error) + print() + + if warnings: + print(f"{Colors.YELLOW}{Colors.BOLD}WARNINGS:{Colors.RESET}") + for warning in warnings: + print_warning(warning) + print() + + if not errors and not warnings: + print(f"{Colors.GREEN}{Colors.BOLD}✓ All checks passed!{Colors.RESET}\n") + return True + elif not errors: + print(f"{Colors.YELLOW}{Colors.BOLD}⚠ Validation passed with warnings{Colors.RESET}\n") + return True + else: + print(f"{Colors.RED}{Colors.BOLD}✗ Validation failed{Colors.RESET}\n") + return False + + +def main(): + parser = argparse.ArgumentParser( + description='Validate OpenRewrite recipe files', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + # Validate a Java recipe + python validate_recipe.py src/main/java/com/example/MyRecipe.java + + # Validate with Java 11 + python validate_recipe.py MyRecipe.java --java-version 11 + + # Skip compilation check + python validate_recipe.py MyRecipe.java --no-compile + + # Validate a YAML recipe + python validate_recipe.py src/main/resources/META-INF/rewrite/my-recipe.yml + ''' + ) + + parser.add_argument('path', type=str, help='Path to recipe file') + parser.add_argument('--java-version', type=int, default=8, + help='Target Java version (default: 8)') + parser.add_argument('--no-compile', action='store_true', + help='Skip compilation check') + + args = parser.parse_args() + + file_path = Path(args.path) + success = validate_recipe(file_path, args.java_version, args.no_compile) + + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main() diff --git a/skills/recipe-writer/templates/license-header.txt b/skills/recipe-writer/templates/license-header.txt new file mode 100644 index 0000000..3c7a454 --- /dev/null +++ b/skills/recipe-writer/templates/license-header.txt @@ -0,0 +1,13 @@ +Copyright ${year} the original author or authors. +

+Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +

+https://www.apache.org/licenses/LICENSE-2.0 +

+Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/skills/recipe-writer/templates/template-declarative-recipe.yml b/skills/recipe-writer/templates/template-declarative-recipe.yml new file mode 100644 index 0000000..6c1840f --- /dev/null +++ b/skills/recipe-writer/templates/template-declarative-recipe.yml @@ -0,0 +1,122 @@ +--- +# OpenRewrite Declarative Recipe Template +# Save this file in: src/main/resources/META-INF/rewrite/ + +# Required: Recipe type identifier +type: specs.openrewrite.org/v1beta/recipe + +# Required: Fully qualified recipe name (convention: com.yourorg.RecipeName) +name: com.yourorg.YourRecipeName + +# Required: Human-readable recipe name (sentence case, end with period if sentence) +displayName: Your recipe display name + +# Required: Clear description of what the recipe does +description: A clear description of what this recipe accomplishes. This can span multiple lines and should explain the purpose and effect of running this recipe. + +# Optional: Tags for categorization and searchability +tags: + - category1 + - category2 + - framework-name + +# Optional: Estimated time saved by this recipe (in ISO-8601 duration format) +# estimatedEffortPerOccurrence: PT5M + +# Optional: Set to true if this recipe requires multiple execution cycles +# causesAnotherCycle: true + +# Optional: Define preconditions that files must meet to run this recipe +# preconditions: +# - org.openrewrite.java.search.UsesType: +# fullyQualifiedTypeName: com.example.TargetType + +# Required: List of recipes to execute (in order) +recipeList: + # Example: Recipe with no parameters + - org.openrewrite.java.format.AutoFormat + + # Example: Recipe with parameters + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: old.package.OldType + newFullyQualifiedTypeName: new.package.NewType + # Optional parameter + ignoreDefinition: false + + # Example: Another recipe with parameters + - org.openrewrite.java.ChangePackage: + oldPackageName: com.old.package + newPackageName: com.new.package + recursive: true + + # Example: Add a dependency (only if type is used) + - org.openrewrite.java.dependencies.AddDependency: + groupId: org.example + artifactId: example-library + version: latest.release + onlyIfUsing: com.example.SomeClass + configuration: implementation + + # Example: Maven-specific recipe + - org.openrewrite.maven.UpgradePluginVersion: + groupId: org.apache.maven.plugins + artifactId: maven-compiler-plugin + newVersion: 3.11.0 + + # Add your recipes here + # - com.yourorg.AnotherRecipe + # - com.yourorg.YetAnotherRecipe: + # parameter: value + +--- +# You can define multiple recipes in the same file by using '---' separator + +# type: specs.openrewrite.org/v1beta/recipe +# name: com.yourorg.AnotherRecipe +# displayName: Another recipe +# description: Description of the second recipe. +# recipeList: +# - ... + +# Common Recipe References: +# +# Java Type Changes: +# - org.openrewrite.java.ChangeType +# - org.openrewrite.java.ChangePackage +# - org.openrewrite.java.ChangeMethodName +# - org.openrewrite.java.ChangeFieldName +# +# Dependency Management: +# - org.openrewrite.java.dependencies.AddDependency +# - org.openrewrite.java.dependencies.RemoveDependency +# - org.openrewrite.java.dependencies.ChangeDependency +# - org.openrewrite.java.dependencies.UpgradeDependencyVersion +# +# Maven: +# - org.openrewrite.maven.UpgradePluginVersion +# - org.openrewrite.maven.UpgradeDependencyVersion +# - org.openrewrite.maven.ChangePropertyValue +# +# Static Analysis: +# - org.openrewrite.staticanalysis.CommonStaticAnalysis +# - org.openrewrite.staticanalysis.FinalizeLocalVariables +# - org.openrewrite.staticanalysis.RemoveUnusedImports +# +# Testing: +# - org.openrewrite.java.testing.junit5.JUnit4to5Migration +# - org.openrewrite.java.testing.junit5.AssertToAssertions +# +# Spring: +# - org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0 +# - org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7 +# +# Formatting: +# - org.openrewrite.java.format.AutoFormat +# - org.openrewrite.java.format.RemoveUnusedImports +# - org.openrewrite.java.format.TabsAndIndents +# +# Search recipes (use these in preconditions): +# - org.openrewrite.java.search.UsesType +# - org.openrewrite.java.search.UsesMethod +# - org.openrewrite.java.search.FindTypes +# - org.openrewrite.java.search.FindMethods diff --git a/skills/recipe-writer/templates/template-imperative-recipe.java b/skills/recipe-writer/templates/template-imperative-recipe.java new file mode 100644 index 0000000..8bef13b --- /dev/null +++ b/skills/recipe-writer/templates/template-imperative-recipe.java @@ -0,0 +1,200 @@ +package com.yourorg; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.NonNull; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.J; + +/** + * TODO: Add recipe description + * + * Example usage in YAML: + * ```yaml + * type: specs.openrewrite.org/v1beta/recipe + * name: com.yourorg.MyRecipeGroup + * recipeList: + * - com.yourorg.TemplateRecipe: + * parameterName: value + * ``` + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class TemplateRecipe extends Recipe { + + /** + * TODO: Add options for recipe configuration + * Each option becomes a parameter that can be configured in YAML + */ + @Option( + displayName = "Parameter Display Name", + description = "A clear description of what this parameter does.", + example = "com.example.ExampleValue" + ) + @NonNull + String parameterName; + + // Add more @Option fields as needed + // @Option(displayName = "Another Parameter", ...) + // String anotherParameter; + + /** + * All recipes must be serializable via Jackson. + * Use @JsonCreator and @JsonProperty annotations. + */ + @JsonCreator + public TemplateRecipe( + @NonNull @JsonProperty("parameterName") String parameterName + ) { + this.parameterName = parameterName; + } + + @Override + public String getDisplayName() { + return "Your recipe display name"; + } + + @Override + public String getDescription() { + return "A clear description of what this recipe does. Use sentence case and end with a period."; + } + + /** + * Optional: Add preconditions to improve performance + * Recipes only run on files that match these conditions + */ + // @Override + // public TreeVisitor getVisitor() { + // return Preconditions.check( + // new UsesType<>("com.example.SomeType", true), + // new TemplateRecipeVisitor() + // ); + // } + + @Override + public TreeVisitor getVisitor() { + // IMPORTANT: Always return a NEW instance (no caching) + return new TemplateRecipeVisitor(); + } + + /** + * The visitor implements the actual transformation logic. + * Use JavaIsoVisitor when always returning the same LST type. + */ + public class TemplateRecipeVisitor extends JavaIsoVisitor { + + /** + * Optional: Create JavaTemplates for complex code generation + * Templates are parsed once and can be reused + */ + // private final JavaTemplate template = JavaTemplate + // .builder("your.code.template(#{any(String)})") + // .imports("your.imports.Here") + // .build(); + + /** + * Override visit methods for the LST elements you want to transform. + * Common visit methods: + * - visitCompilationUnit() - entire file + * - visitClassDeclaration() - class declarations + * - visitMethodDeclaration() - method declarations + * - visitMethodInvocation() - method calls + * - visitVariableDeclarations() - variable declarations + * - visitAssignment() - assignments + * - visitBinary() - binary operations + * - visitImport() - imports + */ + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + // Step 1: ALWAYS call super to traverse the subtree + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + // Step 2: Check if this element needs to be changed + // DO NO HARM: If unsure, return unchanged + if (!shouldChange(cd)) { + return cd; + } + + // Step 3: Make your changes + // Never mutate the LST - always use .withX() methods + cd = makeYourChanges(cd, ctx); + + // Step 4: Optional - chain other visitors + // doAfterVisit(new SomeOtherRecipe().getVisitor()); + + // Step 5: Optional - add/remove imports + // maybeAddImport("java.util.List"); + // maybeRemoveImport("old.package.Type"); + + return cd; + } + + /** + * Helper method to determine if changes are needed + */ + private boolean shouldChange(J.ClassDeclaration classDecl) { + // TODO: Implement your logic + // Check if change is necessary and safe + // Example: Check if class matches criteria + + // Check for null type (avoid NPE) + if (classDecl.getType() == null) { + return false; + } + + // Example: Check fully qualified name + // if (!classDecl.getType().getFullyQualifiedName().equals(parameterName)) { + // return false; + // } + + // Example: Check if change already exists + // if (alreadyHasTheChange(classDecl)) { + // return false; + // } + + return true; + } + + /** + * Helper method to perform the transformation + */ + private J.ClassDeclaration makeYourChanges(J.ClassDeclaration classDecl, ExecutionContext ctx) { + // TODO: Implement your transformation logic + + // Example: Using a JavaTemplate + // classDecl = classDecl.withBody( + // template.apply( + // new Cursor(getCursor(), classDecl.getBody()), + // classDecl.getBody().getCoordinates().lastStatement(), + // someParameter + // ) + // ); + + // Example: Modifying using LST methods + // classDecl = classDecl.withName(classDecl.getName().withSimpleName("NewName")); + + // Example: Using ListUtils to avoid mutation + // classDecl = classDecl.withModifiers( + // ListUtils.concat(classDecl.getModifiers(), newModifier) + // ); + + return classDecl; + } + + /** + * Optional: Example of another visit method + */ + // @Override + // public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + // J.MethodInvocation m = super.visitMethodInvocation(method, ctx); + // + // // Your logic here + // + // return m; + // } + } +} diff --git a/skills/recipe-writer/templates/template-recipe-test.java b/skills/recipe-writer/templates/template-recipe-test.java new file mode 100644 index 0000000..92abc7b --- /dev/null +++ b/skills/recipe-writer/templates/template-recipe-test.java @@ -0,0 +1,292 @@ +package com.yourorg; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test class for TemplateRecipe + * + * Best Practices: + * - Test both cases where changes ARE made and where they ARE NOT made + * - Test edge cases and boundary conditions + * - Use meaningful test names that describe what is being tested + * - Add @DocumentExample to one test to generate documentation + */ +class TemplateRecipeTest implements RewriteTest { + + /** + * Configure defaults for all tests in this class. + * This is called before each test method. + */ + @Override + public void defaults(RecipeSpec spec) { + // Set the recipe to test with default parameters + spec.recipe(new TemplateRecipe("parameter-value")); + + // Optional: Configure parser with classpath dependencies + // spec.parser(JavaParser.fromJavaVersion() + // .classpath("library-name") + // .logCompilationWarningsAndErrors(true)); + + // Optional: Configure for specific Java version + // spec.allSources(s -> s.markers(javaVersion(17))); + } + + /** + * Test that the recipe makes the expected change. + * @DocumentExample marks this as the primary example for documentation. + */ + @DocumentExample + @Test + void makesExpectedChange() { + rewriteRun( + // First argument: before state + // Second argument: after state + java( + """ + package com.example; + + class BeforeExample { + // TODO: Add code that should be changed + } + """, + """ + package com.example; + + class AfterExample { + // TODO: Add expected code after transformation + } + """ + ) + ); + } + + /** + * Test that the recipe does NOT make changes when they are not needed. + * This is crucial - recipes must not make unnecessary changes! + */ + @Test + void doesNotChangeWhenNotNeeded() { + rewriteRun( + java( + """ + package com.example; + + class AlreadyCorrect { + // TODO: Add code that should NOT be changed + } + """ + // No second argument means we expect NO changes + ) + ); + } + + /** + * Test edge case or boundary condition + */ + @Test + void handlesEdgeCase() { + rewriteRun( + java( + """ + package com.example; + + class EdgeCase { + // TODO: Add edge case scenario + } + """, + """ + package com.example; + + class EdgeCase { + // TODO: Add expected result for edge case + } + """ + ) + ); + } + + /** + * Example: Test with multiple files + * Demonstrates that some files change and others don't + */ + @Test + void handlesMultipleFiles() { + rewriteRun( + // First file: should change + java( + """ + package com.example; + + class ShouldChange { + } + """, + """ + package com.example; + + class DidChange { + } + """ + ), + // Second file: should NOT change + java( + """ + package com.example; + + class ShouldNotChange { + } + """ + ) + ); + } + + /** + * Example: Test with custom recipe spec for this specific test + */ + @Test + void testWithCustomConfiguration() { + rewriteRun( + // Customize the spec for just this test + spec -> spec + .recipe(new TemplateRecipe("different-parameter")) + // Add specific classpath for this test + // .parser(JavaParser.fromJavaVersion().classpath("specific-library")) + , + java( + """ + package com.example; + + class Example { + } + """, + """ + package com.example; + + class Example { + // Changes based on different parameter + } + """ + ) + ); + } + + /** + * Example: Test with afterRecipe callback for additional assertions + */ + @Test + void testWithCallback() { + rewriteRun( + java( + """ + package com.example; + + class Example { + } + """, + """ + package com.example; + + class Example { + // Some change + } + """, + // Callback to perform additional assertions after recipe runs + spec -> spec.afterRecipe(cu -> { + // Custom assertions on the compilation unit + assertThat(cu.getClasses()).hasSize(1); + // Add more assertions as needed + }) + ) + ); + } + + /** + * Example: Test for declarative YAML recipe + */ + // @Test + // void testDeclarativeRecipe() { + // rewriteRun( + // spec -> spec + // .recipeFromResources("com.yourorg.TemplateRecipe") + // .parser(JavaParser.fromJavaVersion() + // .classpath("dependencies-needed-for-before-code")), + // java( + // """ + // package com.example; + // + // class Before { + // } + // """, + // """ + // package com.example; + // + // class After { + // } + // """ + // ) + // ); + // } + + /** + * Example: Test with specific file path + */ + // @Test + // void testWithSpecificPath() { + // rewriteRun( + // java( + // """ + // server.port=8080 + // """, + // """ + // server.port=80 + // """, + // spec -> spec.path("src/main/resources/application.properties") + // ) + // ); + // } + + /** + * Example: Test with Java version marker + */ + // @Test + // void testWithJavaVersion() { + // rewriteRun( + // spec -> spec.allSources(s -> s.markers(javaVersion(17))), + // java( + // """ + // package com.example; + // + // class Example { + // // Java 17 specific code + // } + // """ + // ) + // ); + // } + + /** + * Example: Test combining different source file types + */ + // @Test + // void testMultipleFileTypes() { + // rewriteRun( + // java( + // """ + // package com.example; + // class Example { } + // """ + // ), + // // You can mix java(), xml(), yaml(), properties(), etc. + // // yaml( + // // """ + // // key: value + // // """ + // // ) + // ); + // } +} diff --git a/skills/recipe-writer/templates/template-refaster-template.java b/skills/recipe-writer/templates/template-refaster-template.java new file mode 100644 index 0000000..7e8ae94 --- /dev/null +++ b/skills/recipe-writer/templates/template-refaster-template.java @@ -0,0 +1,134 @@ +package com.yourorg; + +import com.google.errorprone.refaster.annotation.AfterTemplate; +import com.google.errorprone.refaster.annotation.BeforeTemplate; +import org.openrewrite.java.template.RecipeDescriptor; + +/** + * Refaster template for simple expression/statement replacements. + * + * Refaster templates provide a middle ground between declarative YAML and imperative Java recipes: + * - Faster than imperative recipes + * - Type-aware matching + * - Concise syntax + * - Good for API migrations + * + * Usage: + * 1. Define @BeforeTemplate with the code pattern to match + * 2. Define @AfterTemplate with the replacement code + * 3. OpenRewrite generates a recipe that performs the transformation + * + * Example usage in YAML: + * ```yaml + * type: specs.openrewrite.org/v1beta/recipe + * name: com.yourorg.MyRefasterRecipe + * recipeList: + * - com.yourorg.TemplateRefaster + * ``` + */ +@RecipeDescriptor( + name = "Your Refaster recipe name", + description = "Clear description of what this Refaster template accomplishes." +) +public class TemplateRefaster { + + /** + * Example 1: Simple method call replacement + * Replaces StringUtils.equals() with Objects.equals() + */ + public static class ReplaceStringUtilsEquals { + @BeforeTemplate + boolean before(String s1, String s2) { + return org.apache.commons.lang3.StringUtils.equals(s1, s2); + } + + @AfterTemplate + boolean after(String s1, String s2) { + return java.util.Objects.equals(s1, s2); + } + } + + /** + * Example 2: Expression replacement with type awareness + * Replaces new ArrayList<>() with List.of() for immutable lists (Java 9+) + */ + public static class ReplaceArrayListWithListOf { + @BeforeTemplate + java.util.List before() { + return new java.util.ArrayList<>(); + } + + @AfterTemplate + java.util.List after() { + return java.util.List.of(); + } + } + + /** + * Example 3: Statement replacement + * Replaces traditional for loop with enhanced for loop + */ + public static class ReplaceTraditionalForWithEnhanced { + @BeforeTemplate + void before(java.util.List items) { + for (int i = 0; i < items.size(); i++) { + String item = items.get(i); + System.out.println(item); + } + } + + @AfterTemplate + void after(java.util.List items) { + for (String item : items) { + System.out.println(item); + } + } + } + + /** + * Example 4: API migration with different parameters + * Migrates from old API to new API with parameter reordering + */ + public static class MigrateOldApiToNew { + @BeforeTemplate + void before(String value, int timeout) { + com.oldapi.Client.connect(value, timeout); + } + + @AfterTemplate + void after(String value, int timeout) { + com.newapi.Client.connect(timeout, value); + } + } + + /** + * TODO: Add your Refaster templates here + * + * Tips: + * - Keep templates simple - complex logic should use imperative recipes + * - Use type parameters for generic matching (, , etc.) + * - Method names (before/after) can be anything - only annotations matter + * - Return types and parameters must match between before and after for type safety + * - You can have multiple nested template classes in one file + */ + + public static class YourRefasterTemplate { + /** + * Define what code pattern to match + */ + @BeforeTemplate + void before() { + // TODO: Add the code pattern you want to match and replace + // Example: someOldMethod() + } + + /** + * Define what to replace it with + */ + @AfterTemplate + void after() { + // TODO: Add the replacement code + // Example: someNewMethod() + } + } +}