Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:41 +08:00
commit c2d0b101b0
22 changed files with 6446 additions and 0 deletions

View File

@@ -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<JavaProject, T>`
- [ ] 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

View File

@@ -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<?, ExecutionContext> 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<JavaProject, Set<String>> projectData = new HashMap<>();
}
```
### Scanner (First Pass - Collect Only)
```java
@Override
public TreeVisitor<?, ExecutionContext> 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<?, ExecutionContext> 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<String> 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);
```

View File

@@ -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<Import> getImports();
List<ClassDeclaration> 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<J.Import> imports = cu.getImports();
// Access classes
List<J.ClassDeclaration> classes = cu.getClasses();
return cu;
}
```
---
### J.ClassDeclaration
Represents class, interface, enum, or record declarations.
```java
public interface ClassDeclaration extends Statement, TypedTree {
List<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
Kind getKind(); // Class, Interface, Enum, Record, Annotation
Identifier getName();
@Nullable TypeParameters getTypeParameters();
@Nullable TypeTree getExtends();
@Nullable Container<TypeTree> 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<Statement> 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<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
@Nullable TypeParameters getTypeParameters();
@Nullable TypeTree getReturnTypeExpression();
Identifier getName();
List<Statement> getParameters();
@Nullable Container<NameTree> 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<Statement> 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<Expression> 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<Expression> 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<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
@Nullable TypeTree getTypeExpression();
List<NamedVariable> 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<Expression> 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<Expression> 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<Statement> 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

View File

@@ -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/

View File

@@ -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<T extends Tree>` 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<T extends Tree> {
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<Yaml.Block> {
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<YamlScalar> {
// 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<U>` 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<U> 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<U> higher(Cursor cursor)
// Stream of all matching traits in descendants (down the tree)
Stream<U> lower(Cursor cursor)
// Stream of all matching traits in entire source file
Stream<U> lower(SourceFile sourceFile)
// Convert matcher to TreeVisitor for use in recipes
<P> TreeVisitor<? extends Tree, P> asVisitor(VisitFunction2<U, P> visitor)
```
### How SimpleTraitMatcher Implements These Methods
`SimpleTraitMatcher<U>` 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<ActionStep> allSteps = stepMatcher.lower(documents);
// Process the stream - e.g., collect all action references
List<String> 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> 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<?, ExecutionContext> 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<U extends Trait<?>>
extends SimpleTraitMatcher<U> {
// 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<ActionStep> {
@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<T extends Tree>` interface with `getCursor()` method
- Nest a `Matcher` as a static inner class extending `SimpleTraitMatcher<T>`
- 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

View File

@@ -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<Document> getDocuments();
Documents withDocuments(List<Document> documents);
}
```
**Usage:**
```java
@Override
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) {
// Process all documents in the file
List<Yaml.Document> 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<Entry> getEntries();
Mapping withEntries(List<Entry> 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<Entry> getEntries();
Sequence withEntries(List<Entry> 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<Yaml.Sequence.Entry> 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<Yaml.Mapping> 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<Yaml.Sequence.Entry> 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