diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java index 42b55f6418e..d2282b0b189 100644 --- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java @@ -32,6 +32,7 @@ import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.FileAttributes; +import org.openrewrite.groovy.internal.Delimiter; import org.openrewrite.groovy.marker.*; import org.openrewrite.groovy.tree.G; import org.openrewrite.internal.EncodingDetectingInputStream; @@ -62,8 +63,8 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; import static org.openrewrite.Tree.randomId; +import static org.openrewrite.groovy.internal.Delimiter.*; import static org.openrewrite.internal.StringUtils.indexOfNextNonWhitespace; import static org.openrewrite.java.tree.Space.EMPTY; import static org.openrewrite.java.tree.Space.format; @@ -1337,13 +1338,11 @@ public void visitConstantExpression(ConstantExpression expression) { value = "@" + value; text = "@" + text; } else { - String delimiter = getDelimiter(); + Delimiter delimiter = getDelimiter(cursor); if (delimiter != null) { - // handle the `pattern` operator - String endDelimiter = "~/".equals(delimiter) ? "/" : delimiter; // Get the string literal from the source, so escaping of newlines and the like works out of the box - value = sourceSubstring(cursor + delimiter.length(), endDelimiter); - text = delimiter + value + endDelimiter; + value = sourceSubstring(cursor + delimiter.open.length(), delimiter.close); + text = delimiter.open + value + delimiter.close; } } } else if (expression.isNullExpression()) { @@ -1597,8 +1596,8 @@ public void visitIfElse(IfStatement ifElse) { @Override public void visitGStringExpression(GStringExpression gstring) { Space fmt = whitespace(); - String delimiter = getDelimiter(); - skip(delimiter); // Opening delim for GString + Delimiter delimiter = getDelimiter(cursor); + skip(delimiter.open); NavigableMap sortedByPosition = new TreeMap<>(); for (ConstantExpression e : gstring.getStrings()) { @@ -1629,7 +1628,7 @@ public void visitGStringExpression(GStringExpression gstring) { } } else if (e instanceof ConstantExpression) { // Get the string literal from the source, so escaping of newlines and the like works out of the box - String value = sourceSubstring(cursor, ("~/".equals(delimiter) ? "/" : delimiter)); + String value = sourceSubstring(cursor, delimiter.close); // There could be a closer GString before the end of the closing delimiter, so shorten the string if needs be int indexNextSign = source.indexOf("$", cursor); if (indexNextSign != -1 && indexNextSign < (cursor + value.length())) { @@ -1643,8 +1642,8 @@ public void visitGStringExpression(GStringExpression gstring) { } } - queue.add(new G.GString(randomId(), fmt, Markers.EMPTY, delimiter, strings, typeMapping.type(gstring.getType()))); - skip(delimiter); // Closing delim for GString + queue.add(new G.GString(randomId(), fmt, Markers.EMPTY, delimiter.open, strings, typeMapping.type(gstring.getType()))); + skip(delimiter.close); } @Override @@ -2537,34 +2536,48 @@ private int determineParenthesisLevel(int childLineNumber, int parentLineNumber, int start = sourceLineNumberOffsets[startingLineNumber - 1] + startingColumn - 1; int end = sourceLineNumberOffsets[endingLineNumber - 1] + endingColumn - 1; - // Determine level of parentheses by going through the source + // Determine level of parentheses by going through the source, don't count parentheses in comments, string literals and regexes int count = 0; + Delimiter delimiter = null; for (int i = start; i < end; i++) { - if (source.charAt(i) == '(') { - count++; - } else if (source.charAt(i) == ')') { - count--; + if (delimiter == null) { + delimiter = getDelimiter(i); + if (delimiter == null) { + if (source.charAt(i) == '(') { + count++; + } else if (source.charAt(i) == ')') { + count--; + } + } else { + i += delimiter.open.length() - 1; // skip the next chars for the rest of the delimiter + } + } else if (source.startsWith(delimiter.close, i)) { + i += delimiter.close.length() - 1; // skip the next chars for the rest of the delimiter + delimiter = null; } } return Math.max(count, 0); } - private @Nullable String getDelimiter() { - String maybeDelimiter = source.substring(cursor, Math.min(cursor + 3, source.length())); - if (maybeDelimiter.startsWith("$/")) { - return "$/"; - } else if (maybeDelimiter.startsWith("\"\"\"")) { - return "\"\"\""; - } else if (maybeDelimiter.startsWith("'''")) { - return "'''"; - } else if (maybeDelimiter.startsWith("~/")) { - return "~/"; - } else if (maybeDelimiter.startsWith("/")) { - return "/"; - } else if (maybeDelimiter.startsWith("\"")) { - return "\""; - } else if (maybeDelimiter.startsWith("'")) { - return "'"; + private @Nullable Delimiter getDelimiter(int cursor) { + if (source.startsWith("$/", cursor)) { + return DOLLAR_SLASHY_STRING; + } else if (source.startsWith("\"\"\"", cursor)) { + return TRIPLE_DOUBLE_QUOTE_STRING; + } else if (source.startsWith("'''", cursor)) { + return TRIPLE_SINGLE_QUOTE_STRING; + } else if (source.startsWith("~/", cursor)) { + return PATTERN_OPERATOR; + } else if (source.startsWith("//", cursor)) { + return SINGLE_LINE_COMMENT; + } else if (source.startsWith("/*", cursor)) { + return MULTILINE_COMMENT; + } else if (source.startsWith("/", cursor)) { + return SLASHY_STRING; + } else if (source.startsWith("\"", cursor)) { + return DOUBLE_QUOTE_STRING; + } else if (source.startsWith("'", cursor)) { + return SINGLE_QUOTE_STRING; } return null; } diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java index 69923fd1a1c..a87fe3389c0 100644 --- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java @@ -19,6 +19,7 @@ import org.openrewrite.Cursor; import org.openrewrite.PrintOutputCapture; import org.openrewrite.Tree; +import org.openrewrite.groovy.internal.Delimiter; import org.openrewrite.groovy.marker.*; import org.openrewrite.groovy.tree.G; import org.openrewrite.groovy.tree.GContainer; @@ -34,6 +35,8 @@ import java.util.Optional; import java.util.function.UnaryOperator; +import static org.openrewrite.groovy.internal.Delimiter.DOUBLE_QUOTE_STRING; + public class GroovyPrinter

extends GroovyVisitor> { private final GroovyJavaPrinter delegate = new GroovyJavaPrinter(); @@ -73,18 +76,14 @@ public J visitCompilationUnit(G.CompilationUnit cu, PrintOutputCapture

p) { @Override public J visitGString(G.GString gString, PrintOutputCapture

p) { beforeSyntax(gString, GSpace.Location.GSTRING, p); - String delimiter = gString.getDelimiter(); + Delimiter delimiter = Delimiter.of(gString.getDelimiter()); if (delimiter == null) { // For backwards compatibility with ASTs before we collected this field - delimiter = "\""; + delimiter = DOUBLE_QUOTE_STRING; } - p.append(delimiter); + p.append(delimiter.open); visit(gString.getStrings(), p); - if ("$/".equals(delimiter)) { - p.append("/$"); - } else { - p.append(delimiter); - } + p.append(delimiter.close); afterSyntax(gString, p); return gString; } diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/internal/Delimiter.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/internal/Delimiter.java new file mode 100644 index 00000000000..fad1e0bd20a --- /dev/null +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/internal/Delimiter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025 the original author or authors. + *

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

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.groovy.internal; + +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; + +@RequiredArgsConstructor +public enum Delimiter { + SINGLE_QUOTE_STRING("'", "'"), + DOUBLE_QUOTE_STRING("\"", "\""), + TRIPLE_SINGLE_QUOTE_STRING("'''", "'''"), + TRIPLE_DOUBLE_QUOTE_STRING("\"\"\"", "\"\"\""), + SLASHY_STRING("/", "/"), + DOLLAR_SLASHY_STRING("$/", "/$"), + PATTERN_OPERATOR("~/", "/"), + SINGLE_LINE_COMMENT("//", "\n"), + MULTILINE_COMMENT("/*", "*/"); + + public final String open; + public final String close; + + public static @Nullable Delimiter of(String open) { + for (Delimiter delim : Delimiter.values()) { + if (delim.open.equals(open)) { + return delim; + } + } + return null; + } +} diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/LiteralTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/LiteralTest.java index 0bc2ac3535d..62a62464c28 100644 --- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/LiteralTest.java +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/LiteralTest.java @@ -191,6 +191,17 @@ void gStringWithEscapedDelimiter() { ); } + @Test + void gStringWithStringLiteralsWithParentheses() { + rewriteRun( + groovy( + """ + "Hello ${from(":-)").via(''':-|(''').via(":-)").to(':-(')}!" + """ + ) + ); + } + @Test void mapLiteral() { rewriteRun( @@ -313,6 +324,11 @@ void stringLiteralInParentheses() { """ def a = (("-")) """ + ), + groovy( + """ + from(":-)").via(''':-|(''').via(":-)").to(':-(') + """ ) ); } diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/MethodInvocationTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/MethodInvocationTest.java index 78e4444c7c0..c7896db23ed 100644 --- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/MethodInvocationTest.java +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/MethodInvocationTest.java @@ -506,4 +506,19 @@ void insideFourParenthesesAndEnters() { ) ); } + + @Test + void insideFourParenthesesAndEntersWithCommentWithParentheses() { + rewriteRun( + groovy( + """ + ((/* comment :-) (( + */(( + // :-(( + /*)*/something(a) + )))) + """ + ) + ); + } }