blob: f4cbd91199c752d651f65c48dc95df02b23d1369 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2021 Red Hat Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.fix;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TextBlock;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer;
import org.eclipse.jdt.core.manipulation.ICleanUpFixCore;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.ui.fix.MultiFixMessages;
public class StringConcatToTextBlockFixCore extends CompilationUnitRewriteOperationsFixCore {
private final static String JAVA_STRING= "java.lang.String"; //$NON-NLS-1$
public static final class StringConcatFinder extends ASTVisitor {
private final List<CompilationUnitRewriteOperation> fOperations;
private final boolean fAllConcats;
public StringConcatFinder(List<CompilationUnitRewriteOperation> operations, boolean allConcats) {
super(true);
fOperations= operations;
fAllConcats= allConcats;
}
private boolean isStringType(Type type) {
if (type instanceof ArrayType) {
return false;
}
ITypeBinding typeBinding= type.resolveBinding();
if (typeBinding == null || !typeBinding.getQualifiedName().equals(JAVA_STRING)) {
return false;
}
return true;
}
@Override
public boolean visit(final VariableDeclarationStatement visited) {
Type type= visited.getType();
if (!isStringType(type)) {
return false;
}
return true;
}
@Override
public boolean visit(final FieldDeclaration visited) {
Type type= visited.getType();
if (!isStringType(type)) {
return false;
}
return true;
}
@Override
public boolean visit(final Assignment visited) {
ITypeBinding typeBinding= visited.resolveTypeBinding();
if (!typeBinding.getQualifiedName().equals(JAVA_STRING)) {
return false;
}
return true;
}
@Override
public boolean visit(final InfixExpression visited) {
if (visited.getOperator() != InfixExpression.Operator.PLUS
|| visited.extendedOperands().isEmpty()) {
return false;
}
ITypeBinding typeBinding= visited.resolveTypeBinding();
if (typeBinding == null || !typeBinding.getQualifiedName().equals(JAVA_STRING)) {
return false;
}
Expression leftHand= visited.getLeftOperand();
if (!(leftHand instanceof StringLiteral)) {
return false;
}
StringLiteral leftLiteral= (StringLiteral)leftHand;
String literal= leftLiteral.getLiteralValue();
if (!literal.isEmpty() && !fAllConcats && !literal.endsWith("\n")) { //$NON-NLS-1$
return false;
}
Expression rightHand= visited.getRightOperand();
if (!(rightHand instanceof StringLiteral)) {
return false;
}
StringLiteral rightLiteral= (StringLiteral)leftHand;
literal= rightLiteral.getLiteralValue();
if (!literal.isEmpty() && !fAllConcats && !literal.endsWith("\n")) { //$NON-NLS-1$
return false;
}
List<Expression> extendedOperands= visited.extendedOperands();
if (extendedOperands.isEmpty()) {
return false;
}
for (int i= 0; i < extendedOperands.size(); ++i) {
Expression operand= extendedOperands.get(i);
if (operand instanceof StringLiteral) {
StringLiteral stringLiteral= (StringLiteral)operand;
String string= stringLiteral.getLiteralValue();
if (!string.isEmpty() && (fAllConcats || string.endsWith("\n") || i == extendedOperands.size() - 1)) { //$NON-NLS-1$
continue;
}
}
return false;
}
fOperations.add(new ChangeStringConcatToTextBlock(visited));
return false;
}
}
public static class ChangeStringConcatToTextBlock extends CompilationUnitRewriteOperation {
private final InfixExpression fInfix;
private final String fIndent;
public ChangeStringConcatToTextBlock(final InfixExpression infix) {
this.fInfix= infix;
this.fIndent= "\t"; //$NON-NLS-1$
}
@Override
public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModelCore linkedModel) throws CoreException {
ASTRewrite rewrite= cuRewrite.getASTRewrite();
TextEditGroup group= createTextEditGroup(MultiFixMessages.StringConcatToTextBlockCleanUp_description, cuRewrite);
rewrite.setTargetSourceRangeComputer(new TargetSourceRangeComputer() {
@Override
public SourceRange computeSourceRange(final ASTNode nodeWithComment) {
if (Boolean.TRUE.equals(nodeWithComment.getProperty(ASTNodes.UNTOUCH_COMMENT))) {
return new SourceRange(nodeWithComment.getStartPosition(), nodeWithComment.getLength());
}
return super.computeSourceRange(nodeWithComment);
}
});
StringBuilder buf= new StringBuilder();
Stream<Expression> expressions= Stream.concat(Stream.of(fInfix.getLeftOperand(), fInfix.getRightOperand()), ((List<Expression>) fInfix.extendedOperands()).stream());
List<String> parts= new ArrayList<>();
expressions.forEach(new Consumer<Expression>() {
@Override
public void accept(Expression t) {
String value= ((StringLiteral) t).getEscapedValue();
parts.addAll(unescapeBlock(value.substring(1, value.length() - 1)));
}
});
buf.append("\"\"\"\n"); //$NON-NLS-1$
boolean newLine= false;
for (String part : parts) {
if (buf.length() > 4) {// the first part has been added after the text block delimiter and newline
if (!newLine) {
// no line terminator in this part: merge the line by emitting a line continuation escape
buf.append("\\").append(System.lineSeparator()); //$NON-NLS-1$
}
}
newLine= part.endsWith(System.lineSeparator());
buf.append(fIndent).append(part);
}
if (newLine) {
buf.append(fIndent);
}
buf.append("\"\"\""); //$NON-NLS-1$
TextBlock textBlock= (TextBlock) rewrite.createStringPlaceholder(buf.toString(), ASTNode.TEXT_BLOCK);
rewrite.replace(fInfix, textBlock, group);
}
/*
* Split a given string into parts of a text block. Transformations undertaken will be:
*
* 1. Split the text at newline boundaries. The newline will be replaced at the end
* of the first line being split with System.lineTerminator()
* 2. Transform any sequence of three or more double quotes in such that it's not interpreted as "end of text block"
* 3. Transform any trailing spaces into \s escapes
* 4. Transform any non-trailing \t characters into tab characters
*/
private List<String> unescapeBlock(String escapedText) {
StringBuilder transformed= new StringBuilder();
int readIndex= 0;
int bsIndex= 0;
List<String> parts= new ArrayList<>();
while ((bsIndex= escapedText.indexOf("\\", readIndex)) >= 0) { //$NON-NLS-1$ "\"
if (escapedText.startsWith("\\n", bsIndex)) { //$NON-NLS-1$ "\n"
transformed.append(escapedText.substring(readIndex, bsIndex));
parts.add(escapeTrailingWhitespace(transformed.toString())+ System.lineSeparator());
transformed= new StringBuilder();
readIndex= bsIndex + 2;
} else if (escapedText.startsWith("\\\"", bsIndex)) { //$NON-NLS-1$ "\""
// if there are more than three quotes in a row, escape the first quote of every triplet to
// avoid it being interpreted as a text block terminator. This code would be much simpler if
// we could escape the third quote of each triplet, but the text block spec recommends this way.
transformed.append(escapedText.substring(readIndex, bsIndex));
int quoteCount= 1;
while (escapedText.startsWith("\\\"", bsIndex + 2 * quoteCount)) { //$NON-NLS-1$
quoteCount++;
}
int i= 0;
while (i < quoteCount / 3) {
transformed.append("\\\"\"\""); //$NON-NLS-1$
i++;
}
if (i > 0 && quoteCount % 3 != 0) {
transformed.append("\\"); //$NON-NLS-1$
}
for (int j = 0; j < quoteCount % 3; j++) {
transformed.append("\""); //$NON-NLS-1$
}
readIndex= bsIndex + 2 * quoteCount;
} else if (escapedText.startsWith("\\t", bsIndex)) { //$NON-NLS-1$ "\t"
transformed.append(escapedText.substring(readIndex, bsIndex));
transformed.append("\t"); //$NON-NLS-1$
readIndex= bsIndex+2;
} else {
transformed.append(escapedText.substring(readIndex, bsIndex));
transformed.append("\\").append(escapedText.charAt(bsIndex + 1)); //$NON-NLS-1$
readIndex= bsIndex + 2;
}
}
if (readIndex < escapedText.length()) {
// there is text at the end of the string that is not followed by a newline
transformed.append(escapeTrailingWhitespace(escapedText.substring(readIndex)));
}
if (transformed.length() > 0) {
parts.add(transformed.toString());
}
return parts;
}
/*
* Escape spaces and tabs at the end of a line, because they would be trimmed from a text block
*/
private static String escapeTrailingWhitespace(String unescaped) {
if (unescaped.length() == 0) {
return ""; //$NON-NLS-1$
}
int whitespaceStart= unescaped.length()-1;
StringBuilder trailingWhitespace= new StringBuilder();
while (whitespaceStart > 0) {
if (unescaped.charAt(whitespaceStart) == ' ') {
whitespaceStart--;
trailingWhitespace.append("\\s"); //$NON-NLS-1$
} else if (unescaped.charAt(whitespaceStart) == '\t') {
whitespaceStart--;
trailingWhitespace.append("\\t"); //$NON-NLS-1$
} else {
break;
}
}
return unescaped.substring(0, whitespaceStart + 1) + trailingWhitespace;
}
}
public static ICleanUpFixCore createCleanUp(final CompilationUnit compilationUnit) {
if (!JavaModelUtil.is15OrHigher(compilationUnit.getJavaElement().getJavaProject()))
return null;
List<CompilationUnitRewriteOperation> operations= new ArrayList<>();
StringConcatFinder finder= new StringConcatFinder(operations, true);
compilationUnit.accept(finder);
if (operations.isEmpty()) {
return null;
}
CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] ops= operations.toArray(new CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[0]);
return new StringBufferToStringBuilderFixCore(FixMessages.StringConcatToTextBlockFix_convert_msg, compilationUnit, ops);
}
public static StringConcatToTextBlockFixCore createStringConcatToTextBlockFix(ASTNode exp) {
CompilationUnit root= (CompilationUnit) exp.getRoot();
if (!JavaModelUtil.is15OrHigher(root.getJavaElement().getJavaProject()))
return null;
List<CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation> operations= new ArrayList<>();
StringConcatFinder finder= new StringConcatFinder(operations, true);
exp.accept(finder);
if (operations.isEmpty())
return null;
return new StringConcatToTextBlockFixCore(FixMessages.StringConcatToTextBlockFix_convert_msg, root, new CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] { operations.get(0) });
}
protected StringConcatToTextBlockFixCore(final String name, final CompilationUnit compilationUnit, final CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] fixRewriteOperations) {
super(name, compilationUnit, fixRewriteOperations);
}
}