All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.java.AutoFormat Maven / Gradle / Ivy

/*
 * Copyright 2020 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; import org.openrewrite.*; import org.openrewrite.internal.StringUtils; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.Statement; import org.openrewrite.refactor.Formatter; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Arrays.stream; import static java.util.stream.IntStream.range; import static org.openrewrite.internal.StringUtils.splitCStyleComments; /** * A general purpose means of formatting arbitrarily complex blocks of code relative based on their * surrounding context. *

* TODO when complete, this should replace {@link ShiftFormatRightVisitor}. */ @Incubating(since = "2.1.0") public class AutoFormat extends JavaIsoRefactorVisitor { private final J[] scope; public AutoFormat(J... scope) { this.scope = scope; } @Override public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu) { andThen(new FixNewlines()); andThen(new FixIndentation()); return super.visitCompilationUnit(cu); } @Override public boolean isIdempotent() { return false; } private class FixNewlines extends JavaIsoRefactorVisitor { FixNewlines() { setCursoringOn(); } /** * Given a single line string with a whitespace prefix and a non-whitespace suffix, alter the whitespace prefix * to have the appropriate indentation. * * Right now comments aren't AST elements so FixIndentation.visitTree() is blind to them. * This special treatment of comment indentation will be able to go away once we start treating * comments as their own, independent AST element. */ public String indentLine(String prefix) { if (!prefix.isEmpty() && stream(scope).anyMatch(s -> getCursor().isScopeInPath(s))) { int indentMultiple = (int) getCursor().getPathAsStream().filter(J.Block.class::isInstance).count(); Formatter.Result wholeSourceIndent = formatter.wholeSourceIndent(); int nonWhiteSpaceIndex = StringUtils.indexOfNonWhitespace(prefix); boolean insideJavaDocComment = false; if(nonWhiteSpaceIndex == -1) { nonWhiteSpaceIndex = prefix.length(); } else if(prefix.charAt(nonWhiteSpaceIndex) == '*') { insideJavaDocComment = true; } String indentation = prefix.substring(0, nonWhiteSpaceIndex); String comment = prefix.substring(nonWhiteSpaceIndex); // +1 because for String.substring(start, end) start is inclusive, but end is exclusive String newIndentation = indentation.substring(0, indentation.lastIndexOf('\n') + 1) + range(0, indentMultiple * wholeSourceIndent.getIndentToUse()) .mapToObj(n -> wholeSourceIndent.isIndentedWithSpaces() ? " " : "\t") .collect(Collectors.joining("")); if(insideJavaDocComment) //noinspection DanglingJavadoc { // By convention Javadoc-style comments are indented such that lines beginning with '*' // have an extra space so as to vertically align the '*' characters from different lines. e.g.: /** * */ newIndentation += " "; } return newIndentation + comment; } return prefix; } /** * Ensure that the element in question is indented the right amount */ private T spaceHorizontally(T j) { if(stream(scope).anyMatch(s -> getCursor().isScopeInPath(s))) { Formatting originalFormatting = j.getFormatting(); // Ensure that comments are indented correctly String newPrefix = stream(originalFormatting.getPrefix().split("\\n")) .map(this::indentLine) .collect(Collectors.joining("\n")); if(originalFormatting.getPrefix().endsWith("\n")) { // split() will eliminate a trailing newline, put it back newPrefix += '\n'; } j = j.withFormatting(originalFormatting.withPrefix(newPrefix)); } return j; } /** * Ensure there's a single blank line between the previous declaration and the current j * Ensure that any comments are on their own line */ private T spaceVertically(T j) { Formatting originalFormatting = j.getFormatting(); List splitPrefix = splitCStyleComments(originalFormatting.getPrefix()); String newPrefix = Stream.concat( Stream.of(splitPrefix.get(0)) .map(it -> StringUtils.ensureNewlineCountBeforeComment(it, 2)), splitPrefix.stream().skip(1) .map(it -> StringUtils.ensureNewlineCountBeforeComment(it, 1)) ) .collect(Collectors.joining()); return j.withFormatting(originalFormatting.withPrefix(newPrefix)); } @Override public J.MethodDecl visitMethod(J.MethodDecl methodDecl) { J.MethodDecl m = super.visitMethod(methodDecl); if(stream(scope).anyMatch(s -> getCursor().isScopeInPath(s))) { m = spaceHorizontally(m); m = spaceVertically(m); // Annotations should each appear on their own line List annotations = new ArrayList<>(m.getAnnotations()); if(!annotations.isEmpty()) { // ensure all annotations except the first have a \n in their prefixes // The first annotation doesn't need a \n since the prefix of the MethodDecl itself should contain that \n for(int i = 1; i < annotations.size(); i++) { if(!annotations.get(i).getPrefix().contains("\n")) { annotations.set(i, annotations.get(i).withPrefix("\n")); } } m = m.withAnnotations(annotations); List modifiers = new ArrayList<>(m.getModifiers()); // ensure first modifier following annotations has \n in prefix if(!modifiers.isEmpty()) { if(!modifiers.get(0).getPrefix().contains("\n")) { modifiers.set(0, modifiers.get(0).withPrefix("\n")); m = m.withModifiers(modifiers); } } // typeParameters else if(m.getTypeParameters() != null) { if(!m.getTypeParameters().getPrefix().contains("\n")) { m = m.withTypeParameters(m.getTypeParameters().withPrefix("\n")); } } // returnTypeExpr else if(m.getReturnTypeExpr() != null) { if(!m.getReturnTypeExpr().getPrefix().contains("\n")) { m = m.withReturnTypeExpr(m.getReturnTypeExpr().withPrefix("\n")); } } // name else if(!m.getName().getPrefix().contains("\n")) { m = m.withName(m.getName().withPrefix("\n")); } } } return m; } @Override public J.ClassDecl visitClassDecl(J.ClassDecl classDecl) { J.ClassDecl cd = super.visitClassDecl(classDecl); if(stream(scope).anyMatch(s -> getCursor().isScopeInPath(s))) { cd = spaceHorizontally(cd); cd = spaceVertically(cd); // check annotations formatting List annotations = new ArrayList<>(cd.getAnnotations()); if (!annotations.isEmpty()) { if(annotations.get(0).getFormatting().getPrefix().contains("\n")) { annotations.set(0, annotations.get(0).withPrefix("")); } // Ensure all annotations have a \n in their prefixes // The first annotation is skipped because the whitespace prior to it is stored in the formatting for ClassDecl for (int i = 1; i < annotations.size(); i++) { if (!annotations.get(i).getPrefix().contains("\n")) { annotations.set(i, annotations.get(i).withPrefix("\n")); } } cd = cd.withAnnotations(annotations); // ensure first statement following annotations has \n in prefix List modifiers = new ArrayList<>(cd.getModifiers()); if (!modifiers.isEmpty()) { if (!modifiers.get(0).getPrefix().contains("\n")) { modifiers.set(0, modifiers.get(0).withPrefix("\n")); cd = cd.withModifiers(modifiers); } } else if (!cd.getKind().getPrefix().contains("\n")) { cd = cd.withKind(cd.getKind().withPrefix("\n")); } } } return cd; } } private class FixIndentation extends JavaIsoRefactorVisitor { FixIndentation() { setCursoringOn(); } @Override public J reduce(J r1, J r2) { J j = super.reduce(r1, r2); if (r2 != null && r2.getPrefix().startsWith("|")) { j = j.withPrefix(r2.getPrefix().substring(1)); } return j; } @Override public J visitTree(Tree tree) { J j = super.visitTree(tree); String prefix = tree.getPrefix(); if (prefix.contains("\n") && stream(scope).anyMatch(s -> getCursor().isScopeInPath(s))) { int indentMultiple = (int) getCursor().getPathAsStream().filter(J.Block.class::isInstance).count(); if(tree instanceof J.Block.End) { indentMultiple--; } Formatter.Result wholeSourceIndent = formatter.wholeSourceIndent(); String shiftedPrefix = "|" + prefix.substring(0, prefix.lastIndexOf('\n') + 1) + range(0, indentMultiple * wholeSourceIndent.getIndentToUse()) .mapToObj(n -> wholeSourceIndent.isIndentedWithSpaces() ? " " : "\t") .collect(Collectors.joining("")); if (!shiftedPrefix.equals(prefix)) { j = j.withPrefix(shiftedPrefix); } } return j; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy