diff --git a/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11ParserVisitor.java b/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11ParserVisitor.java index 243acd89904..22086c5db31 100644 --- a/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11ParserVisitor.java +++ b/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11ParserVisitor.java @@ -30,9 +30,11 @@ import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.FileAttributes; +import org.openrewrite.PrintOutputCapture; import org.openrewrite.internal.EncodingDetectingInputStream; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaParsingException; +import org.openrewrite.java.JavaPrinter; import org.openrewrite.java.internal.JavaTypeCache; import org.openrewrite.java.marker.OmitParentheses; import org.openrewrite.java.tree.*; @@ -191,6 +193,16 @@ public J visitAssignment(AssignmentTree node, Space fmt) { typeMapping.type(node)); } + @Override + public J visitErroneous(ErroneousTree node, Space fmt) { + String erroneousNode = source.substring(((JCTree) node).getStartPosition(), ((JCTree) node).getEndPosition(endPosTable)); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode); + } + @Override public J visitBinary(BinaryTree node, Space fmt) { Expression left = convert(node.getLeftOperand()); @@ -1439,6 +1451,22 @@ public J visitUnary(UnaryTree node, Space fmt) { @Override public J visitVariable(VariableTree node, Space fmt) { + JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) node; + if ("".equals(jcVariableDecl.getName().toString())) { + int startPos = jcVariableDecl.getStartPosition(); + int endPos = jcVariableDecl.getEndPosition(endPosTable); + + if (startPos == endPos) { + endPos = startPos + 1; + } + String erroneousNode = source.substring(startPos, endPos); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode + ); + } return hasFlag(node.getModifiers(), Flags.ENUM) ? visitEnumVariable(node, fmt) : visitVariables(singletonList(node), fmt); // method arguments cannot be multi-declarations @@ -1619,10 +1647,43 @@ private JRightPadded convert(Tree t, Function su J2 j = convert(t); @SuppressWarnings("ConstantConditions") JRightPadded rightPadded = j == null ? null : new JRightPadded<>(j, suffix.apply(t), Markers.EMPTY); - cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + int idx = findFirstNonWhitespaceChar(rightPadded.getAfter().getWhitespace()); + if (idx >= 0) { + rightPadded = (JRightPadded) JRightPadded.build(getErroneous(List.of(rightPadded))); + } + // Cursor hasn't been updated but points at the end of erroneous node already + // This means that error node start position == end position + // Therefore ensure that cursor has moved to the end of erroneous node bu adding its length to the cursor + // Example `/pet` results in 2 erroeneous nodes: `/` and `pet`. The `/` node would have start and end position the + // same from the JC compiler. + if (endPos(t) == cursor && rightPadded.getElement() instanceof J.Erroneous) { + cursor += ((J.Erroneous) rightPadded.getElement()).getText().length(); + } else { + cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + } return rightPadded; } + private J.Erroneous getErroneous(List> converted) { + PrintOutputCapture p = new PrintOutputCapture<>(0); + new JavaPrinter<>().visitContainer(JContainer.build(EMPTY, converted, Markers.EMPTY), JContainer.Location.METHOD_INVOCATION_ARGUMENTS, p); + return new J.Erroneous( + org.openrewrite.Tree.randomId(), + EMPTY, + Markers.EMPTY, + p.getOut() + ); + } + + private static int findFirstNonWhitespaceChar(String s) { + for (int i = 0; i < s.length(); i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + return -1; + } + private long lineNumber(Tree tree) { return source.substring(0, ((JCTree) tree).getStartPosition()).chars().filter(c -> c == '\n').count() + 1; } @@ -1687,9 +1748,7 @@ private Space statementDelim(@Nullable Tree t) { t instanceof JCNewClass || t instanceof JCReturn || t instanceof JCThrow || - t instanceof JCUnary || - t instanceof JCExpressionStatement || - t instanceof JCVariableDecl) { + t instanceof JCUnary) { return sourceBefore(";"); } @@ -1697,9 +1756,39 @@ private Space statementDelim(@Nullable Tree t) { return statementDelim(((JCLabeledStatement) t).getStatement()); } + if (t instanceof JCExpressionStatement) { + ExpressionTree expTree = ((ExpressionStatementTree) t).getExpression(); + if (expTree instanceof ErroneousTree) { + return Space.build(source.substring(((JCTree) expTree).getEndPosition(endPosTable),((JCTree) t).getEndPosition(endPosTable)), Collections.emptyList()); + } else { + return sourceBefore(";"); + } + } + + if (t instanceof JCVariableDecl) { + JCTree.JCVariableDecl varTree = (JCTree.JCVariableDecl) t; + if ("".contentEquals(varTree.getName())) { + int start = varTree.vartype.getEndPosition(endPosTable); + int end = varTree.getEndPosition(endPosTable); + String whitespace = source.substring(start, end); + if (whitespace.contains("\n")) { + return EMPTY; + } else { + return Space.build(source.substring(start, end), Collections.emptyList()); + } + } + return sourceBefore(";"); + } + if (t instanceof JCMethodDecl) { JCMethodDecl m = (JCMethodDecl) t; - return sourceBefore(m.body == null || m.defaultValue != null ? ";" : ""); + if (m.body == null || m.defaultValue != null) { + String suffix = source.substring(cursor, positionOfNext(";", null)); + int idx = findFirstNonWhitespaceChar(suffix); + return sourceBefore(idx >= 0 ? "" : ";"); + } else { + return sourceBefore(""); + } } return EMPTY; @@ -1724,6 +1813,10 @@ private List> convertStatements(@Nullable List> converted = new ArrayList<>(treesGroupedByStartPosition.size()); for (List treeGroup : treesGroupedByStartPosition.values()) { if (treeGroup.size() == 1) { + Tree t = treeGroup.get(0); + int startPosition = ((JCTree) t).getStartPosition(); + if (cursor > startPosition) + continue; converted.add(convert(treeGroup.get(0), suffix)); } else { // multi-variable declarations are split into independent overlapping JCVariableDecl's by the OpenJDK AST diff --git a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java index 25b78628c07..6dea46ebda4 100644 --- a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java +++ b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java @@ -31,9 +31,11 @@ import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.FileAttributes; +import org.openrewrite.PrintOutputCapture; import org.openrewrite.internal.EncodingDetectingInputStream; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaParsingException; +import org.openrewrite.java.JavaPrinter; import org.openrewrite.java.internal.JavaTypeCache; import org.openrewrite.java.marker.CompactConstructor; import org.openrewrite.java.marker.OmitParentheses; @@ -199,6 +201,16 @@ public J visitAssignment(AssignmentTree node, Space fmt) { typeMapping.type(node)); } + @Override + public J visitErroneous(ErroneousTree node, Space fmt) { + String erroneousNode = source.substring(((JCTree) node).getStartPosition(), ((JCTree) node).getEndPosition(endPosTable)); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode); + } + @Override public J visitBinary(BinaryTree node, Space fmt) { Expression left = convert(node.getLeftOperand()); @@ -1521,6 +1533,22 @@ public J visitUnary(UnaryTree node, Space fmt) { @Override public J visitVariable(VariableTree node, Space fmt) { + JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) node; + if ("".equals(jcVariableDecl.getName().toString())) { + int startPos = jcVariableDecl.getStartPosition(); + int endPos = jcVariableDecl.getEndPosition(endPosTable); + + if (startPos == endPos) { + endPos = startPos + 1; // For cases where the error node is a single character like "/" + } + String erroneousNode = source.substring(startPos, endPos); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode + ); + } return hasFlag(node.getModifiers(), Flags.ENUM) ? visitEnumVariable(node, fmt) : visitVariables(singletonList(node), fmt); // method arguments cannot be multi-declarations @@ -1732,11 +1760,45 @@ private void reportJavaParsingException(Throwable ex) { return null; } J2 j = convert(t); - JRightPadded rightPadded = new JRightPadded<>(j, suffix.apply(t), Markers.EMPTY); - cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + @SuppressWarnings("ConstantConditions") JRightPadded rightPadded = j == null ? null : + new JRightPadded<>(j, suffix.apply(t), Markers.EMPTY); + int idx = findFirstNonWhitespaceChar(rightPadded.getAfter().getWhitespace()); + if (idx >= 0) { + rightPadded = (JRightPadded) JRightPadded.build(getErroneous(List.of(rightPadded))); + } + // Cursor hasn't been updated but points at the end of erroneous node already + // This means that error node start position == end position + // Therefore ensure that cursor has moved to the end of erroneous node bu adding its length to the cursor + // Example `/pet` results in 2 erroeneous nodes: `/` and `pet`. The `/` node would have start and end position the + // same from the JC compiler. + if (endPos(t) == cursor && rightPadded.getElement() instanceof J.Erroneous) { + cursor += ((J.Erroneous) rightPadded.getElement()).getText().length(); + } else { + cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + } return rightPadded; } + private J.Erroneous getErroneous(List> converted) { + PrintOutputCapture p = new PrintOutputCapture<>(0); + new JavaPrinter<>().visitContainer(JContainer.build(EMPTY, converted, Markers.EMPTY), JContainer.Location.METHOD_INVOCATION_ARGUMENTS, p); + return new J.Erroneous( + org.openrewrite.Tree.randomId(), + EMPTY, + Markers.EMPTY, + p.getOut() + ); + } + + private static int findFirstNonWhitespaceChar(String s) { + for (int i = 0; i < s.length(); i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + return -1; + } + private long lineNumber(Tree tree) { return source.substring(0, ((JCTree) tree).getStartPosition()).chars().filter(c -> c == '\n').count() + 1; } @@ -1783,25 +1845,50 @@ private List> convertAll(List tr private Space statementDelim(@Nullable Tree t) { switch (t.getKind()) { + case CONTINUE: + case RETURN: + case BREAK: case ASSERT: case ASSIGNMENT: - case BREAK: - case CONTINUE: case DO_WHILE_LOOP: case IMPORT: case METHOD_INVOCATION: case NEW_CLASS: - case RETURN: case THROW: + return sourceBefore(";"); case EXPRESSION_STATEMENT: + ExpressionTree expTree = ((ExpressionStatementTree) t).getExpression(); + if (expTree instanceof ErroneousTree) { + return Space.build(source.substring(((JCTree) expTree).getEndPosition(endPosTable),((JCTree) t).getEndPosition(endPosTable)), Collections.emptyList()); + } else { + return sourceBefore(";"); + } case VARIABLE: + JCTree.JCVariableDecl varTree = (JCTree.JCVariableDecl) t; + if ("".contentEquals(varTree.getName())) { + int start = varTree.vartype.getEndPosition(endPosTable); + int end = varTree.getEndPosition(endPosTable); + String whitespace = source.substring(start, end); + if (whitespace.contains("\n")) { + return EMPTY; + } else { + return Space.build(source.substring(start, end), Collections.emptyList()); + } + } + return sourceBefore(";"); case YIELD: return sourceBefore(";"); case LABELED_STATEMENT: return statementDelim(((JCLabeledStatement) t).getStatement()); case METHOD: JCMethodDecl m = (JCMethodDecl) t; - return sourceBefore(m.body == null || m.defaultValue != null ? ";" : ""); + if (m.body == null || m.defaultValue != null) { + String suffix = source.substring(cursor, positionOfNext(";", null)); + int idx = findFirstNonWhitespaceChar(suffix); + return sourceBefore(idx >= 0 ? "" : ";"); + } else { + return sourceBefore(""); + } default: return t instanceof JCAssignOp || t instanceof JCUnary ? sourceBefore(";") : EMPTY; } @@ -1829,6 +1916,10 @@ private List> convertStatements(@Nullable List> converted = new ArrayList<>(treesGroupedByStartPosition.size()); for (List treeGroup : treesGroupedByStartPosition.values()) { if (treeGroup.size() == 1) { + Tree t = treeGroup.get(0); + int startPosition = ((JCTree) t).getStartPosition(); + if (cursor > startPosition) + continue; converted.add(convert(treeGroup.get(0), suffix)); } else { // multi-variable declarations are split into independent overlapping JCVariableDecl's by the OpenJDK AST diff --git a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java index a64eeab242f..8fd8d2809d9 100644 --- a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java +++ b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java @@ -30,9 +30,11 @@ import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.FileAttributes; +import org.openrewrite.PrintOutputCapture; import org.openrewrite.internal.EncodingDetectingInputStream; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaParsingException; +import org.openrewrite.java.JavaPrinter; import org.openrewrite.java.internal.JavaTypeCache; import org.openrewrite.java.marker.CompactConstructor; import org.openrewrite.java.marker.OmitParentheses; @@ -198,6 +200,16 @@ public J visitAssignment(AssignmentTree node, Space fmt) { typeMapping.type(node)); } + @Override + public J visitErroneous(ErroneousTree node, Space fmt) { + String erroneousNode = source.substring(((JCTree) node).getStartPosition(), ((JCTree) node).getEndPosition(endPosTable)); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode); + } + @Override public J visitBinary(BinaryTree node, Space fmt) { Expression left = convert(node.getLeftOperand()); @@ -1517,6 +1529,22 @@ public J visitUnary(UnaryTree node, Space fmt) { @Override public J visitVariable(VariableTree node, Space fmt) { + JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) node; + if ("".equals(jcVariableDecl.getName().toString())) { + int startPos = jcVariableDecl.getStartPosition(); + int endPos = jcVariableDecl.getEndPosition(endPosTable); + + if (startPos == endPos) { + endPos = startPos + 1; // For cases where the error node is a single character like "/" + } + String erroneousNode = source.substring(startPos, endPos); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode + ); + } return hasFlag(node.getModifiers(), Flags.ENUM) ? visitEnumVariable(node, fmt) : visitVariables(singletonList(node), fmt); // method arguments cannot be multi-declarations @@ -1707,10 +1735,43 @@ private JRightPadded convert(Tree t, Function su J2 j = convert(t); @SuppressWarnings("ConstantConditions") JRightPadded rightPadded = j == null ? null : new JRightPadded<>(j, suffix.apply(t), Markers.EMPTY); - cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + int idx = findFirstNonWhitespaceChar(rightPadded.getAfter().getWhitespace()); + if (idx >= 0) { + rightPadded = (JRightPadded) JRightPadded.build(getErroneous(List.of(rightPadded))); + } + // Cursor hasn't been updated but points at the end of erroneous node already + // This means that error node start position == end position + // Therefore ensure that cursor has moved to the end of erroneous node bu adding its length to the cursor + // Example `/pet` results in 2 erroeneous nodes: `/` and `pet`. The `/` node would have start and end position the + // same from the JC compiler. + if (endPos(t) == cursor && rightPadded.getElement() instanceof J.Erroneous) { + cursor += ((J.Erroneous) rightPadded.getElement()).getText().length(); + } else { + cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + } return rightPadded; } + private J.Erroneous getErroneous(List> converted) { + PrintOutputCapture p = new PrintOutputCapture<>(0); + new JavaPrinter<>().visitContainer(JContainer.build(EMPTY, converted, Markers.EMPTY), JContainer.Location.METHOD_INVOCATION_ARGUMENTS, p); + return new J.Erroneous( + org.openrewrite.Tree.randomId(), + EMPTY, + Markers.EMPTY, + p.getOut() + ); + } + + private static int findFirstNonWhitespaceChar(String s) { + for (int i = 0; i < s.length(); i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + return -1; + } + private long lineNumber(Tree tree) { return source.substring(0, ((JCTree) tree).getStartPosition()).chars().filter(c -> c == '\n').count() + 1; } @@ -1765,25 +1826,50 @@ private List> convertAll(List tr private Space statementDelim(@Nullable Tree t) { switch (t.getKind()) { + case CONTINUE: + case RETURN: + case BREAK: case ASSERT: case ASSIGNMENT: - case BREAK: - case CONTINUE: case DO_WHILE_LOOP: case IMPORT: case METHOD_INVOCATION: case NEW_CLASS: - case RETURN: case THROW: + return sourceBefore(";"); case EXPRESSION_STATEMENT: + ExpressionTree expTree = ((ExpressionStatementTree) t).getExpression(); + if (expTree instanceof ErroneousTree) { + return Space.build(source.substring(((JCTree) expTree).getEndPosition(endPosTable),((JCTree) t).getEndPosition(endPosTable)), Collections.emptyList()); + } else { + return sourceBefore(";"); + } case VARIABLE: + JCTree.JCVariableDecl varTree = (JCTree.JCVariableDecl) t; + if ("".contentEquals(varTree.getName())) { + int start = varTree.vartype.getEndPosition(endPosTable); + int end = varTree.getEndPosition(endPosTable); + String whitespace = source.substring(start, end); + if (whitespace.contains("\n")) { + return EMPTY; + } else { + return Space.build(source.substring(start, end), Collections.emptyList()); + } + } + return sourceBefore(";"); case YIELD: return sourceBefore(";"); case LABELED_STATEMENT: return statementDelim(((JCLabeledStatement) t).getStatement()); case METHOD: JCMethodDecl m = (JCMethodDecl) t; - return sourceBefore(m.body == null || m.defaultValue != null ? ";" : ""); + if (m.body == null || m.defaultValue != null) { + String suffix = source.substring(cursor, positionOfNext(";", null)); + int idx = findFirstNonWhitespaceChar(suffix); + return sourceBefore(idx >= 0 ? "" : ";"); + } else { + return sourceBefore(""); + } default: return t instanceof JCAssignOp || t instanceof JCUnary ? sourceBefore(";") : EMPTY; } @@ -1808,6 +1894,10 @@ private List> convertStatements(@Nullable List> converted = new ArrayList<>(treesGroupedByStartPosition.size()); for (List treeGroup : treesGroupedByStartPosition.values()) { if (treeGroup.size() == 1) { + Tree t = treeGroup.get(0); + int startPosition = ((JCTree) t).getStartPosition(); + if (cursor > startPosition) + continue; converted.add(convert(treeGroup.get(0), suffix)); } else { // multi-variable declarations are split into independent overlapping JCVariableDecl's by the OpenJDK AST diff --git a/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8ParserVisitor.java b/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8ParserVisitor.java index b07b7f18999..e9123f68062 100644 --- a/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8ParserVisitor.java +++ b/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8ParserVisitor.java @@ -29,9 +29,11 @@ import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.FileAttributes; +import org.openrewrite.PrintOutputCapture; import org.openrewrite.internal.EncodingDetectingInputStream; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.internal.JavaTypeCache; +import org.openrewrite.java.JavaPrinter; import org.openrewrite.java.marker.OmitParentheses; import org.openrewrite.java.tree.*; import org.openrewrite.marker.Markers; @@ -189,6 +191,16 @@ public J visitAssignment(AssignmentTree node, Space fmt) { typeMapping.type(node)); } + @Override + public J visitErroneous(ErroneousTree node, Space fmt) { + String erroneousNode = source.substring(((JCTree) node).getStartPosition(), ((JCTree) node).getEndPosition(endPosTable)); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode); + } + @Override public J visitBinary(BinaryTree node, Space fmt) { Expression left = convert(node.getLeftOperand()); @@ -1433,6 +1445,22 @@ public J visitUnary(UnaryTree node, Space fmt) { @Override public J visitVariable(VariableTree node, Space fmt) { + JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) node; + if ("".equals(jcVariableDecl.getName().toString())) { + int startPos = jcVariableDecl.getStartPosition(); + int endPos = jcVariableDecl.getEndPosition(endPosTable); + + if (startPos == endPos) { + endPos = startPos + 1; + } + String erroneousNode = source.substring(startPos, endPos); + return new J.Erroneous( + randomId(), + fmt, + Markers.EMPTY, + erroneousNode + ); + } return hasFlag(node.getModifiers(), Flags.ENUM) ? visitEnumVariable(node, fmt) : visitVariables(singletonList(node), fmt); // method arguments cannot be multi-declarations @@ -1606,10 +1634,43 @@ private JRightPadded convert(Tree t, Function su J2 j = convert(t); @SuppressWarnings("ConstantConditions") JRightPadded rightPadded = j == null ? null : new JRightPadded<>(j, suffix.apply(t), Markers.EMPTY); - cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + int idx = findFirstNonWhitespaceChar(rightPadded.getAfter().getWhitespace()); + if (idx >= 0) { + rightPadded = (JRightPadded) JRightPadded.build(getErroneous(Collections.singletonList(rightPadded))); + } + // Cursor hasn't been updated but points at the end of erroneous node already + // This means that error node start position == end position + // Therefore ensure that cursor has moved to the end of erroneous node bu adding its length to the cursor + // Example `/pet` results in 2 erroeneous nodes: `/` and `pet`. The `/` node would have start and end position the + // same from the JC compiler. + if (endPos(t) == cursor && rightPadded.getElement() instanceof J.Erroneous) { + cursor += ((J.Erroneous) rightPadded.getElement()).getText().length(); + } else { + cursor(max(endPos(t), cursor)); // if there is a non-empty suffix, the cursor may have already moved past it + } return rightPadded; } + private J.Erroneous getErroneous(List> converted) { + PrintOutputCapture p = new PrintOutputCapture<>(0); + new JavaPrinter<>().visitContainer(JContainer.build(EMPTY, converted, Markers.EMPTY), JContainer.Location.METHOD_INVOCATION_ARGUMENTS, p); + return new J.Erroneous( + org.openrewrite.Tree.randomId(), + EMPTY, + Markers.EMPTY, + p.getOut() + ); + } + + private static int findFirstNonWhitespaceChar(String s) { + for (int i = 0; i < s.length(); i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + return -1; + } + private long lineNumber(Tree tree) { int lineNumber = 1; for (char c : source.substring(0, ((JCTree) tree).getStartPosition()).toCharArray()) { @@ -1680,9 +1741,7 @@ private Space statementDelim(@Nullable Tree t) { t instanceof JCNewClass || t instanceof JCReturn || t instanceof JCThrow || - t instanceof JCUnary || - t instanceof JCExpressionStatement || - t instanceof JCVariableDecl) { + t instanceof JCUnary) { return sourceBefore(";"); } @@ -1690,9 +1749,39 @@ private Space statementDelim(@Nullable Tree t) { return statementDelim(((JCLabeledStatement) t).getStatement()); } + if (t instanceof JCExpressionStatement) { + ExpressionTree expTree = ((ExpressionStatementTree) t).getExpression(); + if (expTree instanceof ErroneousTree) { + return Space.build(source.substring(((JCTree) expTree).getEndPosition(endPosTable),((JCTree) t).getEndPosition(endPosTable)), Collections.emptyList()); + } else { + return sourceBefore(";"); + } + } + + if (t instanceof JCVariableDecl) { + JCTree.JCVariableDecl varTree = (JCTree.JCVariableDecl) t; + if ("".contentEquals(varTree.getName())) { + int start = varTree.vartype.getEndPosition(endPosTable); + int end = varTree.getEndPosition(endPosTable); + String whitespace = source.substring(start, end); + if (whitespace.contains("\n")) { + return EMPTY; + } else { + return Space.build(source.substring(start, end), Collections.emptyList()); + } + } + return sourceBefore(";"); + } + if (t instanceof JCMethodDecl) { JCMethodDecl m = (JCMethodDecl) t; - return sourceBefore(m.body == null || m.defaultValue != null ? ";" : ""); + if (m.body == null || m.defaultValue != null) { + String suffix = source.substring(cursor, positionOfNext(";", null)); + int idx = findFirstNonWhitespaceChar(suffix); + return sourceBefore(idx >= 0 ? "" : ";"); + } else { + return sourceBefore(""); + } } return EMPTY; @@ -1717,6 +1806,10 @@ private List> convertStatements(@Nullable List> converted = new ArrayList<>(treesGroupedByStartPosition.size()); for (List treeGroup : treesGroupedByStartPosition.values()) { if (treeGroup.size() == 1) { + Tree t = treeGroup.get(0); + int startPosition = ((JCTree) t).getStartPosition(); + if (cursor > startPosition) + continue; converted.add(convert(treeGroup.get(0), suffix)); } else { // multi-variable declarations are split into independent overlapping JCVariableDecl's by the OpenJDK AST diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java index 3115386c7d7..4314094918f 100644 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java @@ -25,6 +25,7 @@ import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.Issue; import org.openrewrite.SourceFile; +import org.openrewrite.java.search.FindCompileErrors; import org.openrewrite.java.tree.J; import org.openrewrite.test.RewriteTest; @@ -178,6 +179,130 @@ void moduleInfo() { assertFalse(JavaParser.fromJavaVersion().build().accept(Path.of("src/main/java/foo/module-info.java"))); } + @ParameterizedTest + //language=java + @ValueSource(strings = { + """ + package com.example.demo; + class FooBar { + public void test() { + ownerR + } + } + """, + """ + package com.example.demo; + class FooBar { + public void test(int num string msg) { + String a; this.ownerR + System.out.println(); + } + } + """, + """ + package com.example.demo; + class FooBar { + public void test(int num string s, int b) { + String a; this.ownerR + System.out.println(); + } + } + """, + """ + package com.example.demo; + class FooBar { + public void test(int num) { + String a; this.ownerR // comment + System.out.println(); + } + } + """, + """ + package com.example.demo; + class FooBar { + public void test(int num) { + // comment + this.ownerR + } + } + """, + """ + package com.example.demo; + class FooBar { + public void test(int param ) { + this.ownerR + // comment + } + } + """ + }) + void erroneousExpressionStatements(@Language("java") String source) { + rewriteRun( + java(source) + ); + } + + @Test + void erroneousVariableDeclarations() { + rewriteRun( + spec -> spec.recipe(new FindCompileErrors()), + java( + """ + package com.example.demo; + class Foo { + /pet + public void test() { + } + } + """, + """ + package com.example.demo; + class Foo { + /*~~>*///*~~>*/pet + public void test() { + } + } + """ + ), + java( + """ + package com.example.demo; + class Bar { + pet + public void test() { + } + } + """, + """ + package com.example.demo; + class Bar { + /*~~>*/pet + public void test() { + } + } + """ + ), + java( + """ + package com.example.demo; + class Baz { + -pet + public void test() { + } + } + """, + """ + package com.example.demo; + class Baz { + /*~~>*/-/*~~>*/pet + public void test() { + } + } + """ + ) + ); + } + @Test @Issue("https://github.com/openrewrite/rewrite/pull/4624") void shouldParseComments() { diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaVisitorTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaVisitorTest.java index 8c3e7fc9e22..de0f9daa151 100644 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaVisitorTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaVisitorTest.java @@ -132,4 +132,40 @@ public void method2() { """) ); } + + @Test + void javaVisitorHandlesErroneousNodes() { + rewriteRun( + spec -> spec + .expectedCyclesThatMakeChanges(2) + .recipes( + toRecipe(() -> new JavaIsoVisitor<>() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext p) { + if (method.getSimpleName().equals("test")) { + return JavaTemplate.builder("Exception").contextSensitive().build() + .apply(getCursor(), method.getCoordinates().replaceThrows()); + } + return method; + } + }) + ), + java( + """ + class A { + void test() { + owner + } + } + """, + """ + class A { + void test() throws Exception { + owner + } + } + """ + ) + ); + } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ChangeStaticFieldToMethod.java b/rewrite-java/src/main/java/org/openrewrite/java/ChangeStaticFieldToMethod.java index d4c278783cc..6155a3b433d 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/ChangeStaticFieldToMethod.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/ChangeStaticFieldToMethod.java @@ -90,9 +90,9 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ct @Override public J visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { if (getCursor().firstEnclosing(J.Import.class) == null && - TypeUtils.isOfClassType(fieldAccess.getTarget().getType(), oldClassName) && - fieldAccess.getSimpleName().equals(oldFieldName)) { - return useNewMethod(fieldAccess); + TypeUtils.isOfClassType(fieldAccess.getTarget().getType(), oldClassName) && + fieldAccess.getSimpleName().equals(oldFieldName)) { + return useNewMethod(fieldAccess, fieldAccess.getCoordinates().replace()); } return super.visitFieldAccess(fieldAccess, ctx); } @@ -101,43 +101,18 @@ public J visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) { public J visitIdentifier(J.Identifier ident, ExecutionContext ctx) { JavaType.Variable varType = ident.getFieldType(); if (varType != null && - TypeUtils.isOfClassType(varType.getOwner(), oldClassName) && - varType.getName().equals(oldFieldName)) { - return useNewMethod(ident); + TypeUtils.isOfClassType(varType.getOwner(), oldClassName) && + varType.getName().equals(oldFieldName)) { + return useNewMethod(ident, ident.getCoordinates().replace()); } return ident; } - private J useNewMethod(TypeTree tree) { + private J useNewMethod(TypeTree tree, JavaCoordinates coordinates) { String newClass = newClassName == null ? oldClassName : newClassName; - maybeRemoveImport(oldClassName); maybeAddImport(newClass); - - Cursor statementCursor = getCursor().dropParentUntil(Statement.class::isInstance); - Statement statement = statementCursor.getValue(); - J applied = makeNewMethod(newClass).apply(statementCursor, statement.getCoordinates().replace()); - - J.MethodInvocation method = null; - if (applied instanceof J.Block) { - J.Block block = (J.Block) applied; - method = block.getStatements().get(0).withPrefix(tree.getPrefix()); - } else if (applied instanceof J.NewArray) { - J.NewArray newArray = (J.NewArray) applied; - method = (J.MethodInvocation) newArray.getInitializer().get(0); - } - if (method == null || method.getMethodType() == null) { - throw new IllegalArgumentException("Error while changing a static field to a method. The generated template using a the new class [" + - newClass + "] and the method [" + newMethodName + "] resulted in a null method type."); - } - if (tree.getType() != null) { - JavaType.Method mt = method.getMethodType().withReturnType(tree.getType()); - method = method.withMethodType(mt); - if (method.getName().getType() != null) { - method = method.withName(method.getName().withType(mt)); - } - } - return method; + return makeNewMethod(newClass).apply(getCursor(), coordinates); } @NonNull @@ -145,7 +120,8 @@ private JavaTemplate makeNewMethod(String newClass) { String packageName = StringUtils.substringBeforeLast(newClass, "."); String simpleClassName = StringUtils.substringAfterLast(newClass, "."); - String methodInvocationTemplate = "{" + simpleClassName + (newTarget != null ? "." + newTarget + "." : ".") + newMethodName + "();}"; + + String methodInvocationTemplate = simpleClassName + (newTarget != null ? "." + newTarget + "." : ".") + newMethodName + "()"; @Language("java") String methodStub; if (newTarget == null) { diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaIsoVisitor.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaIsoVisitor.java index b15951d90e8..99234db3a4e 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaIsoVisitor.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaIsoVisitor.java @@ -364,4 +364,10 @@ public J.Wildcard visitWildcard(J.Wildcard wildcard, P p) { public J.Yield visitYield(J.Yield yield, P p) { return (J.Yield) super.visitYield(yield, p); } + + @Override + public J.Erroneous visitErroneous(J.Erroneous erroneous, P p) { + return (J.Erroneous) super.visitErroneous(erroneous, p); + } + } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java index f6b8081eca3..b75e24d02c3 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java @@ -18,6 +18,7 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.Cursor; import org.openrewrite.PrintOutputCapture; +import org.openrewrite.Tree; import org.openrewrite.java.marker.CompactConstructor; import org.openrewrite.java.marker.OmitParentheses; import org.openrewrite.java.tree.*; @@ -27,6 +28,7 @@ import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; public class JavaPrinter

extends JavaVisitor> { @@ -414,7 +416,8 @@ protected void printStatementTerminator(Statement s, PrintOutputCapture

p) { } if (s instanceof MethodDeclaration && ((MethodDeclaration) s).getBody() == null) { - p.append(';'); + if (!hasError(s)) + p.append(';'); return; } @@ -445,6 +448,19 @@ protected void printStatementTerminator(Statement s, PrintOutputCapture

p) { } } + private static boolean hasError(Tree tree) { + AtomicBoolean isError = new AtomicBoolean(false); + new JavaIsoVisitor() { + + @Override + public Erroneous visitErroneous(Erroneous erroneous, AtomicBoolean atomicBoolean) { + atomicBoolean.set(true); + return erroneous; + } + }.visit(tree, isError); + return isError.get(); + } + @Override public J visitBreak(Break breakStatement, PrintOutputCapture

p) { beforeSyntax(breakStatement, Space.Location.BREAK_PREFIX, p); @@ -1196,6 +1212,14 @@ public J visitYield(Yield yield, PrintOutputCapture

p) { return yield; } + @Override + public J visitErroneous(Erroneous error, PrintOutputCapture

p) { + beforeSyntax(error, Space.Location.ERRONEOUS, p); + p.append(error.getText()); + afterSyntax(error, p); + return error; + } + private static final UnaryOperator JAVA_MARKER_WRAPPER = out -> "/*~~" + out + (out.isEmpty() ? "" : "~~") + ">*/"; diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaVisitor.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaVisitor.java index fe218390396..eec6aeb187d 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaVisitor.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaVisitor.java @@ -1423,6 +1423,13 @@ public J visitYield(J.Yield yield, P p) { JContainer.build(before, js, container.getMarkers()); } + public J visitErroneous(J.Erroneous erroneous, P p) { + J.Erroneous u = erroneous; + u = u.withPrefix(visitSpace(u.getPrefix(), Space.Location.ERRONEOUS, p)); + u = u.withMarkers(visitMarkers(u.getMarkers(), p)); + return u; + } + /** * Check if a child AST element is in the same lexical scope as that of the AST element associated with the base * cursor. (i.e.: Are the variables and declarations visible in the base scope also visible to the child AST diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java index e39d37b01ea..f586b011fcb 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java @@ -466,7 +466,8 @@ private void contextTemplate(Cursor cursor, J prior, StringBuilder before, Strin J firstEnclosing = cursor.getParentOrThrow().firstEnclosing(J.class); if (m.getArguments().stream().anyMatch(arg -> referToSameElement(prior, arg))) { before.insert(0, "__M__.any("); - if (firstEnclosing instanceof J.Block || firstEnclosing instanceof J.Case) { + if (firstEnclosing instanceof J.Block || firstEnclosing instanceof J.Case || + firstEnclosing instanceof J.If || firstEnclosing instanceof J.If.Else) { after.append(");"); } else { after.append(")"); diff --git a/rewrite-java/src/main/java/org/openrewrite/java/search/FindCompileErrors.java b/rewrite-java/src/main/java/org/openrewrite/java/search/FindCompileErrors.java new file mode 100644 index 00000000000..21fd93439c2 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/search/FindCompileErrors.java @@ -0,0 +1,56 @@ +/* + * 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.java.search; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.table.CompileErrors; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +public class FindCompileErrors extends Recipe { + + transient CompileErrors report = new CompileErrors(this); + + @Override + public String getDisplayName() { + return "Find compile errors"; + } + + @Override + public String getDescription() { + return "Compile errors result in a particular LST structure that can be searched for."; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.Erroneous visitErroneous(J.Erroneous erroneous, ExecutionContext ctx) { + J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class); + String sourceFile = cu != null ? cu.getSourcePath().toString() : "Unknown source file"; + String code = erroneous.print(getCursor()); + report.insertRow(ctx, new CompileErrors.Row( + sourceFile, + code + )); + return SearchResult.found(erroneous); + } + }; + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/table/CompileErrors.java b/rewrite-java/src/main/java/org/openrewrite/java/table/CompileErrors.java new file mode 100644 index 00000000000..ff39951e875 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/table/CompileErrors.java @@ -0,0 +1,40 @@ +/* + * 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.java.table; + +import lombok.Value; +import org.openrewrite.Column; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +public class CompileErrors extends DataTable { + public CompileErrors(Recipe recipe) { + super(recipe, + "Compile errors", + "The source code of compile errors."); + } + + @Value + public static class Row { + @Column(displayName = "Source file", + description = "The source file that the method call occurred in.") + String sourceFile; + + @Column(displayName = "Source", + description = "The source code of the type use.") + String code; + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java index 1ad1cee2193..417db943f5f 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java @@ -6268,4 +6268,52 @@ public

J acceptJava(JavaVisitor

v, P p) { } } } + + /** + * A node that represents an erroneous element. + */ + @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + @Data + @With + final class Erroneous implements Statement, Expression { + @With + @EqualsAndHashCode.Include + UUID id; + + @With + Space prefix; + + @With + Markers markers; + + @With + String text; + + @Override + public

J acceptJava(JavaVisitor

v, P p) { + return v.visitErroneous(this, p); + } + + @Override + public @Nullable JavaType getType() { + return JavaType.Unknown.getInstance(); + } + + @Override + public T withType(@Nullable JavaType type) { + return (T) this; + } + + @Override + @Transient + public CoordinateBuilder.Statement getCoordinates() { + return new CoordinateBuilder.Statement(this); + } + + @Override + public String toString() { + return withPrefix(Space.EMPTY).printTrimmed(new JavaPrinter<>()); + } + } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/Space.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/Space.java index 834fb6b6800..fc40698f5d9 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/Space.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/Space.java @@ -344,6 +344,7 @@ public enum Location { ENUM_VALUE_PREFIX, ENUM_VALUE_SET_PREFIX, ENUM_VALUE_SUFFIX, + ERRONEOUS, EXPRESSION_PREFIX, EXTENDS, FIELD_ACCESS_NAME, diff --git a/rewrite-java/src/test/java/org/openrewrite/java/search/FindCompileErrorsTest.java b/rewrite-java/src/test/java/org/openrewrite/java/search/FindCompileErrorsTest.java new file mode 100644 index 00000000000..168e88ee0ec --- /dev/null +++ b/rewrite-java/src/test/java/org/openrewrite/java/search/FindCompileErrorsTest.java @@ -0,0 +1,61 @@ +/* + * 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.java.search; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.table.CompileErrors; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.java.Assertions.java; + +class FindCompileErrorsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipes(new FindCompileErrors()); + } + + @DocumentExample + @Test + void javaVisitorHandlesErroneousNodes() { + rewriteRun( + spec -> spec.dataTable(CompileErrors.Row.class, + rows -> assertThat(rows).singleElement().satisfies(row -> { + assertThat(row.getSourceFile()).isEqualTo("A.java"); + assertThat(row.getCode()).isEqualTo("\n owner"); + })), + java( + """ + class A { + void test() { + owner + } + } + """, + """ + class A { + void test() { + /*~~>*/owner + } + } + """ + ) + ); + } +}