From 464c76cb9ac6aad0a807bcc69978173127d41d51 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Tue, 30 Jun 2026 15:24:45 -0400 Subject: [PATCH] YAML: fix MergeYaml + skip CoalesceProperties on block scalars MergeYaml.mergeScalar: when merging into an existing FOLDED/LITERAL block scalar, route the new body through BlockScalarUtils so the block envelope (chomp indicator, header newline, body indent, trailing whitespace) is preserved rather than clobbered. Previously the new value was assigned via Yaml.Scalar.withValue and corrupted siblings. BlockScalarUtils.withBody: drop the trailing-newline fallback. The previous fallback added a '\n' when the existing scalar's value had no trailing whitespace, which was wrong for scalars produced by autoFormat (in which the boundary newline lives in the next entry's prefix). Preserving the existing trailing whitespace exactly fixes a latent extra-blank-line bug visible through MergeYaml's addLiteralStyleBlockWhichDoesAlreadyExist path. CoalescePropertiesVisitor: skip the dot-joined key coalesce when the sub-mapping (transitively) contains a block scalar. The downstream ShiftFormatLeftVisitor cannot dedent block-scalar body indent (stored inside Yaml.Scalar.value) nor entries whose prefix has no '\n' because the boundary newline lives in a preceding block scalar's value. Conservatively leaving those mappings un-coalesced keeps the file structurally intact. --- .../yaml/CoalescePropertiesVisitor.java | 25 +++++++++- .../openrewrite/yaml/MergeYamlVisitor.java | 7 +-- .../yaml/CoalescePropertiesTest.java | 48 +++++++++++++++++++ .../org/openrewrite/yaml/MergeYamlTest.java | 41 ++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/CoalescePropertiesVisitor.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/CoalescePropertiesVisitor.java index f5983ee63b2..7edbe6557ea 100644 --- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/CoalescePropertiesVisitor.java +++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/CoalescePropertiesVisitor.java @@ -62,7 +62,8 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, P p) { Yaml.Mapping valueMapping = (Yaml.Mapping) entry.getValue(); if (valueMapping.getEntries().size() == 1) { Yaml.Mapping.Entry subEntry = valueMapping.getEntries().get(0); - if (!subEntry.getPrefix().contains("#") && !isExcluded(entry, subEntry) && isApplied(entry)) { + if (!subEntry.getPrefix().contains("#") && !isExcluded(entry, subEntry) && isApplied(entry) + && !containsBlockScalar(subEntry.getValue())) { int indentToUse = findIndent.getMostCommonIndent() > 0 ? findIndent.getMostCommonIndent() : 4; doAfterVisit(new ShiftFormatLeftVisitor<>(subEntry.getValue(), indentToUse)); @@ -75,6 +76,28 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, P p) { })); } + private static boolean containsBlockScalar(Yaml.Block block) { + if (block instanceof Yaml.Scalar) { + Yaml.Scalar.Style style = ((Yaml.Scalar) block).getStyle(); + return style == Yaml.Scalar.Style.FOLDED || style == Yaml.Scalar.Style.LITERAL; + } + if (block instanceof Yaml.Mapping) { + for (Yaml.Mapping.Entry e : ((Yaml.Mapping) block).getEntries()) { + if (containsBlockScalar(e.getValue())) { + return true; + } + } + } + if (block instanceof Yaml.Sequence) { + for (Yaml.Sequence.Entry e : ((Yaml.Sequence) block).getEntries()) { + if (containsBlockScalar(e.getBlock())) { + return true; + } + } + } + return false; + } + private boolean isExcluded(Yaml.Mapping.Entry entry, Yaml.Mapping.Entry subEntry) { if (exclusionMatchers.isEmpty()) { return false; diff --git a/rewrite-yaml/src/main/java/org/openrewrite/yaml/MergeYamlVisitor.java b/rewrite-yaml/src/main/java/org/openrewrite/yaml/MergeYamlVisitor.java index a938e6b2356..2cf6897b2fa 100644 --- a/rewrite-yaml/src/main/java/org/openrewrite/yaml/MergeYamlVisitor.java +++ b/rewrite-yaml/src/main/java/org/openrewrite/yaml/MergeYamlVisitor.java @@ -23,6 +23,7 @@ import org.openrewrite.style.GeneralFormatStyle; import org.openrewrite.style.Style; import org.openrewrite.yaml.MergeYaml.InsertMode; +import org.openrewrite.yaml.internal.BlockScalarUtils; import org.openrewrite.yaml.tree.Yaml; import java.util.ArrayList; @@ -380,9 +381,9 @@ private Yaml.Sequence mergeSequence(Yaml.Sequence s1, Yaml.Sequence s2, P p, Cur } private Yaml.Scalar mergeScalar(Yaml.Scalar y1, Yaml.Scalar y2) { - String s1 = y1.getValue(); - String s2 = y2.getValue(); - return !s1.equals(s2) && !acceptTheirs ? y1.withValue(s2) : y1; + String s1 = BlockScalarUtils.getBody(y1); + String s2 = BlockScalarUtils.getBody(y2); + return !s1.equals(s2) && !acceptTheirs ? BlockScalarUtils.withBody(y1, s2) : y1; } /** diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/CoalescePropertiesTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/CoalescePropertiesTest.java index 629d6828035..dfa1aa06a14 100644 --- a/rewrite-yaml/src/test/java/org/openrewrite/yaml/CoalescePropertiesTest.java +++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/CoalescePropertiesTest.java @@ -376,4 +376,52 @@ void applyToWithExclusion() { ) ); } + + @Test + void skipsCoalesceWhenInnerMappingContainsBlockScalar() { + rewriteRun( + spec -> spec.recipe(new CoalesceProperties(null, null)), + yaml( + """ + outer: + inner: + java_opts: >- + -Da=1 + -Db=2 + after: tail + """ + ) + ); + } + + @Test + void skipsCoalesceWhenInnerValueIsBlockScalar() { + rewriteRun( + spec -> spec.recipe(new CoalesceProperties(null, null)), + yaml( + """ + outer: + inner: | + line one + line two + """ + ) + ); + } + + @Test + void skipsCoalesceWhenBlockScalarNestedDeeperInSubEntry() { + rewriteRun( + spec -> spec.recipe(new CoalesceProperties(null, null)), + yaml( + """ + outer: + inner: + details: + script: | + echo hello + """ + ) + ); + } } diff --git a/rewrite-yaml/src/test/java/org/openrewrite/yaml/MergeYamlTest.java b/rewrite-yaml/src/test/java/org/openrewrite/yaml/MergeYamlTest.java index 527492eb5eb..f31c047a093 100644 --- a/rewrite-yaml/src/test/java/org/openrewrite/yaml/MergeYamlTest.java +++ b/rewrite-yaml/src/test/java/org/openrewrite/yaml/MergeYamlTest.java @@ -3752,4 +3752,45 @@ void mergeYamlPreservesInlineCommentOnDocumentEnd() { ); } + @Test + void overwriteFoldedStripBlockScalarPreservesEnvelope() { + rewriteRun( + spec -> spec.recipe(new MergeYaml("$", "key: replaced\n", false, null, null, null, null, null)), + yaml( + """ + key: >- + line one + line two + after: tail + """, + """ + key: >- + replaced + after: tail + """ + ) + ); + } + + @Test + void overwriteLiteralKeepBlockScalarPreservesEnvelope() { + rewriteRun( + spec -> spec.recipe(new MergeYaml("$", "key: replaced\n", false, null, null, null, null, null)), + yaml( + """ + key: |+ + line one + line two + + after: tail + """, + """ + key: |+ + replaced + + after: tail + """ + ) + ); + } }