Files
2025-11-30 08:57:41 +08:00

19 KiB

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.

public interface Documents extends Yaml {
    List<Document> getDocuments();
    Documents withDocuments(List<Document> documents);
}

Usage:

@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 ---).

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:

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

public interface Mapping extends Block {
    List<Entry> getEntries();
    Mapping withEntries(List<Entry> entries);
    String getAnchor();  // YAML anchor if present
}

Example YAML:

name: my-workflow
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest

Navigation:

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

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:

@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).

public interface Sequence extends Block {
    List<Entry> getEntries();
    Sequence withEntries(List<Entry> entries);
    String getAnchor();
}

Example YAML:

branches:
  - main
  - develop
  - feature/*

# Or inline style:
branches: [main, develop, feature/*]

Navigation:

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

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

public interface Entry extends Yaml {
    Block getBlock();  // The actual value
    Entry withBlock(Block block);
    boolean isTrailingCommaPrefix();
}

Usage:

@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).

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:

plain: value
single: 'value'
double: "value"
number: 42
boolean: true
null_value: null
multiline: |
  Line 1
  Line 2

Usage:

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

public interface Key extends Yaml {
    String getValue();
    Key withValue(String value);
}

Usage:

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

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

@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

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

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

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

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

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

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

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

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

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

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

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

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