Initial commit
This commit is contained in:
198
skills/recipe-writer/references/checklist-recipe-development.md
Normal file
198
skills/recipe-writer/references/checklist-recipe-development.md
Normal 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
|
||||
356
skills/recipe-writer/references/common-patterns.md
Normal file
356
skills/recipe-writer/references/common-patterns.md
Normal 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);
|
||||
```
|
||||
420
skills/recipe-writer/references/java-lst-reference.md
Normal file
420
skills/recipe-writer/references/java-lst-reference.md
Normal 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
|
||||
524
skills/recipe-writer/references/jsonpath-patterns.md
Normal file
524
skills/recipe-writer/references/jsonpath-patterns.md
Normal 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/
|
||||
328
skills/recipe-writer/references/trait-implementation-guide.md
Normal file
328
skills/recipe-writer/references/trait-implementation-guide.md
Normal 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
|
||||
736
skills/recipe-writer/references/yaml-lst-reference.md
Normal file
736
skills/recipe-writer/references/yaml-lst-reference.md
Normal 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
|
||||
Reference in New Issue
Block a user