Skip to content

Commit

Permalink
Make multiline scalars work
Browse files Browse the repository at this point in the history
  • Loading branch information
jevanlingen committed Nov 22, 2024
1 parent 7598df6 commit bdc90bf
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.regex.Pattern;

public class StringUtils {
private static final Pattern LINE_BREAK = Pattern.compile("\\R");

private StringUtils() {
}

Expand Down Expand Up @@ -158,50 +158,6 @@ private static int minCommonIndentLevel(String text) {
return minIndent;
}

public static int mostCommonIndent(SortedMap<Integer, Long> indentFrequencies) {
// the frequency with which each indent level is an integral divisor of longer indent levels
SortedMap<Integer, Integer> indentFrequencyAsDivisors = new TreeMap<>();
for (Map.Entry<Integer, Long> indentFrequency : indentFrequencies.entrySet()) {
int indent = indentFrequency.getKey();
int freq;
switch (indent) {
case 0:
freq = indentFrequency.getValue().intValue();
break;
case 1:
// gcd(1, N) == 1, so we can avoid the test for this case
freq = (int) indentFrequencies.tailMap(indent).values().stream().mapToLong(l -> l).sum();
break;
default:
freq = (int) indentFrequencies.tailMap(indent).entrySet().stream()
.filter(inF -> gcd(inF.getKey(), indent) != 0)
.mapToLong(Map.Entry::getValue)
.sum();
}

indentFrequencyAsDivisors.put(indent, freq);
}

if (indentFrequencies.getOrDefault(0, 0L) > 1) {
return 0;
}

return indentFrequencyAsDivisors.entrySet().stream()
.max((e1, e2) -> {
int valCompare = e1.getValue().compareTo(e2.getValue());
return valCompare != 0 ?
valCompare :
// take the smallest indent otherwise, unless it would be zero
e1.getKey() == 0 ? -1 : e2.getKey().compareTo(e1.getKey());
})
.map(Map.Entry::getKey)
.orElse(0);
}

static int gcd(int n1, int n2) {
return n2 == 0 ? n1 : gcd(n2, n1 % n2);
}

/**
* Check if the String is null or has only whitespaces.
* <p>
Expand Down Expand Up @@ -271,7 +227,7 @@ public static String capitalize(String value) {
return value;
}
return Character.toUpperCase(value.charAt(0)) +
value.substring(1);
value.substring(1);
}

public static String uncapitalize(String value) {
Expand Down Expand Up @@ -467,7 +423,7 @@ private static boolean matchesGlob(String pattern, String str, boolean caseSensi
break;
}
if (ch != '?' &&
different(caseSensitive, ch, str.charAt(strIdxStart))) {
different(caseSensitive, ch, str.charAt(strIdxStart))) {
return false; // Character mismatch
}
patIdxStart++;
Expand Down Expand Up @@ -760,4 +716,8 @@ public static int indexOfNextNonWhitespace(int cursor, String source) {
public static String formatUriForPropertiesFile(String uri) {
return uri.replaceAll("(?<!\\\\)://", "\\\\://");
}

public static boolean hasLineBreak(@Nullable String s) {
return s != null && LINE_BREAK.matcher(s).find();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import java.util.List;

public class CoalescePropertiesVisitor<P> extends YamlIsoVisitor<P> {
private final FindIndentYamlVisitor<P> findIndent = new FindIndentYamlVisitor<>(0);
private final FindIndentYamlVisitor<P> findIndent = new FindIndentYamlVisitor<>();

public CoalescePropertiesVisitor() {
}
Expand Down Expand Up @@ -55,8 +55,7 @@ public Yaml.Mapping visitMapping(Yaml.Mapping mapping, P p) {
entries.add(entry.withKey(coalescedKey)
.withValue(subEntry.getValue()));

int indentToUse = findIndent.getMostCommonIndent() > 0 ?
findIndent.getMostCommonIndent() : 4;
int indentToUse = findIndent.getMostCommonIndent() > 0 ? findIndent.getMostCommonIndent() : 4;
doAfterVisit(new ShiftFormatLeftVisitor<>(subEntry.getValue(), indentToUse));

changed = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.openrewrite.yaml.tree.Yaml;
import org.openrewrite.yaml.tree.Yaml.Document;
import org.openrewrite.yaml.tree.Yaml.Mapping;
import org.openrewrite.yaml.tree.Yaml.Mapping.Entry;
import org.openrewrite.yaml.tree.Yaml.Scalar;

import java.util.ArrayList;
Expand All @@ -34,22 +33,13 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.lang.System.lineSeparator;
import static org.openrewrite.Cursor.ROOT_VALUE;
import static org.openrewrite.Tree.randomId;
import static org.openrewrite.internal.ListUtils.*;
import static org.openrewrite.internal.StringUtils.hasLineBreak;
import static org.openrewrite.internal.StringUtils.repeat;
import static org.openrewrite.yaml.MergeYaml.REMOVE_PREFIX;
/**
* Visitor class to merge two yaml files.
*
* @implNote Loops recursively through the documents, for every part a new MergeYamlVisitor instance will be created.
* As inline comments are put on the prefix of the next element (which can be an item very much higher in the tree),
* the following solutions are chosen to merge the comments as well:
* <ul>
* <li>when an element has new items, the comment of the next element is copied to the previous element
* <li>the original comment will be removed (either by traversing the children or by using cursor messages)
*
* @param <P> An input object that is passed to every visit method.
*/

@RequiredArgsConstructor
public class MergeYamlVisitor<P> extends YamlVisitor<P> {

Expand Down Expand Up @@ -163,9 +153,18 @@ private boolean keyMatches(Yaml.Mapping m1, Yaml.Mapping m2) {
}

private Mapping mergeMapping(Yaml.Mapping m1, Yaml.Mapping m2, P p, Cursor cursor) {
// Merge same key, different value together
List<Yaml.Mapping.Entry> mergedEntries = map(m1.getEntries(), existingEntry -> {
for (Entry incomingEntry : m2.getEntries()) {
for (Yaml.Mapping.Entry incomingEntry : m2.getEntries()) {
if (keyMatches(existingEntry, incomingEntry)) {
// multiline scalar cannot be formatted automatically, apply indent from existing value
if (shouldAutoFormat && incomingEntry.getValue() instanceof Yaml.Scalar && hasLineBreak(((Scalar) incomingEntry.getValue()).getValue())) {
String s = grabAfterFirstLineBreak(((Yaml.Scalar) existingEntry.getValue()).getValue());
int indent = s.length() - s.replaceFirst("^\\s+", "").length();
String x = ((Yaml.Scalar) incomingEntry.getValue()).getValue().replaceAll("\\R *", linebreak() + repeat(" ", indent));
incomingEntry = incomingEntry.withValue(((Scalar) incomingEntry.getValue()).withValue(x));
}

return existingEntry.withValue((Yaml.Block)
new MergeYamlVisitor<>(existingEntry.getValue(), incomingEntry.getValue(), acceptTheirs, objectIdentifyingProperty, shouldAutoFormat)
.visitNonNull(existingEntry.getValue(), p, new Cursor(cursor, existingEntry)));
Expand All @@ -174,15 +173,15 @@ private Mapping mergeMapping(Yaml.Mapping m1, Yaml.Mapping m2, P p, Cursor curso
return existingEntry;
});

List<Entry> mutatedEntries = concatAll(mergedEntries, map(m2.getEntries(), it -> {
for (Entry existingEntry : m1.getEntries()) {
// Merge existing and new entries together
List<Yaml.Mapping.Entry> mutatedEntries = concatAll(mergedEntries, map(m2.getEntries(), it -> {
for (Yaml.Mapping.Entry existingEntry : m1.getEntries()) {
if (keyMatches(existingEntry, it)) {
return null;
}
}
// workaround: autoFormat cannot handle new inserted values very well
if (!mergedEntries.isEmpty() && it.getValue() instanceof Yaml.Scalar && hasLineBreak(mergedEntries.get(0), 2)) {
return it.withPrefix(linebreak() + grabAfterFirstLineBreak(mergedEntries.get(0)));
if (shouldAutoFormat && it.getValue() instanceof Yaml.Scalar && hasLineBreak(((Scalar) it.getValue()).getValue())) {
it = it.withValue(it.getValue().withMarkers(it.getValue().getMarkers().add(new MultilineScalarAdded(randomId()))));
}
return shouldAutoFormat ? autoFormat(it, p, cursor) : it;
}));
Expand Down Expand Up @@ -221,7 +220,7 @@ private Mapping mergeMapping(Yaml.Mapping m1, Yaml.Mapping m2, P p, Cursor curso
}
}
// or retrieve it for last item from next element (could potentially be much higher in the tree)
if (comment == null && hasLineBreak(entries.get(entries.size() - 1), 1)) {
if (comment == null && hasLineBreak(entries.get(entries.size() - 1).getPrefix())) {
comment = grabBeforeFirstLineBreak(entries.get(entries.size() - 1));
}
}
Expand Down Expand Up @@ -313,17 +312,17 @@ private Yaml.Sequence mergeSequence(Yaml.Sequence s1, Yaml.Sequence s2, P p, Cur
}
}

private boolean hasLineBreak(Yaml.Mapping.Entry entry, int atLeastParts) {
return LINE_BREAK.matcher(entry.getPrefix()).find() && LINE_BREAK.split(entry.getPrefix()).length >= atLeastParts;
}

private String grabBeforeFirstLineBreak(Yaml.Mapping.Entry entry) {
String[] parts = LINE_BREAK.split(entry.getPrefix());
return parts.length > 0 ? parts[0] : "";
}

private String grabAfterFirstLineBreak(Yaml.Mapping.Entry entry) {
String[] parts = LINE_BREAK.split(entry.getPrefix());
return grabAfterFirstLineBreak(entry.getPrefix());
}

private String grabAfterFirstLineBreak(String s) {
String[] parts = LINE_BREAK.split(s);
return parts.length > 1 ? String.join(linebreak(), Arrays.copyOfRange(parts, 1, parts.length)) : "";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.openrewrite.yaml;

import lombok.Value;
import lombok.With;
import org.openrewrite.marker.Marker;

import java.util.UUID;

/**
* Multiline scalars are added directly to the tree, which leads to a wrong ident level.
*/
@Value
@With
public class MultilineScalarAdded implements Marker {
UUID id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.openrewrite.Cursor;
import org.openrewrite.Tree;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.yaml.MultilineScalarAdded;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.style.IndentsStyle;
import org.openrewrite.yaml.tree.Yaml;
Expand All @@ -28,7 +29,12 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.openrewrite.internal.StringUtils.repeat;

public class IndentsVisitor<P> extends YamlIsoVisitor<P> {

private static final Pattern COMMENT_PATTERN = Pattern.compile("^(\\s*)(.*\n?)", Pattern.MULTILINE);

private final IndentsStyle style;

@Nullable
Expand All @@ -46,7 +52,7 @@ public IndentsVisitor(IndentsStyle style, @Nullable Tree stopAfter) {
Yaml y = c.getValue();
String prefix = y.getPrefix();

if (prefix.contains("\n")) {
if (StringUtils.hasLineBreak(prefix)) {
int indent = findIndent(prefix);
if (indent != 0) {
c.putMessage("lastIndent", indent);
Expand All @@ -68,7 +74,7 @@ public IndentsVisitor(IndentsStyle style, @Nullable Tree stopAfter) {

Yaml y = tree;
int indent = getCursor().getNearestMessage("lastIndent", 0);
if (y.getPrefix().contains("\n") && !isUnindentedTopLevel()) {
if (StringUtils.hasLineBreak(y.getPrefix()) && !isUnindentedTopLevel()) {
if (y instanceof Yaml.Sequence.Entry) {
indent = getCursor().getParentOrThrow().getMessage("sequenceEntryIndent", indent);

Expand All @@ -95,12 +101,18 @@ public IndentsVisitor(IndentsStyle style, @Nullable Tree stopAfter) {
y = y.withPrefix(indentComments(y.getPrefix(), indent));
}
}

if (y instanceof Yaml.Scalar && y.getMarkers().findFirst(MultilineScalarAdded.class).isPresent()) {
String newValue = ((Yaml.Scalar) y).getValue().replaceAll("\\R", "\n" + repeat(" ", indent));
y = ((Yaml.Scalar) y).withValue(newValue);
}

return y;
}

private boolean isUnindentedTopLevel() {
return getCursor().getParentOrThrow().getValue() instanceof Yaml.Document ||
getCursor().getParentOrThrow(2).getValue() instanceof Yaml.Document;
getCursor().getParentOrThrow(2).getValue() instanceof Yaml.Document;
}

@Override
Expand All @@ -120,7 +132,7 @@ private boolean isUnindentedTopLevel() {
}

private String indentTo(String prefix, int column) {
if (prefix.contains("\n")) {
if (StringUtils.hasLineBreak(prefix)) {
int indent = findIndent(prefix);
if (indent != column) {
int shift = column - indent;
Expand All @@ -130,21 +142,19 @@ private String indentTo(String prefix, int column) {
return indentComments(prefix, column);
}

private static final Pattern COMMENT_PATTERN = Pattern.compile("^(\\s*)(.*\n?)", Pattern.MULTILINE);
private String indentComments(String prefix, int column) {
// If the prefix contains a newline followed by a comment ensure the comment begins at the indentation column
if (prefix.contains("#")) {
Matcher m = COMMENT_PATTERN.matcher(prefix);
StringBuilder result = new StringBuilder();
String indent = StringUtils.repeat(" ", column);
String indent = repeat(" ", column);
boolean firstLine = true;
while (m.find()) {
String whitespace = m.group(1);
String comment = m.group(2);
int newlineCount = StringUtils.countOccurrences(whitespace, "\n");
if (firstLine && newlineCount == 0) {
if(getCursor().getValue() instanceof Yaml.Documents ||
getCursor().getValue() instanceof Yaml.Document) {
if (getCursor().getValue() instanceof Yaml.Documents || getCursor().getValue() instanceof Yaml.Document) {
// Comments on a top-level
result.append(indent);
} else {
Expand Down
Loading

0 comments on commit bdc90bf

Please sign in to comment.