# YAML LST Structure Reference Complete reference for OpenRewrite's YAML Lossless Semantic Tree (LST) structure. ## Overview The YAML LST represents YAML documents as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance. ## Type Hierarchy ``` org.openrewrite.yaml.tree.Yaml ├── Yaml.Documents (root container) ├── Yaml.Document (single document in multi-doc file) ├── Yaml.Mapping (key-value pairs, similar to JSON object) │ └── Yaml.Mapping.Entry (single key-value pair) ├── Yaml.Sequence (arrays/lists) │ └── Yaml.Sequence.Entry (single array item) ├── Yaml.Scalar (primitive values: strings, numbers, booleans) │ └── Yaml.Scalar.Key (key in a key-value pair) └── Yaml.Anchor (YAML anchors and aliases) ``` ## Core Types ### Yaml.Documents The root element containing one or more YAML documents. ```java public interface Documents extends Yaml { List getDocuments(); Documents withDocuments(List documents); } ``` **Usage:** ```java @Override public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { // Process all documents in the file List modified = ListUtils.map( documents.getDocuments(), doc -> (Yaml.Document) visit(doc, ctx) ); return documents.withDocuments(modified); } ``` --- ### Yaml.Document A single YAML document (files can contain multiple documents separated by `---`). ```java public interface Document extends Yaml { Block getBlock(); // Root block (usually Mapping or Sequence) Document withBlock(Block block); boolean isExplicit(); // True if document starts with --- } ``` **Usage:** ```java @Override public Yaml.Document visitDocument(Yaml.Document document, ExecutionContext ctx) { if (document.getBlock() instanceof Yaml.Mapping) { Yaml.Mapping root = (Yaml.Mapping) document.getBlock(); // Process root mapping Yaml.Mapping modified = (Yaml.Mapping) visit(root, ctx); if (modified != root) { return document.withBlock(modified); } } return super.visitDocument(document, ctx); } ``` --- ### Yaml.Mapping Represents a YAML mapping (key-value pairs), equivalent to JSON objects or Python dictionaries. ```java public interface Mapping extends Block { List getEntries(); Mapping withEntries(List entries); String getAnchor(); // YAML anchor if present } ``` **Example YAML:** ```yaml name: my-workflow on: push: branches: [main] jobs: build: runs-on: ubuntu-latest ``` **Navigation:** ```java @Override public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) { for (Yaml.Mapping.Entry entry : mapping.getEntries()) { String key = entry.getKey().getValue(); Block value = entry.getValue(); if ("jobs".equals(key) && value instanceof Yaml.Mapping) { Yaml.Mapping jobsMapping = (Yaml.Mapping) value; // Process each job for (Yaml.Mapping.Entry jobEntry : jobsMapping.getEntries()) { String jobName = jobEntry.getKey().getValue(); // Process job... } } } return super.visitMapping(mapping, ctx); } ``` --- ### Yaml.Mapping.Entry A single key-value pair within a mapping. ```java public interface Entry extends Yaml { Yaml.Scalar.Key getKey(); Block getValue(); // Can be Scalar, Mapping, or Sequence Entry withKey(Yaml.Scalar.Key key); Entry withValue(Block value); } ``` **Key Methods:** - `getKey().getValue()` - Get the key as a string (always safe, no cast needed) - `getValue()` - Get the value (requires type check and cast) - `withKey()` - Create new entry with different key - `withValue()` - Create new entry with different value **Usage:** ```java @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { String keyName = entry.getKey().getValue(); // Safe access // Check value type before processing if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); String value = scalar.getValue(); // Modify value if ("old-value".equals(value)) { return entry.withValue(scalar.withValue("new-value")); } } else if (entry.getValue() instanceof Yaml.Mapping) { Yaml.Mapping nested = (Yaml.Mapping) entry.getValue(); // Process nested mapping } else if (entry.getValue() instanceof Yaml.Sequence) { Yaml.Sequence sequence = (Yaml.Sequence) entry.getValue(); // Process sequence } return super.visitMappingEntry(entry, ctx); } ``` --- ### Yaml.Sequence Represents a YAML sequence (array/list). ```java public interface Sequence extends Block { List getEntries(); Sequence withEntries(List entries); String getAnchor(); } ``` **Example YAML:** ```yaml branches: - main - develop - feature/* # Or inline style: branches: [main, develop, feature/*] ``` **Navigation:** ```java @Override public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) { List entries = sequence.getEntries(); // Iterate through sequence items for (Yaml.Sequence.Entry entry : entries) { if (entry.getBlock() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); String value = scalar.getValue(); // Process value... } else if (entry.getBlock() instanceof Yaml.Mapping) { Yaml.Mapping mapping = (Yaml.Mapping) entry.getBlock(); // Process mapping item... } } return super.visitSequence(sequence, ctx); } ``` **Adding Items:** ```java @Override public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) { // Check if 'main' branch exists boolean hasMain = false; for (Yaml.Sequence.Entry entry : sequence.getEntries()) { if (entry.getBlock() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); if ("main".equals(scalar.getValue())) { hasMain = true; break; } } } if (!hasMain) { // Create new scalar for 'main' Yaml.Scalar mainScalar = new Yaml.Scalar( Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, "main" ); // Wrap in sequence entry Yaml.Sequence.Entry mainEntry = new Yaml.Sequence.Entry( Tree.randomId(), Space.format("\n - "), // Proper indentation Markers.EMPTY, mainScalar, false ); // Add to sequence return sequence.withEntries( ListUtils.concat(sequence.getEntries(), mainEntry) ); } return super.visitSequence(sequence, ctx); } ``` --- ### Yaml.Sequence.Entry A single item in a sequence. ```java public interface Entry extends Yaml { Block getBlock(); // The actual value Entry withBlock(Block block); boolean isTrailingCommaPrefix(); } ``` **Usage:** ```java @Override public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) { if (entry.getBlock() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock(); String value = scalar.getValue(); // Update version references if (value.contains("@v2")) { String updated = value.replace("@v2", "@v3"); return entry.withBlock(scalar.withValue(updated)); } } return super.visitSequenceEntry(entry, ctx); } ``` --- ### Yaml.Scalar Represents primitive values (strings, numbers, booleans, null). ```java public interface Scalar extends Block { Style getStyle(); // PLAIN, SINGLE_QUOTED, DOUBLE_QUOTED, etc. String getAnchor(); String getValue(); Scalar withValue(String value); Scalar withStyle(Style style); enum Style { PLAIN, SINGLE_QUOTED, DOUBLE_QUOTED, LITERAL, FOLDED } } ``` **Example YAML:** ```yaml plain: value single: 'value' double: "value" number: 42 boolean: true null_value: null multiline: | Line 1 Line 2 ``` **Usage:** ```java // Reading scalar values if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); String value = scalar.getValue(); Yaml.Scalar.Style style = scalar.getStyle(); // Modify value while preserving style Yaml.Scalar updated = scalar.withValue("new-value"); return entry.withValue(updated); } // Creating new scalars Yaml.Scalar newScalar = new Yaml.Scalar( Tree.randomId(), // Unique ID Space.EMPTY, // Prefix whitespace Markers.EMPTY, // Markers for search results, etc. Yaml.Scalar.Style.PLAIN, // Quoting style null, // Anchor "value" // Actual value ); ``` --- ### Yaml.Scalar.Key Special scalar type used for keys in mappings. ```java public interface Key extends Yaml { String getValue(); Key withValue(String value); } ``` **Usage:** ```java // Keys are always accessible via entry.getKey() String keyName = entry.getKey().getValue(); // No casting needed! // Rename a key if ("old-key".equals(entry.getKey().getValue())) { Yaml.Scalar.Key newKey = entry.getKey().withValue("new-key"); return entry.withKey(newKey); } ``` --- ## Navigation Patterns ### Cursor Navigation The `Cursor` provides context about the current position in the tree. ```java // Get parent elements Cursor parent = getCursor().getParent(); Cursor grandparent = getCursor().getParent(2); // Check parent type if (parent != null && parent.getValue() instanceof Yaml.Mapping) { Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue(); // Process parent... } // Get all ancestors of a type Iterator mappings = getCursor().getPathAsStream() .filter(p -> p instanceof Yaml.Mapping) .map(p -> (Yaml.Mapping) p) .iterator(); ``` ### Finding Siblings ```java @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { // Get parent mapping to access siblings Cursor parent = getCursor().getParent(); if (parent != null && parent.getValue() instanceof Yaml.Mapping) { Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue(); // Find sibling entries for (Yaml.Mapping.Entry sibling : parentMapping.getEntries()) { if (sibling != entry) { String siblingKey = sibling.getKey().getValue(); // Check sibling... } } } return super.visitMappingEntry(entry, ctx); } ``` ### Path-Based Navigation with JsonPath ```java // Match specific paths in YAML structure JsonPathMatcher jobMatcher = new JsonPathMatcher("$.jobs.*"); JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.*.steps[*]"); JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { // This is a 'uses' field within a step // Process it... } return super.visitMappingEntry(entry, ctx); } ``` --- ## Type Checking and Casting ### Safe Type Checking Pattern ```java Block value = entry.getValue(); if (value instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) value; String stringValue = scalar.getValue(); // Process scalar... } else if (value instanceof Yaml.Mapping) { Yaml.Mapping mapping = (Yaml.Mapping) value; // Process mapping... } else if (value instanceof Yaml.Sequence) { Yaml.Sequence sequence = (Yaml.Sequence) value; // Process sequence... } else { // Handle other types or null } ``` ### Null Safety ```java // Keys never need null checking (always present) String key = entry.getKey().getValue(); // Safe // Values might be null or unexpected types Block value = entry.getValue(); if (value == null) { // Handle null value (represents 'key:' with no value) } // Scalar values can be null string if (value instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) value; String stringValue = scalar.getValue(); if (stringValue == null || "null".equals(stringValue)) { // Handle YAML null } } ``` --- ## Creating New Elements ### Creating Scalars ```java // Plain scalar Yaml.Scalar plain = new Yaml.Scalar( Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, "value" ); // Quoted scalar Yaml.Scalar quoted = new Yaml.Scalar( Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.DOUBLE_QUOTED, null, "value with spaces" ); ``` ### Creating Mapping Entries ```java // Create key Yaml.Scalar.Key key = new Yaml.Scalar.Key( Tree.randomId(), Space.EMPTY, Markers.EMPTY, "key-name" ); // Create value Yaml.Scalar value = new Yaml.Scalar( Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, "value" ); // Create entry Yaml.Mapping.Entry newEntry = new Yaml.Mapping.Entry( Tree.randomId(), Space.format("\n "), // Indentation Markers.EMPTY, key, Space.format(" "), // Space after colon value ); ``` ### Creating Sequences ```java // Create sequence items List entries = new ArrayList<>(); entries.add(new Yaml.Sequence.Entry( Tree.randomId(), Space.format("\n - "), Markers.EMPTY, new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, "item1"), false )); entries.add(new Yaml.Sequence.Entry( Tree.randomId(), Space.format("\n - "), Markers.EMPTY, new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Yaml.Scalar.Style.PLAIN, null, "item2"), false )); // Create sequence Yaml.Sequence sequence = new Yaml.Sequence( Tree.randomId(), Space.EMPTY, Markers.EMPTY, null, // anchor entries ); ``` --- ## Common Pitfalls ### 1. Not Calling Super Methods ```java // ❌ WRONG - tree traversal stops @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { if ("target".equals(entry.getKey().getValue())) { return entry.withValue(newValue); } return entry; // ❌ Should call super } // ✅ CORRECT @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { if ("target".equals(entry.getKey().getValue())) { return entry.withValue(newValue); } return super.visitMappingEntry(entry, ctx); // ✅ } ``` ### 2. Mutating Instead of Creating New Objects ```java // ❌ WRONG - LST is immutable Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); scalar.setValue("new-value"); // ❌ This doesn't exist // ✅ CORRECT Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); Yaml.Scalar updated = scalar.withValue("new-value"); return entry.withValue(updated); ``` ### 3. Forgetting Type Checks ```java // ❌ WRONG - may throw ClassCastException Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); // ✅ CORRECT if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); // Process safely } ``` ### 4. Incorrect Whitespace/Indentation ```java // ❌ WRONG - no indentation Space.EMPTY // Results in key:value on same line as parent // ✅ CORRECT - proper YAML indentation Space.format("\n ") // Newline + 2-space indent ``` ### 5. Not Returning Original When Unchanged ```java // ❌ WRONG - creates unnecessary tree copies return entry.withValue(entry.getValue()); // ✅ CORRECT - return original if unchanged if (shouldModify) { return entry.withValue(newValue); } return super.visitMappingEntry(entry, ctx); // Returns original ``` --- ## Complete Example: Multi-Level Navigation ```java /** * Find all GitHub Actions steps using deprecated actions * and update them to newer versions */ @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { // Match: $.jobs.*.steps[*].uses JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses"); if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) { if (entry.getValue() instanceof Yaml.Scalar) { Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue(); String actionRef = scalar.getValue(); // Navigate to parent step to check conditions Cursor stepCursor = getCursor().getParent(2); if (stepCursor != null && stepCursor.getValue() instanceof Yaml.Mapping) { Yaml.Mapping step = (Yaml.Mapping) stepCursor.getValue(); // Check if step has 'if' condition boolean hasCondition = false; for (Yaml.Mapping.Entry stepEntry : step.getEntries()) { if ("if".equals(stepEntry.getKey().getValue())) { hasCondition = true; break; } } // Only update unconditional steps if (!hasCondition && actionRef.contains("@v2")) { String updated = actionRef.replace("@v2", "@v3"); return entry.withValue(scalar.withValue(updated)); } } } } return super.visitMappingEntry(entry, ctx); } ``` --- ## Reference Chart | LST Type | Represents | Common Methods | Notes | |----------|-----------|----------------|-------| | `Documents` | File root | `getDocuments()` | Container for multiple docs | | `Document` | Single doc | `getBlock()` | May have `---` separator | | `Mapping` | Key-value pairs | `getEntries()` | Like JSON object | | `Mapping.Entry` | One key-value | `getKey()`, `getValue()` | Basic building block | | `Sequence` | Array/list | `getEntries()` | Ordered collection | | `Sequence.Entry` | Array item | `getBlock()` | Wraps actual value | | `Scalar` | Primitive value | `getValue()`, `getStyle()` | String, number, bool, null | | `Scalar.Key` | Mapping key | `getValue()` | Always string, no cast needed | ## Additional Resources - OpenRewrite YAML LST JavaDoc: https://docs.openrewrite.org/reference/yaml-lossless-semantic-trees - YAML Specification: https://yaml.org/spec/1.2/spec.html - OpenRewrite Visitor Pattern: https://docs.openrewrite.org/concepts-and-explanations/visitors