/*
* 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.format;
import org.jspecify.annotations.Nullable;
import org.openrewrite.Cursor;
import org.openrewrite.Tree;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.style.TabsAndIndentsStyle;
import org.openrewrite.java.tree.*;
import java.util.Iterator;
import java.util.List;
public class TabsAndIndentsVisitor
extends JavaIsoVisitor
{
@Nullable
private final Tree stopAfter;
private final TabsAndIndentsStyle style;
public TabsAndIndentsVisitor(TabsAndIndentsStyle style) {
this(style, null);
}
public TabsAndIndentsVisitor(TabsAndIndentsStyle style, @Nullable Tree stopAfter) {
this.style = style;
this.stopAfter = stopAfter;
}
@Override
public @Nullable J visit(@Nullable Tree tree, P p, Cursor parent) {
setCursor(parent);
for (Cursor c = parent; c != null; c = c.getParent()) {
Object v = c.getValue();
Space space = null;
if (v instanceof J) {
space = ((J) v).getPrefix();
} else if (v instanceof JRightPadded) {
space = ((JRightPadded>) v).getAfter();
} else if (v instanceof JLeftPadded) {
space = ((JLeftPadded>) v).getBefore();
} else if (v instanceof JContainer) {
space = ((JContainer>) v).getBefore();
}
if (space != null && space.getLastWhitespace().contains("\n")) {
int indent = findIndent(space);
if (indent != 0) {
c.putMessage("lastIndent", indent);
}
}
}
Iterator itr = parent.getPath(J.class::isInstance);
J next = (itr.hasNext()) ? (J) itr.next() : null;
if (next != null) {
preVisit(next, p);
}
return visit(tree, p);
}
@Override
public @Nullable J preVisit(@Nullable J tree, P p) {
if (tree instanceof JavaSourceFile ||
tree instanceof J.Package ||
tree instanceof J.Import ||
tree instanceof J.Label ||
tree instanceof J.DoWhileLoop ||
tree instanceof J.ArrayDimension ||
tree instanceof J.ClassDeclaration) {
getCursor().putMessage("indentType", IndentType.ALIGN);
} else if (tree instanceof J.Block ||
tree instanceof J.If ||
tree instanceof J.If.Else ||
tree instanceof J.ForLoop ||
tree instanceof J.ForEachLoop ||
tree instanceof J.WhileLoop ||
tree instanceof J.Case ||
tree instanceof J.EnumValueSet) {
getCursor().putMessage("indentType", IndentType.INDENT);
} else {
getCursor().putMessage("indentType", IndentType.CONTINUATION_INDENT);
}
return tree;
}
@Override
public J.ForLoop.Control visitForControl(J.ForLoop.Control control, P p) {
// FIXME fix formatting of control sections
return control;
}
@Override
public J.ForEachLoop.Control visitForEachControl(J.ForEachLoop.Control control, P p) {
// FIXME fix formatting of control sections
return control;
}
@Override
public Space visitSpace(Space space, Space.Location loc, P p) {
getCursor().putMessage("lastLocation", loc);
boolean alignToAnnotation = false;
Cursor parent = getCursor().getParent();
if (parent != null && parent.getValue() instanceof J.Annotation) {
parent.getParentOrThrow().putMessage("afterAnnotation", true);
} else if (parent != null && !getCursor().getParentOrThrow().getPath(J.Annotation.class::isInstance).hasNext()) {
// when annotations are on their own line, other parts of the declaration that follow are aligned left to it
alignToAnnotation = getCursor().pollNearestMessage("afterAnnotation") != null &&
!(getCursor().getParentOrThrow().getValue() instanceof J.Annotation);
}
if (space.getComments().isEmpty() && !space.getLastWhitespace().contains("\n") || parent == null) {
return space;
}
if (loc == Space.Location.METHOD_SELECT_SUFFIX) {
Integer chainedIndent = getCursor().getParentTreeCursor().getMessage("chainedIndent");
if (chainedIndent != null) {
getCursor().getParentTreeCursor().putMessage("lastIndent", chainedIndent);
return indentTo(space, chainedIndent, loc);
}
}
int indent = getCursor().getNearestMessage("lastIndent", 0);
IndentType indentType = getCursor().getParentOrThrow().getNearestMessage("indentType", IndentType.ALIGN);
// block spaces are always aligned to their parent
Object value = getCursor().getValue();
boolean alignBlockPrefixToParent = loc.equals(Space.Location.BLOCK_PREFIX) && space.getWhitespace().contains("\n") &&
// ignore init blocks.
(value instanceof J.Block && !(getCursor().getParentTreeCursor().getValue() instanceof J.Block));
boolean alignBlockToParent = loc.equals(Space.Location.BLOCK_END) ||
loc.equals(Space.Location.NEW_ARRAY_INITIALIZER_SUFFIX) ||
loc.equals(Space.Location.CATCH_PREFIX) ||
loc.equals(Space.Location.TRY_FINALLY) ||
loc.equals(Space.Location.ELSE_PREFIX);
if ((loc.equals(Space.Location.EXTENDS) && space.getWhitespace().contains("\n")) ||
Space.Location.EXTENDS.equals(getCursor().getParent().getMessage("lastLocation"))) {
indentType = IndentType.CONTINUATION_INDENT;
}
if (alignBlockPrefixToParent || alignBlockToParent || alignToAnnotation) {
indentType = IndentType.ALIGN;
}
switch (indentType) {
case ALIGN:
break;
case INDENT:
indent += style.getIndentSize();
break;
case CONTINUATION_INDENT:
indent += style.getContinuationIndent();
break;
}
Space s = indentTo(space, indent, loc);
if (value instanceof J && !(value instanceof J.EnumValueSet)) {
getCursor().putMessage("lastIndent", indent);
} else if (loc == Space.Location.METHOD_SELECT_SUFFIX) {
getCursor().getParentTreeCursor().putMessage("lastIndent", indent);
}
return s;
}
@Override
public JRightPadded visitRightPadded(@Nullable JRightPadded right, JRightPadded.Location loc, P p) {
if (right == null) {
//noinspection ConstantConditions
return null;
}
setCursor(new Cursor(getCursor(), right));
T t = right.getElement();
Space after;
int indent = getCursor().getNearestMessage("lastIndent", 0);
if (right.getElement() instanceof J) {
J elem = (J) right.getElement();
if ((right.getAfter().getLastWhitespace().contains("\n") ||
elem.getPrefix().getLastWhitespace().contains("\n"))) {
switch (loc) {
case FOR_CONDITION:
case FOR_UPDATE: {
J.ForLoop.Control control = getCursor().getParentOrThrow().getValue();
Space initPrefix = Space.firstPrefix(control.getInit());
if (!initPrefix.getLastWhitespace().contains("\n")) {
int initIndent = forInitColumn();
getCursor().getParentOrThrow().putMessage("lastIndent", initIndent - style.getContinuationIndent());
elem = visitAndCast(elem, p);
getCursor().getParentOrThrow().putMessage("lastIndent", indent);
after = indentTo(right.getAfter(), initIndent, loc.getAfterLocation());
} else {
elem = visitAndCast(elem, p);
after = visitSpace(right.getAfter(), loc.getAfterLocation(), p);
}
break;
}
case METHOD_DECLARATION_PARAMETER:
case RECORD_STATE_VECTOR: {
if (elem instanceof J.Empty) {
elem = elem.withPrefix(indentTo(elem.getPrefix(), indent, loc.getAfterLocation()));
after = right.getAfter();
break;
}
JContainer container = getCursor().getParentOrThrow().getValue();
List elements = container.getElements();
J firstArg = elements.iterator().next();
J lastArg = elements.get(elements.size() - 1);
if (style.getMethodDeclarationParameters().getAlignWhenMultiple()) {
J.MethodDeclaration method = getCursor().firstEnclosing(J.MethodDeclaration.class);
if (method != null) {
int alignTo;
if (firstArg.getPrefix().getLastWhitespace().contains("\n")) {
alignTo = getLengthOfWhitespace(firstArg.getPrefix().getLastWhitespace());
} else {
String source = method.print(getCursor());
int firstArgIndex = source.indexOf(firstArg.print(getCursor()));
int lineBreakIndex = source.lastIndexOf('\n', firstArgIndex);
alignTo = (firstArgIndex - (lineBreakIndex == -1 ? 0 : lineBreakIndex)) - 1;
}
getCursor().getParentOrThrow().putMessage("lastIndent", alignTo - style.getContinuationIndent());
elem = visitAndCast(elem, p);
getCursor().getParentOrThrow().putMessage("lastIndent", indent);
after = indentTo(right.getAfter(), t == lastArg ? indent : alignTo, loc.getAfterLocation());
} else {
after = right.getAfter();
}
} else {
elem = visitAndCast(elem, p);
after = indentTo(right.getAfter(), t == lastArg ? indent : style.getContinuationIndent(), loc.getAfterLocation());
}
break;
}
case METHOD_INVOCATION_ARGUMENT:
if (!elem.getPrefix().getLastWhitespace().contains("\n")) {
if (elem instanceof J.Lambda) {
J body = ((J.Lambda) elem).getBody();
if (!(body instanceof J.Binary)) {
if (!body.getPrefix().getLastWhitespace().contains("\n")) {
getCursor().getParentOrThrow().putMessage("lastIndent", indent + style.getContinuationIndent());
}
}
}
}
elem = visitAndCast(elem, p);
after = indentTo(right.getAfter(), indent, loc.getAfterLocation());
if (!after.getComments().isEmpty() || after.getLastWhitespace().contains("\n")) {
Cursor parent = getCursor().getParentTreeCursor();
Cursor grandparent = parent.getParentTreeCursor();
// propagate indentation up in the method chain hierarchy
if (grandparent.getValue() instanceof J.MethodInvocation && ((J.MethodInvocation) grandparent.getValue()).getSelect() == parent.getValue()) {
grandparent.putMessage("lastIndent", indent);
grandparent.putMessage("chainedIndent", indent);
}
}
break;
case NEW_CLASS_ARGUMENTS:
case ARRAY_INDEX:
case PARENTHESES:
case TYPE_PARAMETER: {
elem = visitAndCast(elem, p);
after = indentTo(right.getAfter(), indent, loc.getAfterLocation());
break;
}
case ANNOTATION_ARGUMENT:
JContainer args = getCursor().getParentOrThrow().getValue();
elem = visitAndCast(elem, p);
// the end parentheses on an annotation is aligned to the annotation
if (args.getPadding().getElements().get(args.getElements().size() - 1) == right) {
getCursor().getParentOrThrow().putMessage("indentType", IndentType.ALIGN);
}
after = visitSpace(right.getAfter(), loc.getAfterLocation(), p);
break;
default:
elem = visitAndCast(elem, p);
after = visitSpace(right.getAfter(), loc.getAfterLocation(), p);
}
} else {
switch (loc) {
case NEW_CLASS_ARGUMENTS:
case METHOD_INVOCATION_ARGUMENT:
if (!elem.getPrefix().getLastWhitespace().contains("\n")) {
JContainer args = getCursor().getParentOrThrow().getValue();
boolean anyOtherArgOnOwnLine = false;
for (JRightPadded arg : args.getPadding().getElements()) {
if (arg == getCursor().getValue()) {
continue;
}
if (arg.getElement().getPrefix().getLastWhitespace().contains("\n")) {
anyOtherArgOnOwnLine = true;
break;
}
}
if (!anyOtherArgOnOwnLine) {
elem = visitAndCast(elem, p);
after = indentTo(right.getAfter(), indent, loc.getAfterLocation());
break;
}
}
if (!(elem instanceof J.Binary)) {
if (!(elem instanceof J.MethodInvocation)) {
getCursor().putMessage("lastIndent", indent + style.getContinuationIndent());
} else if (elem.getPrefix().getLastWhitespace().contains("\n")) {
getCursor().putMessage("lastIndent", indent + style.getContinuationIndent());
} else {
J.MethodInvocation methodInvocation = (J.MethodInvocation) elem;
Expression select = methodInvocation.getSelect();
if (select instanceof J.FieldAccess || select instanceof J.Identifier || select instanceof J.MethodInvocation) {
getCursor().putMessage("lastIndent", indent + style.getContinuationIndent());
}
}
}
elem = visitAndCast(elem, p);
after = visitSpace(right.getAfter(), loc.getAfterLocation(), p);
break;
default:
elem = visitAndCast(elem, p);
after = right.getAfter();
}
}
//noinspection unchecked
t = (T) elem;
} else {
after = visitSpace(right.getAfter(), loc.getAfterLocation(), p);
}
setCursor(getCursor().getParent());
return (after == right.getAfter() && t == right.getElement()) ? right : new JRightPadded<>(t, after, right.getMarkers());
}
@Override
public JContainer visitContainer(JContainer container, JContainer.Location loc, P p) {
setCursor(new Cursor(getCursor(), container));
Space before;
List> js;
int indent = getCursor().getNearestMessage("lastIndent", 0);
if (container.getBefore().getLastWhitespace().contains("\n")) {
switch (loc) {
case TYPE_PARAMETERS:
case IMPLEMENTS:
case THROWS:
case NEW_CLASS_ARGUMENTS:
before = indentTo(container.getBefore(), indent + style.getContinuationIndent(), loc.getBeforeLocation());
getCursor().putMessage("indentType", IndentType.ALIGN);
getCursor().putMessage("lastIndent", indent + style.getContinuationIndent());
js = ListUtils.map(container.getPadding().getElements(), t -> visitRightPadded(t, loc.getElementLocation(), p));
break;
default:
before = visitSpace(container.getBefore(), loc.getBeforeLocation(), p);
js = ListUtils.map(container.getPadding().getElements(), t -> visitRightPadded(t, loc.getElementLocation(), p));
}
} else {
switch (loc) {
case IMPLEMENTS:
case METHOD_INVOCATION_ARGUMENTS:
case NEW_CLASS_ARGUMENTS:
case TYPE_PARAMETERS:
case THROWS:
getCursor().putMessage("indentType", IndentType.CONTINUATION_INDENT);
before = visitSpace(container.getBefore(), loc.getBeforeLocation(), p);
js = ListUtils.map(container.getPadding().getElements(), t -> visitRightPadded(t, loc.getElementLocation(), p));
break;
default:
before = visitSpace(container.getBefore(), loc.getBeforeLocation(), p);
js = ListUtils.map(container.getPadding().getElements(), t -> visitRightPadded(t, loc.getElementLocation(), p));
}
}
setCursor(getCursor().getParent());
return js == container.getPadding().getElements() && before == container.getBefore() ?
container :
JContainer.build(before, js, container.getMarkers());
}
private Space indentTo(Space space, int column, Space.Location spaceLocation) {
Space s = space;
String whitespace = s.getWhitespace();
if (spaceLocation == Space.Location.COMPILATION_UNIT_PREFIX && !StringUtils.isNullOrEmpty(whitespace)) {
s = s.withWhitespace("");
} else if (s.getComments().isEmpty() && !s.getLastWhitespace().contains("\n")) {
return s;
}
if (s.getComments().isEmpty()) {
int indent = findIndent(s);
if (indent != column) {
int shift = column - indent;
s = s.withWhitespace(indent(whitespace, shift));
}
} else {
boolean hasFileLeadingComment = !space.getComments().isEmpty() && (
spaceLocation == Space.Location.COMPILATION_UNIT_PREFIX ||
(spaceLocation == Space.Location.CLASS_DECLARATION_PREFIX && space.getComments().get(0).isMultiline())
);
int finalColumn = spaceLocation == Space.Location.BLOCK_END ?
column + style.getIndentSize() : column;
String lastIndent = space.getWhitespace().substring(space.getWhitespace().lastIndexOf('\n') + 1);
int indent = getLengthOfWhitespace(StringUtils.indent(lastIndent));
if (indent != finalColumn) {
if (hasFileLeadingComment || whitespace.contains("\n") &&
// Do not shift single line comments at col 0.
!(!s.getComments().isEmpty() && s.getComments().get(0) instanceof TextComment &&
!s.getComments().get(0).isMultiline() && getLengthOfWhitespace(s.getWhitespace()) == 0)) {
int shift = finalColumn - indent;
s = s.withWhitespace(whitespace.substring(0, whitespace.lastIndexOf('\n') + 1) +
indent(lastIndent, shift));
}
Space finalSpace = s;
int lastCommentPos = s.getComments().size() - 1;
s = s.withComments(ListUtils.map(s.getComments(), (i, c) -> {
if (c instanceof TextComment && !c.isMultiline()) {
// Do not shift single line comments at col 0.
if ((i != lastCommentPos) && getLengthOfWhitespace(c.getSuffix()) == 0) {
return c;
}
}
String priorSuffix = i == 0 ?
space.getWhitespace() :
finalSpace.getComments().get(i - 1).getSuffix();
int toColumn = spaceLocation == Space.Location.BLOCK_END && i != finalSpace.getComments().size() - 1 ?
column + style.getIndentSize() :
column;
Comment c2 = c;
if (priorSuffix.contains("\n") || hasFileLeadingComment) {
c2 = indentComment(c, priorSuffix, toColumn);
}
if (c2.getSuffix().contains("\n")) {
int suffixIndent = getLengthOfWhitespace(c2.getSuffix());
int shift = toColumn - suffixIndent;
c2 = c2.withSuffix(indent(c2.getSuffix(), shift));
}
return c2;
}));
}
}
return s;
}
private Comment indentComment(Comment comment, String priorSuffix, int column) {
if (comment instanceof TextComment) {
TextComment textComment = (TextComment) comment;
String text = textComment.getText();
if (!text.contains("\n")) {
return comment;
}
// the margin is the baseline for how much we should shift left or right
String margin = StringUtils.commonMargin(null, priorSuffix);
int indent = getLengthOfWhitespace(margin);
int shift = column - indent;
if (shift > 0) {
String newMargin = indent(margin, shift);
if (textComment.isMultiline()) {
StringBuilder multiline = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\n') {
multiline.append(c).append(newMargin);
i += margin.length();
} else {
multiline.append(c);
}
}
return textComment.withText(multiline.toString());
}
} else if (shift < 0) {
if (textComment.isMultiline()) {
StringBuilder multiline = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\n') {
multiline.append(c);
for (int j = 0; j < Math.abs(shift) && i+j+1 < text.length() && (text.charAt(j + i + 1) == ' ' || text.charAt(j + i + 1) == '\t'); j++) {
i++;
}
} else {
multiline.append(c);
}
}
return textComment.withText(multiline.toString());
}
} else {
return textComment;
}
} else if (comment instanceof Javadoc.DocComment) {
final Javadoc.DocComment docComment = (Javadoc.DocComment) comment;
return docComment.withBody(ListUtils.map(docComment.getBody(), (i, jdoc) -> {
if(!(jdoc instanceof Javadoc.LineBreak)) {
return jdoc;
}
Javadoc.LineBreak lineBreak = (Javadoc.LineBreak) jdoc;
String linebreak;
if(lineBreak.getMargin().charAt(0) == '\r') {
linebreak = "\r\n";
} else {
linebreak = "\n";
}
return lineBreak.withMargin(lineBreak.getMargin().replaceAll("^\\s+", indent(linebreak, column + 1)));
}));
}
return comment;
}
private String indent(String whitespace, int shift) {
StringBuilder newWhitespace = new StringBuilder(whitespace);
shift(newWhitespace, shift);
return newWhitespace.toString();
}
private void shift(StringBuilder text, int shift) {
int tabIndent = style.getTabSize();
if (!style.getUseTabCharacter()) {
tabIndent = Integer.MAX_VALUE;
}
if (shift > 0) {
for (int i = 0; i < shift / tabIndent; i++) {
text.append('\t');
}
for (int i = 0; i < shift % tabIndent; i++) {
text.append(' ');
}
} else {
int len;
if (style.getUseTabCharacter()) {
len = text.length() + (shift / tabIndent);
} else {
len = text.length() + shift;
}
if (len >= 0) {
text.delete(len, text.length());
}
}
}
private int findIndent(Space space) {
String indent = space.getIndent();
return getLengthOfWhitespace(indent);
}
private int getLengthOfWhitespace(@Nullable String whitespace) {
if (whitespace == null) {
return 0;
}
int size = 0;
for (int i = 0; i < whitespace.length(); i++) {
char c = whitespace.charAt(i);
size += c == '\t' ? style.getTabSize() : 1;
if (c == '\n' || c == '\r') {
size = 0;
}
}
return size;
}
private int forInitColumn() {
Cursor forCursor = getCursor().dropParentUntil(J.ForLoop.class::isInstance);
J.ForLoop forLoop = forCursor.getValue();
Object parent = forCursor.getParentOrThrow().getValue();
@SuppressWarnings("ConstantConditions") J alignTo = parent instanceof J.Label ?
((J.Label) parent).withStatement(forLoop.withBody(null)) :
forLoop.withBody(null);
int column = 0;
boolean afterInitStart = false;
String print = alignTo.print(getCursor());
for (int i = 0; i < print.length(); i++) {
char c = print.charAt(i);
if (c == '(') {
afterInitStart = true;
} else if (afterInitStart && !Character.isWhitespace(c)) {
return column - 1;
}
column++;
}
throw new IllegalStateException("For loops must have a control section");
}
@Override
public @Nullable J postVisit(J tree, P p) {
if (stopAfter != null && stopAfter.isScope(tree)) {
getCursor().putMessageOnFirstEnclosing(JavaSourceFile.class, "stop", true);
}
return super.postVisit(tree, p);
}
@Override
public @Nullable J visit(@Nullable Tree tree, P p) {
if (getCursor().getNearestMessage("stop") != null) {
return (J) tree;
}
return super.visit(tree, p);
}
private enum IndentType {
ALIGN,
INDENT,
CONTINUATION_INDENT
}
}