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 `` 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 maybeRedundantDef(ClassNode type, String name) { int saveCursor = cursor; Space defPrefix = whitespace(); - if(source.startsWith("def", cursor)) { + if (source.startsWith("def", cursor)) { skip("def"); // The def is redundant only when it is followed by the method's return type // I hope no one puts an annotation between "def" and the return type @@ -181,9 +181,9 @@ public G.CompilationUnit visit(SourceUnit unit, ModuleNode ast) throws GroovyPar for (ClassNode aClass : ast.getClasses()) { if (aClass.getSuperClass() == null || - !("groovy.lang.Script".equals(aClass.getSuperClass().getName()) || - "RewriteGradleProject".equals(aClass.getSuperClass().getName()) || - "RewriteSettings".equals(aClass.getSuperClass().getName()))) { + !("groovy.lang.Script".equals(aClass.getSuperClass().getName()) || + "RewriteGradleProject".equals(aClass.getSuperClass().getName()) || + "RewriteSettings".equals(aClass.getSuperClass().getName()))) { sortedByPosition.computeIfAbsent(pos(aClass), i -> new ArrayList<>()).add(aClass); } } @@ -218,8 +218,8 @@ public G.CompilationUnit visit(SourceUnit unit, ModuleNode ast) throws GroovyPar } throw new GroovyParsingException( "Failed to parse " + sourcePath + " at cursor position " + cursor + - ". The next 10 characters in the original source are `" + - source.substring(cursor, Math.min(source.length(), cursor + 10)) + "`", t); + ". The next 10 characters in the original source are `" + + source.substring(cursor, Math.min(source.length(), cursor + 10)) + "`", t); } } @@ -761,8 +761,135 @@ private List> visitRightPadded(ASTNode[] nodes, @Nullable St return ts; } + private Stack parenthesesStack = new Stack<>(); + + @RequiredArgsConstructor + private class ParenthesesHandler { + //private final int offset; + private final Space before; + + public Expression handle(Expression expr, Space after) { + System.out.println("HANDLE!! -> " + expr); + System.out.println(before); + System.out.println(after); + + cursor++; + return new J.Parentheses<>(randomId(), before, Markers.EMPTY, padRight(expr, after)); + } + } + private Expression insideParentheses(ASTNode node, Function parenthesizedTree) { + System.out.println("I->" + node); + + int saveCursor = cursor; + Space prefix = whitespace(); + System.out.println("WHITESPACE -> " +prefix+ " -> AST known level: " + getInsideParenthesesLevel(node)); + + /* + The biggest problem I encounter is the following, having this code of `lotOfSpacesAroundConstantWithParentheses` test (yes, with all those whitespace): + + ( ( ( "x" )).toString() ) + ^ ^ + | | + outer parenthese 2 parenthesis of + of MethodCallExpression ConstantExpression + + Now the algorithm start running: + 1. and then `(` => >MethodCallExpression is passed down to next cycle (recursive `insideParentheses`, line 851) + 2. and then `(` => MethodCallExpression is passed down to next cycle + 3. and then `(` => MethodCallExpression is passed down to next cycle + 3. and then "x" => we arrived at the ConstantExpression! + BUT the MethodCallExpression is not yet handled, so that's gonna be done first: `parenthesizedTree.apply(prefix);` + (to make it more complex, the MethodCallExpression will call the ConstantExpression, because it will call `visit(call.getObjectExpression())`) + The `prefix` here is the two whitespaces, applied to the MethodCallExpression. But that's actually wrong, because it should be applied to the prefix of the `ConstantExpression`. + + We could kinda fix this to combine the new code, with the old `getInsideParenthesesLevel(node)`. See below in comments. + The AST still knows the wrapping level of the ConstantExpression (that is 2), so maybe we could apply that first. + WARNING: Notice you still needs to pop from the parenthesesStack, because at that point you already have parsed all opening parenthesis... + */ + + + /*Integer insideParenthesesLevel = getInsideParenthesesLevel(node); + //System.out.println("insideParenthesesLevel: " + insideParenthesesLevel + " => " + cursor); + // AST contains information about the parentheses level, so apply it directly + if (insideParenthesesLevel != null) { + Deque openingParens = new ArrayDeque<>(); + for (int i = 0; i < insideParenthesesLevel; i++) { + //System.out.println(" HIER!!! ====> " + cursor); + //if (source.charAt(cursor - 1) == '(' && !parenthesesStack.isEmpty()) { + //System.out.println("POP!"); + openingParens.offerLast(parenthesesStack.pop().before); + //} else { + // openingParens.push(sourceBefore("(")); + //} + } + //System.out.println("EN BOOM: " + cursor); + Expression parenthesized = parenthesizedTree.apply(whitespace()); + for (int i = 0; i < insideParenthesesLevel; i++) { + parenthesized = new J.Parentheses<>(randomId(), openingParens.pop(), Markers.EMPTY, padRight(parenthesized, sourceBefore(")"))); + } + return parenthesized; + }*/ + + //return parenthesizedTree.apply(whitespace()); + + + + + //System.out.println("EAT WHITESPACE: " + cursor); + /*if (node instanceof CastExpression) { + // FIXME + return parenthesizedTree.apply(prefix); + }*/ + + Expression expr; + if (source.charAt(cursor) == '(' && (!(node instanceof CastExpression) || ((CastExpression) node).isCoerce())) { + System.out.println("INSIDE PARENTHESES => " + cursor); + cursor++; + System.out.println("SAVE CURSOR: " + saveCursor + " == PREFIX <" + prefix + ">"); + parenthesesStack.push(new ParenthesesHandler(/*saveCursor,*/ prefix)); + expr = insideParentheses(node, parenthesizedTree); + } else { + System.out.println("NOT INSIDE PARENTHESES ==> " + cursor); + expr = parenthesizedTree.apply(prefix); + System.out.println("APPLY, AND THEN: " + cursor); + } + + int saveCursor2 = cursor; + Space after = whitespace(); + System.out.println("AND GOOOO: " + cursor); + if (cursor < source.length() && source.charAt(cursor) == ')' && !parenthesesStack.isEmpty() /*&& + (parenthesesStack.peek().offset == saveCursor || parenthesesStack.peek().offset + 1 == saveCursor)*/) { + System.out.println("HANDLE PARENTHESES"); + return parenthesesStack.pop().handle(expr, after); + } else { + //System.out.println("RESET WHITESPACE TO: " + saveCursor2); + cursor = saveCursor2; + } + return expr; + } + + + private Expression insideParenthesesForMethodInvocation(ASTNode node, Function parenthesizedTree) { + System.out.println(node); + System.out.println(source.charAt(cursor)); + + Space x = whitespace(); + + /* if (source.charAt(cursor) == '(') { + parentheses.push(() -> p) + }*/ + + + return parenthesizedTree.apply(whitespace()); + } + + private Expression insideParenthesesOld(ASTNode node, Function parenthesizedTree) { + System.out.println(node); + System.out.println(source.charAt(cursor)); + Integer insideParenthesesLevel = getInsideParenthesesLevel(node); + // AST contains information about the parentheses level, so apply it directly if (insideParenthesesLevel != null) { Stack openingParens = new Stack<>(); for (int i = 0; i < insideParenthesesLevel; i++) { @@ -770,8 +897,7 @@ private Expression insideParentheses(ASTNode node, Function p } Expression parenthesized = parenthesizedTree.apply(whitespace()); for (int i = 0; i < insideParenthesesLevel; i++) { - parenthesized = new J.Parentheses<>(randomId(), openingParens.pop(), Markers.EMPTY, - padRight(parenthesized, sourceBefore(")"))); + parenthesized = new J.Parentheses<>(randomId(), openingParens.pop(), Markers.EMPTY, padRight(parenthesized, sourceBefore(")"))); } return parenthesized; } @@ -821,9 +947,9 @@ public void visitArgumentlistExpression(ArgumentListExpression expression) { // https://docs.groovy-lang.org/latest/html/documentation/#_named_parameters_2 // When named parameters are in use they may appear before, after, or intermixed with any positional arguments if (unparsedArgs.size() > 1 && unparsedArgs.get(0) instanceof MapExpression && - (unparsedArgs.get(0).getLastLineNumber() > unparsedArgs.get(1).getLastLineNumber() || - (unparsedArgs.get(0).getLastLineNumber() == unparsedArgs.get(1).getLastLineNumber() && - unparsedArgs.get(0).getLastColumnNumber() > unparsedArgs.get(1).getLastColumnNumber()))) { + (unparsedArgs.get(0).getLastLineNumber() > unparsedArgs.get(1).getLastLineNumber() || + (unparsedArgs.get(0).getLastLineNumber() == unparsedArgs.get(1).getLastLineNumber() && + unparsedArgs.get(0).getLastColumnNumber() > unparsedArgs.get(1).getLastColumnNumber()))) { // Figure out the source-code ordering of the expressions MapExpression namedArgExpressions = (MapExpression) unparsedArgs.get(0); @@ -1119,8 +1245,8 @@ public void visitCatchStatement(CatchStatement node) { // Groovy allows catch variables to omit their type, shorthand for being of type java.lang.Exception // Can't use isSynthetic() here because groovy doesn't record the line number on the Parameter if ("java.lang.Exception".equals(param.getType().getName()) && - !source.startsWith("Exception", cursor) && - !source.startsWith("java.lang.Exception", cursor)) { + !source.startsWith("Exception", cursor) && + !source.startsWith("java.lang.Exception", cursor)) { paramType = new J.Identifier(randomId(), paramPrefix, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Exception"), null); } else { @@ -1189,22 +1315,20 @@ private J.Case visitDefaultCaseStatement(BlockStatement statement) { @Override public void visitCastExpression(CastExpression cast) { queue.add(insideParentheses(cast, prefix -> { - // Might be looking at a Java-style cast "(type)object" or a groovy-style cast "object as type" - if (source.charAt(cursor) == '(') { - cursor++; // skip '(' - return new J.TypeCast(randomId(), prefix, Markers.EMPTY, - new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY, - new JRightPadded<>(visitTypeTree(cast.getType()), sourceBefore(")"), Markers.EMPTY) - ), - visit(cast.getExpression())); - } else { + System.out.println("visitCastExpression isCoerce: " + cast.isCoerce() + " =>> " + cursor + " | PREFIX: " + prefix); + + if (cast.isCoerce()) { // a groovy-style cast "object as type" Expression expr = visit(cast.getExpression()); Space asPrefix = sourceBefore("as"); - + System.out.println("visitCastExpression CUUUURSSOOOOOR: " + cursor); return new J.TypeCast(randomId(), prefix, new Markers(randomId(), singletonList(new AsStyleTypeCast(randomId()))), - new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY, - new JRightPadded<>(visitTypeTree(cast.getType()), asPrefix, Markers.EMPTY)), + new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY, padRight(visitTypeTree(cast.getType()), asPrefix)), expr); + } else { // a Java-style cast"(type)object" + cursor++; // skip '(' + return new J.TypeCast(randomId(), prefix, Markers.EMPTY, + new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY, padRight(visitTypeTree(cast.getType()), sourceBefore(")"))), + visit(cast.getExpression())); } })); } @@ -1291,6 +1415,7 @@ public void visitClosureListExpression(ClosureListExpression closureListExpressi @Override public void visitConstantExpression(ConstantExpression expression) { queue.add(insideParentheses(expression, fmt -> { + System.out.println("visitConstantExpression CUUUURSSOOOOOR: " + cursor); JavaType.Primitive jType; // The unaryPlus is not included in the expression and must be handled through the source. String text = expression.getText(); @@ -1669,6 +1794,8 @@ public void visitMethodCallExpression(MethodCallExpression call) { JRightPadded select = null; if (!call.isImplicitThis()) { Expression selectExpr = visit(call.getObjectExpression()); + + System.out.println("visitMethodCallExpression CUUUURSSOOOOOR: " + cursor); int saveCursor = cursor; Space afterSelect = whitespace(); if (source.charAt(cursor) == '.' || source.charAt(cursor) == '?' || source.charAt(cursor) == '*') { @@ -1888,7 +2015,7 @@ public void visitRangeExpression(RangeExpression range) { public void visitReturnStatement(ReturnStatement return_) { Space fmt = sourceBefore("return"); if (return_.getExpression() instanceof ConstantExpression && isSynthetic(return_.getExpression()) && - (((ConstantExpression) return_.getExpression()).getValue() == null)) { + (((ConstantExpression) return_.getExpression()).getValue() == null)) { queue.add(new J.Return(randomId(), fmt, Markers.EMPTY, null)); } else { queue.add(new J.Return(randomId(), fmt, Markers.EMPTY, visit(return_.getExpression()))); @@ -2110,6 +2237,7 @@ public TypeTree visitVariableExpressionType(VariableExpression expression) { @Override public void visitVariableExpression(VariableExpression expression) { + System.out.println("...VariableExpression... => " + cursor); queue.add(insideParentheses(expression, fmt -> { JavaType type; if (expression.isDynamicTyped() && expression.getAccessedVariable() != null && expression.getAccessedVariable().getType() != expression.getOriginType()) { @@ -2317,6 +2445,10 @@ private int positionOfNext(String untilDelim) { return delimIndex > source.length() - untilDelim.length() ? -1 : delimIndex; } + /** + * Get all whitespace characters of the source file between the cursor and the first non whitespace character. + * The cursor will be moved before the next non whitespace character. + */ private Space whitespace() { String prefix = source.substring(cursor, indexOfNextNonWhitespace(cursor, source)); cursor += prefix.length(); @@ -2464,15 +2596,18 @@ private int sourceLengthOfString(ConstantExpression expr) { return lengthAccordingToAst; } - private static @Nullable Integer getInsideParenthesesLevel(ASTNode node) { + private @Nullable Integer getInsideParenthesesLevel(ASTNode node) { Object rawIpl = node.getNodeMetaData("_INSIDE_PARENTHESES_LEVEL"); if (rawIpl instanceof AtomicInteger) { // On Java 11 and newer _INSIDE_PARENTHESES_LEVEL is an AtomicInteger return ((AtomicInteger) rawIpl).get(); - } else { + } else if (rawIpl instanceof Integer) { // On Java 8 _INSIDE_PARENTHESES_LEVEL is a regular Integer return (Integer) rawIpl; - } + }/* else if (node instanceof MethodCallExpression) { + return GroovyParserParentheseDiscoverer.getInsideParenthesesLevel((MethodCallExpression) node, source, cursor); + }*/ + return null; } private int getDelimiterLength() { @@ -2646,7 +2781,7 @@ private String name() { Can contain a J.FieldAccess, as in a variable declaration with fully qualified type parameterization: List */ - private JContainer visitTypeParameterizations(GenericsType @Nullable[] genericsTypes) { + private JContainer visitTypeParameterizations(GenericsType @Nullable [] genericsTypes) { Space prefix = sourceBefore("<"); List> parameters; diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyParserParentheseDiscovererTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyParserParentheseDiscovererTest.java new file mode 100644 index 00000000000..6fa79d7f8c1 --- /dev/null +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/GroovyParserParentheseDiscovererTest.java @@ -0,0 +1,186 @@ +/* + * 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 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(