diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserParentheseDiscoverer.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserParentheseDiscoverer.java new file mode 100644 index 00000000000..9448290c356 --- /dev/null +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserParentheseDiscoverer.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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;
+
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.internal.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class GroovyParserParentheseDiscoverer {
+ // Matches a code block including leading and trailing parenthesis
+ // Eg: ((((a.invoke~~>("arg")))))<~~ or ~~>(((((( "" as String )))))<~~.toString())
+ private static final Pattern PARENTHESES_GROUP = Pattern.compile(".*?(\\(+[^()]+\\)+).*", Pattern.DOTALL);
+ // Matches when string consist of multiple parenthese blocks
+ // Eg: `((a.invoke())` does not match, `((a.invoke() && b.invoke())` does match
+ private static final Pattern HAS_SUB_PARENTHESIS = Pattern.compile("\\(+[^)]+\\)+[^)]+\\).*", Pattern.DOTALL);
+
+ /**
+ * Calculates the insideParenthesesLevel for method calls, because the compiler does not set the _INSIDE_PARENTHESES_LEVEL flag for method calls.
+ */
+ public static @Nullable Integer getInsideParenthesesLevel(MethodCallExpression node, String source, int cursor) {
+ String sourceFromCursor = source.substring(cursor);
+
+ // start with a (` character with optional whitespace
+ if (sourceFromCursor.matches("(?s)^\\s*\\(.*")) {
+ String sourceNoWhitespace = sourceFromCursor.replaceAll("\\s+", "");
+ // grab the source code until method and closing parenthesis
+ Matcher m = Pattern.compile("(?s)(.*?" + node.getMethodAsString() + "[^)]*\\)+).*").matcher(sourceNoWhitespace);
+ if (m.matches()) {
+ // Replace all string literals with `
+ * 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;
+
+import lombok.SneakyThrows;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.ExpectedToFail;
+import sun.reflect.ReflectionFactory;
+
+import java.lang.reflect.Constructor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class GroovyParserParentheseDiscovererTest {
+
+ @Test
+ void invoke() {
+ MethodCallExpression node = MockMethodCallExpression.of("invoke");
+
+ //language=groovy
+ String source = "(a.invoke())";
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(1, result);
+ }
+
+ @Test
+ void invokeWithArguments() {
+ MethodCallExpression node = MockMethodCallExpression.of("invoke");
+
+ //language=groovy
+ String source = """
+ (a.invoke("A", "\\$", "C?)"))
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(1, result);
+ }
+
+ @Test
+ void invokeWithSpacesInParenthesis() {
+ MethodCallExpression node = MockMethodCallExpression.of("invoke");
+
+ //language=groovy
+ String source = "( ( (((a.invoke()) ) ) ) )";
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(5, result);
+ }
+
+ @Test
+ void invokeWithSpacesInParenthesis2() {
+ MethodCallExpression node = MockMethodCallExpression.of("equals");
+
+ //language=groovy
+ String source = """
+ (((((((someMap.get("(bar"))))).equals("baz") ) ) )
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(3, result);
+ }
+
+ @Test
+ void invokeWithNullSafeAndInQuotesAndArgumentsWithoutParenthesis() {
+ MethodCallExpression node = MockMethodCallExpression.of("invoke");
+
+ //language=groovy
+ String source = """
+ (something?.'invoke' "s" "a" )
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(1, result);
+ }
+
+ @Test
+ void multipleParenthesesLeftBiggerThanRight() {
+ MethodCallExpression node = MockMethodCallExpression.of("equals");
+
+ //language=groovy
+ String source = """
+ ((((((someMap.get("baz"))))).equals("baz")))
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(2, result);
+ }
+
+ @Test
+ void multipleParenthesesRightBiggerThanLeft() {
+ MethodCallExpression node = MockMethodCallExpression.of("get");
+
+ //language=groovy
+ String source = """
+ ((((someMap.get("(bar")))))
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(4, result);
+ }
+
+ @Test
+ void binaryWithLinebreakInArgument() {
+ MethodCallExpression node = MockMethodCallExpression.of("equals");
+
+ //language=groovy
+ String source = """
+ ((someMap.containsKey("foo")) && ((someMap.get("foo")).'equals' ""\"bar
+ ""\" ))
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 0);
+
+ assertEquals(1, result);
+ }
+
+ @Test
+ void sourceCodeWithRepetition() {
+ MethodCallExpression node = MockMethodCallExpression.of("invoke");
+
+ //language=groovy
+ String source = "(a.invoke());((((a.invoke()))));(a.invoke())";
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 13);
+
+ assertEquals(4, result);
+ }
+
+ @Test
+ @ExpectedToFail("Comments are not yet working...")
+ void comments() {
+ MethodCallExpression node = MockMethodCallExpression.of("equals");
+
+ //language=groovy
+ String source = """
+ // comment
+ ((someMap./* comment */containsKey("f/* no-com"+
+
+ "ment */oo")) && /* multi line
+ comment
+ */((someMap.get("foo")).'equals' ""\"bar
+ ""\" ))
+ /* comment
+
+ */
+ """;
+ Integer result = GroovyParserParentheseDiscoverer.getInsideParenthesesLevel(node, source, 13);
+
+ assertEquals(4, result);
+ }
+
+ private static class MockMethodCallExpression extends MethodCallExpression {
+ String method = "";
+
+ @SneakyThrows
+ private static MockMethodCallExpression of(String method) {
+ Constructor> constructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(MockMethodCallExpression.class, Object.class.getConstructor());
+ MockMethodCallExpression expression = (MockMethodCallExpression) constructor.newInstance();
+ expression.method = method;
+ return expression;
+ }
+
+ public MockMethodCallExpression(Expression objectExpression, String method, Expression arguments) {
+ super(null, method, null);
+ }
+
+ @Override
+ public String getMethodAsString() {
+ return method;
+ }
+ }
+}
diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/BinaryTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/BinaryTest.java
index 13239c0fc76..aca502d3571 100644
--- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/BinaryTest.java
+++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/BinaryTest.java
@@ -15,7 +15,6 @@
*/
package org.openrewrite.groovy.tree;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.openrewrite.Issue;
import org.openrewrite.test.RewriteTest;
@@ -34,6 +33,7 @@ void insideParentheses() {
// NOT inside parentheses, but verifies the parser's
// test for "inside parentheses" condition
+ groovy("( 1 ) + 1"),
groovy("(1) + 1"),
// And combine the two cases
groovy("((1) + 1)")
@@ -216,6 +216,35 @@ void stringMultipliedInParentheses() {
);
}
+ @Issue("https://github.com/openrewrite/rewrite/issues/4703")
+ @Test
+ void cxds() {
+ rewriteRun(
+ groovy(
+ """
+ def differenceInDays(int time) {
+ return (int) ((time)/(1000*60*60*24))
+ }
+ """
+ )
+ );
+ }
+
+ @Issue("https://github.com/openrewrite/rewrite/issues/4703")
+ @Test
+ void extraParensAroundInfixOxxxperator() {
+ rewriteRun(
+ groovy(
+ """
+ def timestamp(int hours, int minutes, int seconds) {
+ 30 * (hours)
+ (((((hours))))) * 30
+ }
+ """
+ )
+ );
+ }
+
@Issue("https://github.com/openrewrite/rewrite/issues/4703")
@Test
void extraParensAroundInfixOperator() {
diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CastTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CastTest.java
index 5cf82ae85be..c2a5fa54242 100755
--- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CastTest.java
+++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CastTest.java
@@ -25,6 +25,18 @@
@SuppressWarnings({"UnnecessaryQualifiedReference", "GroovyUnusedAssignment", "GrUnnecessarySemicolon"})
class CastTest implements RewriteTest {
+ @Test
+ void javaStyleCastNew() {
+ rewriteRun(
+ groovy(
+ """
+ String foo = ( String ) "hallo"
+ String bar = "hallo" as String
+ """
+ )
+ );
+ }
+
@Test
void javaStyleCast() {
rewriteRun(
@@ -74,14 +86,13 @@ void groovyCastAndInvokeMethod() {
rewriteRun(
groovy(
"""
- ( "" as String ).toString()
+ ( "x" as String ).toString()
"""
)
);
}
@Test
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
void groovyCastAndInvokeMethodWithParentheses() {
rewriteRun(
groovy(
@@ -103,7 +114,6 @@ void javaCastAndInvokeMethod() {
);
}
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
@Test
void javaCastAndInvokeMethodWithParentheses() {
rewriteRun(
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 cd5ea56bed3..30f60290fb4 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
@@ -16,7 +16,6 @@
package org.openrewrite.groovy.tree;
import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.ExpectedToFail;
import org.openrewrite.Issue;
import org.openrewrite.test.RewriteTest;
@@ -46,7 +45,6 @@ void gradle() {
);
}
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
@Test
@Issue("https://github.com/openrewrite/rewrite/issues/4615")
void gradleWithParentheses() {
@@ -280,7 +278,29 @@ static void main(String[] args) {
);
}
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
+ @Issue("https://github.com/openrewrite/rewrite/issues/4703")
+ @Test
+ void insideParenthesesSimple() {
+ rewriteRun(
+ groovy(
+ """
+ ((a.invoke "b" ))
+ """
+ )
+ );
+ }
+
+ @Test
+ void lotOfSpacesAroundConstantWithParentheses() {
+ rewriteRun(
+ groovy(
+ """
+ ( ( ( "x" ) ).toString() )
+ """
+ )
+ );
+ }
+
@Issue("https://github.com/openrewrite/rewrite/issues/4703")
@Test
void insideParentheses() {
@@ -296,7 +316,6 @@ static def foo(Map map) {
);
}
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
@Issue("https://github.com/openrewrite/rewrite/issues/4703")
@Test
void insideParenthesesWithoutNewLineAndEscapedMethodName() {
diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RangeTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RangeTest.java
index ac839e34d88..1bf691a8ca6 100644
--- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RangeTest.java
+++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RangeTest.java
@@ -48,7 +48,6 @@ void parenthesized() {
);
}
- @ExpectedToFail("Parentheses with method invocation is not yet supported")
@Test
void parenthesizedAndInvokeMethodWithParentheses() {
rewriteRun(
` to prevent weird matches with the PARENTHESES_GROUP regex
+ String s = m.group(1).replaceAll("\"[^\"]*\"", "");
+ return GroovyParserParentheseDiscoverer.getInsideParenthesesLevelForMethodCalls(s);
+ }
+ }
+
+ return null;
+ }
+
+ private static int getInsideParenthesesLevelForMethodCalls(String source) {
+ String s = source;
+ // lookup for first code block with parenthesis
+ Matcher m = PARENTHESES_GROUP.matcher(s);
+ // check if source matches any code block with parenthesis + check if there are still inner parenthesis
+ while (m.matches() && HAS_SUB_PARENTHESIS.matcher(s).find()) {
+ int parenthesis = lowestParentheseLevel(m.group(1));
+ String part = m.group(1).replaceAll("\\(", "").replaceAll("\\)", "");
+ String regex = StringUtils.repeat("\\(", parenthesis) + part + StringUtils.repeat("\\)", parenthesis);
+ // remove parentheses and arguments in source code
+ s = s.replaceAll(regex, "");
+ // move to next possible code block with parenthesis
+ m = PARENTHESES_GROUP.matcher(s);
+ }
+
+ return lowestParentheseLevel(s);
+ }
+
+ private static int lowestParentheseLevel(String s) {
+ int leadingParenthesis = 0;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
+ else if (c == '(') leadingParenthesis++;
+ else break;
+ }
+ int trailingParenthesis = 0;
+ for (int i = s.length() - 1; i >= 0; i--) {
+ char c = s.charAt(i);
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
+ else if (c == ')') trailingParenthesis++;
+ else break;
+ }
+
+ return Math.min(leadingParenthesis, trailingParenthesis);
+ }
+}
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 b710c0c78cc..7b0567381ae 100644
--- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java
+++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java
@@ -113,16 +113,16 @@ private static boolean isOlderThanGroovy3() {
}
/**
- * Groovy methods can be declared with "def" AND a return type
- * In these cases the "def" is semantically meaningless but needs to be preserved for source code accuracy
- * If there is both a def and a return type, this method returns a RedundantDef object and advances the cursor
- * position past the "def" keyword, leaving the return type to be parsed as normal.
- * In any other situation an empty Optional is returned and the cursor is not advanced.
+ * Groovy methods can be declared with "def" AND a return type
+ * In these cases the "def" is semantically meaningless but needs to be preserved for source code accuracy
+ * If there is both a def and a return type, this method returns a RedundantDef object and advances the cursor
+ * position past the "def" keyword, leaving the return type to be parsed as normal.
+ * In any other situation an empty Optional is returned and the cursor is not advanced.
*/
private Optional