Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.NameCaseConvention;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.yaml.trait.BlockScalar;
import org.openrewrite.yaml.tree.Yaml;

import java.util.Iterator;
Expand Down Expand Up @@ -96,7 +97,7 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
String prop = getProperty(getCursor());
if (keyMatcher.matchesGlob(prop) && matchesOldValue(e.getValue())) {
Yaml.Block updatedValue = updateValue(e.getValue());
Yaml.Block updatedValue = updateValue(e.getValue(), getCursor());
if (updatedValue != null) {
e = e.withValue(updatedValue);
}
Expand All @@ -107,19 +108,24 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
}

// returns null if value should not change
private Yaml.@Nullable Block updateValue(Yaml.Block value) {
private Yaml.@Nullable Block updateValue(Yaml.Block value, Cursor parent) {
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
Yaml.Scalar newScalar = scalar.withValue(Boolean.TRUE.equals(regex) ?
scalar.getValue().replaceAll(Objects.requireNonNull(oldValue), newValue) :
newValue);
return scalar.getValue().equals(newScalar.getValue()) ? null : newScalar;
BlockScalar block = new BlockScalar.Matcher().get(scalar, parent).orElse(null);
String body = block != null ? block.getBody() : scalar.getValue();
String updatedBody = Boolean.TRUE.equals(regex) ?
body.replaceAll(Objects.requireNonNull(oldValue), newValue) :
newValue;
if (body.equals(updatedBody)) {
return null;
}
return block != null ? block.withBody(updatedBody) : scalar.withValue(updatedBody);
}
if (value instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) value;
return sequence.withEntries(ListUtils.map(sequence.getEntries(), entry -> {
if (matchesOldValue(entry.getBlock())) {
Yaml.Block updatedValue = updateValue(entry.getBlock());
Yaml.Block updatedValue = updateValue(entry.getBlock(), parent);
if (updatedValue != null) {
return entry.withBlock(updatedValue);
}
Expand Down
20 changes: 18 additions & 2 deletions rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangeValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.marker.Markers;
import org.openrewrite.yaml.trait.BlockScalar;
import org.openrewrite.yaml.tree.Yaml;

import static org.openrewrite.Tree.randomId;
Expand Down Expand Up @@ -66,7 +67,15 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
if (matcher.matches(getCursor()) && (!(e.getValue() instanceof Yaml.Scalar) || !((Yaml.Scalar) e.getValue()).getValue().equals(value))) {
if (!matcher.matches(getCursor())) {
return e;
}
BlockScalar block = new BlockScalar.Matcher().get(new Cursor(getCursor(), e.getValue())).orElse(null);
if (block != null) {
if (!block.getBody().equals(value)) {
e = e.withValue(block.withBody(value));
}
} else if (!(e.getValue() instanceof Yaml.Scalar) || !((Yaml.Scalar) e.getValue()).getValue().equals(value)) {
Yaml.Anchor anchor = (e.getValue() instanceof Yaml.Scalar) ? ((Yaml.Scalar) e.getValue()).getAnchor() : null;
Yaml.Tag tag = (e.getValue() instanceof Yaml.Scalar) ? ((Yaml.Scalar) e.getValue()).getTag() : null;
String prefix = e.getValue() instanceof Yaml.Sequence ? ((Yaml.Sequence) e.getValue()).getOpeningBracketPrefix() : e.getValue().getPrefix();
Expand All @@ -82,7 +91,14 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
Yaml.Scalar s = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
s = s.withValue(value);
BlockScalar block = new BlockScalar.Matcher().get(getCursor()).orElse(null);
if (block != null) {
if (!block.getBody().equals(value)) {
s = block.withBody(value);
}
} else if (!s.getValue().equals(value)) {
s = s.withValue(value);
}
}
return s;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
entry = entry.withPrefix(firstDeletedPrefix);
} else if (previousWasDeleted && !entries.isEmpty() && !containsNewline(entry.getPrefix())) {
entry = entry.withPrefix("\n" + entry.getPrefix());
} else if (previousWasDeleted && !entries.isEmpty()
&& endsWithBlockScalar(entries.get(entries.size() - 1))
&& containsNewline(entry.getPrefix())) {
// Block-scalar predecessor already owns the boundary newline; strip the duplicate.
entry = entry.withPrefix(stripLeadingLineBreak(entry.getPrefix()));
}
entries.add(entry);
previousWasDeleted = false;
Expand Down Expand Up @@ -257,6 +262,32 @@ private static boolean containsOnlyWhitespace(@Nullable String str) {
return true;
}

private static String stripLeadingLineBreak(String s) {
if (s.startsWith("\r\n")) {
return s.substring(2);
}
if (s.startsWith("\n") || s.startsWith("\r")) {
return s.substring(1);
}
// Line break not at the start — strip the first one found.
int idx = -1;
int idxN = s.indexOf('\n');
int idxR = s.indexOf('\r');
if (idxN >= 0 && (idxR < 0 || idxN < idxR)) {
idx = idxN;
} else if (idxR >= 0) {
idx = idxR;
}
if (idx < 0) {
return s;
}
int after = idx + 1;
if (s.charAt(idx) == '\r' && after < s.length() && s.charAt(after) == '\n') {
after++;
}
return s.substring(0, idx) + s.substring(after);
}

private static boolean containsNewline(@Nullable String str) {
return str != null && str.indexOf('\n') >= 0;
}
Expand Down
123 changes: 123 additions & 0 deletions rewrite-yaml/src/main/java/org/openrewrite/yaml/trait/BlockScalar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.yaml.trait;

import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.Cursor;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.trait.SimpleTraitMatcher;
import org.openrewrite.trait.Trait;
import org.openrewrite.yaml.tree.Yaml;

/**
* Body-aware access to a FOLDED / LITERAL {@link Yaml.Scalar}: the raw {@link Yaml.Scalar#value}
* carries the block envelope (chomp indicator, header newline, indented body, trailing whitespace
* bounding the next sibling), so rewriting it directly via the Lombok-generated {@code withValue}
* clobbers the envelope.
*/
@Value
public class BlockScalar implements Trait<Yaml.Scalar> {
Cursor cursor;

public String getBody() {
String value = getTree().getValue();
int headerEnd = value.indexOf('\n');
if (headerEnd < 0) {
return "";
}
int bodyEnd = value.length();
while (bodyEnd > headerEnd + 1 && Character.isWhitespace(value.charAt(bodyEnd - 1))) {
bodyEnd--;
}
if (bodyEnd <= headerEnd + 1) {
return "";
}
String bodyRegion = value.substring(headerEnd + 1, bodyEnd);
int indent = 0;
while (indent < bodyRegion.length() && bodyRegion.charAt(indent) == ' ') {
indent++;
}
String indentStr = bodyRegion.substring(0, indent);
String[] lines = bodyRegion.split("\r\n|\r|\n", -1);
StringBuilder out = new StringBuilder(bodyRegion.length());
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (indent > 0 && line.startsWith(indentStr)) {
line = line.substring(indent);
}
if (i > 0) {
out.append('\n');
}
out.append(line);
}
return out.toString();
}

public Yaml.Scalar withBody(String newBody) {
return withBody(newBody, 2);
}

public Yaml.Scalar withBody(String newBody, int defaultIndentSpaces) {
Yaml.Scalar scalar = getTree();
String value = scalar.getValue();
int headerEnd = value.indexOf('\n');
String header = headerEnd < 0 ? value : value.substring(0, headerEnd + 1);
String newLine = (headerEnd > 0 && value.charAt(headerEnd - 1) == '\r') ? "\r\n" : "\n";
int bodyEnd = value.length();
while (bodyEnd > 0 && Character.isWhitespace(value.charAt(bodyEnd - 1))) {
bodyEnd--;
}
String indent;
if (headerEnd >= 0 && headerEnd + 1 < bodyEnd) {
int indentEnd = headerEnd + 1;
while (indentEnd < bodyEnd && value.charAt(indentEnd) == ' ') {
indentEnd++;
}
indent = value.substring(headerEnd + 1, indentEnd);
if (indent.isEmpty()) {
indent = StringUtils.repeat(" ", defaultIndentSpaces);
}
} else {
indent = StringUtils.repeat(" ", defaultIndentSpaces);
}
String trailing = value.substring(bodyEnd);
String[] lines = newBody.split("\r\n|\r|\n", -1);
StringBuilder body = new StringBuilder();
for (int i = 0; i < lines.length; i++) {
if (i > 0) {
body.append(newLine);
}
if (!lines[i].isEmpty()) {
body.append(indent).append(lines[i]);
}
}
return scalar.withValue(header + body + trailing);
}

public static class Matcher extends SimpleTraitMatcher<BlockScalar> {
@Override
protected @Nullable BlockScalar test(Cursor cursor) {
if (cursor.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar.Style style = ((Yaml.Scalar) cursor.getValue()).getStyle();
if (style == Yaml.Scalar.Style.FOLDED || style == Yaml.Scalar.Style.LITERAL) {
return new BlockScalar(cursor);
}
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public boolean supportsRename() {
public Tree rename(Renamer renamer, Cursor cursor, ExecutionContext ctx) {
Tree tree = cursor.getValue();
if (tree instanceof Yaml.Scalar) {
return ((Yaml.Scalar) tree).withValue(renamer.rename(this));
Yaml.Scalar scalar = (Yaml.Scalar) tree;
String newName = renamer.rename(this);
return new BlockScalar.Matcher()
.get(cursor)
.map(b -> (Yaml.Scalar) b.withBody(newName))
.orElseGet(() -> scalar.withValue(newName));
}
throw new IllegalArgumentException("cursor.getValue() must be an Yaml.Scalar but is: " + tree.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ public Yaml.Scalar getValueAsScalar() {
}

public YamlValue withValue(String newValue) {
Yaml.Scalar value = getValueAsScalar().withValue(newValue);
cursor = new Cursor(cursor.getParent(), getTree().withValue(value));
Yaml.Scalar scalar = getValueAsScalar();
Yaml.Scalar updated = new BlockScalar.Matcher()
.get(scalar, cursor)
.map(b -> b.withBody(newValue))
.orElseGet(() -> scalar.withValue(newValue));
cursor = new Cursor(cursor.getParent(), getTree().withValue(updated));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@ class Scalar implements Block, YamlKey {
@Nullable
Tag tag;

/**
Comment thread
steve-aom-elliott marked this conversation as resolved.
* For FOLDED/LITERAL scalars this includes the chomp indicator, header newline,
* indented body, and trailing whitespace bounding the next sibling; the Lombok-generated
* {@code withValue} cannot safely rewrite a block scalar's body. Use the
* {@code org.openrewrite.yaml.trait.BlockScalar} trait to mutate the body without
* clobbering the block envelope.
*/
String value;

public enum Style {
Expand Down
Loading
Loading