| /******************************************************************************* |
| * 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); |
| } |
| |
| } |