blob: 90cbed2be1dfd1af7e4463769bbe95a7d463b77e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020 Julian Honnen
*
* 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:
* Julian Honnen <julian.honnen@vector.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.correction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.eclipse.swt.graphics.Image;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.NamingConventions;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayCreation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.ui.text.java.IInvocationContext;
import org.eclipse.jdt.ui.text.java.correction.ASTRewriteCorrectionProposal;
import org.eclipse.jdt.ui.text.java.correction.ICommandAccess;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposal;
class ConvertStringConcatenationProposals {
private final IInvocationContext fContext;
private final AST fAst;
private final InfixExpression fOldInfixExpression;
private ConvertStringConcatenationProposals(IInvocationContext context, AST ast, InfixExpression oldInfixExpression) {
fContext= context;
fAst= ast;
fOldInfixExpression= oldInfixExpression;
}
public static boolean getProposals(IInvocationContext context, Collection<ICommandAccess> resultingCollections) {
ASTNode node= context.getCoveringNode();
BodyDeclaration parentDecl= ASTResolving.findParentBodyDeclaration(node);
if (!(parentDecl instanceof MethodDeclaration) && !(parentDecl instanceof Initializer))
return false;
AST ast= node.getAST();
ITypeBinding stringBinding= ast.resolveWellKnownType("java.lang.String"); //$NON-NLS-1$
if (node instanceof Expression && !(node instanceof InfixExpression)) {
node= node.getParent();
}
if (node instanceof VariableDeclarationFragment) {
node= ((VariableDeclarationFragment) node).getInitializer();
} else if (node instanceof Assignment) {
node= ((Assignment) node).getRightHandSide();
}
InfixExpression oldInfixExpression= null;
while (node instanceof InfixExpression) {
InfixExpression curr= (InfixExpression) node;
if (curr.resolveTypeBinding() == stringBinding && curr.getOperator() == InfixExpression.Operator.PLUS) {
oldInfixExpression= curr; // is a infix expression we can use
} else {
break;
}
node= node.getParent();
}
if (oldInfixExpression == null)
return false;
if (resultingCollections == null) {
return true;
}
ConvertStringConcatenationProposals convertStringConcatenation= new ConvertStringConcatenationProposals(context, ast, oldInfixExpression);
convertStringConcatenation.createProposals(resultingCollections);
return true;
}
private void createProposals(Collection<ICommandAccess> resultingCollections) {
ASTRewriteCorrectionProposal stringBufferProposal= getConvertToStringBufferProposal();
resultingCollections.add(stringBufferProposal);
ASTRewriteCorrectionProposal messageFormatProposal= getConvertToMessageFormatProposal();
if (messageFormatProposal != null)
resultingCollections.add(messageFormatProposal);
ASTRewriteCorrectionProposal stringFormatProposal= getConvertToStringFormatProposal();
if (stringFormatProposal != null)
resultingCollections.add(stringFormatProposal);
}
private ASTRewriteCorrectionProposal getConvertToStringBufferProposal() {
String bufferOrBuilderName;
ICompilationUnit cu= fContext.getCompilationUnit();
if (JavaModelUtil.is50OrHigher(cu.getJavaProject())) {
bufferOrBuilderName= "StringBuilder"; //$NON-NLS-1$
} else {
bufferOrBuilderName= "StringBuffer"; //$NON-NLS-1$
}
ASTRewrite rewrite= ASTRewrite.create(fAst);
SimpleName existingBuffer= getEnclosingAppendBuffer(fOldInfixExpression);
String mechanismName= BasicElementLabels.getJavaElementName(existingBuffer == null ? bufferOrBuilderName : existingBuffer.getIdentifier());
String label= Messages.format(CorrectionMessages.QuickAssistProcessor_convert_to_string_buffer_description, mechanismName);
Image image= JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE);
LinkedCorrectionProposal proposal= new LinkedCorrectionProposal(label, cu, rewrite, IProposalRelevance.CONVERT_TO_STRING_BUFFER, image);
proposal.setCommandId(QuickAssistProcessor.CONVERT_TO_STRING_BUFFER_ID);
Statement insertAfter;
String bufferName;
String groupID= "nameId"; //$NON-NLS-1$
ListRewrite listRewrite;
Statement enclosingStatement= ASTResolving.findParentStatement(fOldInfixExpression);
if (existingBuffer != null) {
if (ASTNodes.isControlStatementBody(enclosingStatement.getLocationInParent())) {
Block newBlock= fAst.newBlock();
listRewrite= rewrite.getListRewrite(newBlock, Block.STATEMENTS_PROPERTY);
insertAfter= null;
rewrite.replace(enclosingStatement, newBlock, null);
} else {
listRewrite= rewrite.getListRewrite(enclosingStatement.getParent(), (ChildListPropertyDescriptor) enclosingStatement.getLocationInParent());
insertAfter= enclosingStatement;
}
bufferName= existingBuffer.getIdentifier();
} else {
// create buffer
VariableDeclarationFragment frag= fAst.newVariableDeclarationFragment();
// check if name is already in use and provide alternative
List<String> fExcludedVariableNames= Arrays.asList(ASTResolving.getUsedVariableNames(fOldInfixExpression));
SimpleType bufferType= fAst.newSimpleType(fAst.newName(bufferOrBuilderName));
ClassInstanceCreation newBufferExpression= fAst.newClassInstanceCreation();
String[] newBufferNames= StubUtility.getVariableNameSuggestions(NamingConventions.VK_LOCAL, cu.getJavaProject(), bufferOrBuilderName, 0, fExcludedVariableNames, true);
bufferName= newBufferNames[0];
SimpleName bufferNameDeclaration= fAst.newSimpleName(bufferName);
frag.setName(bufferNameDeclaration);
proposal.addLinkedPosition(rewrite.track(bufferNameDeclaration), true, groupID);
for (String newBufferName : newBufferNames) {
proposal.addLinkedPositionProposal(groupID, newBufferName, null);
}
newBufferExpression.setType(bufferType);
frag.setInitializer(newBufferExpression);
VariableDeclarationStatement bufferDeclaration= fAst.newVariableDeclarationStatement(frag);
bufferDeclaration.setType(fAst.newSimpleType(fAst.newName(bufferOrBuilderName)));
insertAfter= bufferDeclaration;
Statement statement= ASTResolving.findParentStatement(fOldInfixExpression);
if (ASTNodes.isControlStatementBody(statement.getLocationInParent())) {
Block newBlock= fAst.newBlock();
listRewrite= rewrite.getListRewrite(newBlock, Block.STATEMENTS_PROPERTY);
listRewrite.insertFirst(bufferDeclaration, null);
listRewrite.insertLast(rewrite.createMoveTarget(statement), null);
rewrite.replace(statement, newBlock, null);
} else {
listRewrite= rewrite.getListRewrite(statement.getParent(), (ChildListPropertyDescriptor) statement.getLocationInParent());
listRewrite.insertBefore(bufferDeclaration, statement, null);
}
}
List<Expression> operands= new ArrayList<>();
collectInfixPlusOperands(fOldInfixExpression, operands);
Statement lastAppend= insertAfter;
for (Expression operand : operands) {
MethodInvocation appendIncovationExpression= fAst.newMethodInvocation();
appendIncovationExpression.setName(fAst.newSimpleName("append")); //$NON-NLS-1$
SimpleName bufferNameReference= fAst.newSimpleName(bufferName);
// If there was an existing name, don't offer to rename it
if (existingBuffer == null) {
proposal.addLinkedPosition(rewrite.track(bufferNameReference), true, groupID);
}
appendIncovationExpression.setExpression(bufferNameReference);
appendIncovationExpression.arguments().add(rewrite.createCopyTarget(operand));
ExpressionStatement appendExpressionStatement= fAst.newExpressionStatement(appendIncovationExpression);
if (lastAppend == null) {
listRewrite.insertFirst(appendExpressionStatement, null);
} else {
listRewrite.insertAfter(appendExpressionStatement, lastAppend, null);
}
lastAppend= appendExpressionStatement;
}
if (existingBuffer != null) {
proposal.setEndPosition(rewrite.track(lastAppend));
if (insertAfter != null) {
rewrite.remove(enclosingStatement, null);
}
} else {
// replace old expression with toString
MethodInvocation bufferToString= fAst.newMethodInvocation();
bufferToString.setName(fAst.newSimpleName("toString")); //$NON-NLS-1$
SimpleName bufferNameReference= fAst.newSimpleName(bufferName);
bufferToString.setExpression(bufferNameReference);
proposal.addLinkedPosition(rewrite.track(bufferNameReference), true, groupID);
rewrite.replace(fOldInfixExpression, bufferToString, null);
proposal.setEndPosition(rewrite.track(bufferToString));
}
return proposal;
}
private ASTRewriteCorrectionProposal getConvertToMessageFormatProposal() {
ICompilationUnit cu= fContext.getCompilationUnit();
boolean is50OrHigher= JavaModelUtil.is50OrHigher(cu.getJavaProject());
ASTRewrite rewrite= ASTRewrite.create(fAst);
CompilationUnit root= fContext.getASTRoot();
ImportRewrite importRewrite= StubUtility.createImportRewrite(root, true);
ContextSensitiveImportRewriteContext importContext= new ContextSensitiveImportRewriteContext(root, fOldInfixExpression.getStartPosition(), importRewrite);
// collect operands
List<Expression> operands= new ArrayList<>();
collectInfixPlusOperands(fOldInfixExpression, operands);
List<Expression> formatArguments= new ArrayList<>();
StringBuilder formatString= new StringBuilder();
int i= 0;
for (Expression operand : operands) {
if (operand instanceof StringLiteral) {
String value= ((StringLiteral) operand).getEscapedValue();
value= value.substring(1, value.length() - 1);
value= value.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$
formatString.append(value);
} else {
formatString.append("{").append(i).append("}"); //$NON-NLS-1$ //$NON-NLS-2$
Expression argument;
if (is50OrHigher) {
argument= (Expression) rewrite.createCopyTarget(operand);
} else {
ITypeBinding binding= operand.resolveTypeBinding();
if (binding == null)
return null;
argument= (Expression) rewrite.createCopyTarget(operand);
if (binding.isPrimitive()) {
ITypeBinding boxedBinding= Bindings.getBoxedTypeBinding(binding, fAst);
if (boxedBinding != binding) {
Type boxedType= importRewrite.addImport(boxedBinding, fAst, importContext);
ClassInstanceCreation cic= fAst.newClassInstanceCreation();
cic.setType(boxedType);
cic.arguments().add(argument);
argument= cic;
}
}
}
formatArguments.add(argument);
i++;
}
}
if (formatArguments.isEmpty())
return null;
String label= CorrectionMessages.QuickAssistProcessor_convert_to_message_format;
Image image= JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE);
ASTRewriteCorrectionProposal proposal= new ASTRewriteCorrectionProposal(label, cu, rewrite, IProposalRelevance.CONVERT_TO_MESSAGE_FORMAT, image);
proposal.setCommandId(QuickAssistProcessor.CONVERT_TO_MESSAGE_FORMAT_ID);
proposal.setImportRewrite(importRewrite);
String messageType= importRewrite.addImport("java.text.MessageFormat", importContext); //$NON-NLS-1$
MethodInvocation formatInvocation= fAst.newMethodInvocation();
formatInvocation.setExpression(fAst.newName(messageType));
formatInvocation.setName(fAst.newSimpleName("format")); //$NON-NLS-1$
List<Expression> arguments= formatInvocation.arguments();
StringLiteral formatStringArgument= fAst.newStringLiteral();
formatStringArgument.setEscapedValue("\"" + formatString.append("\"").toString()); //$NON-NLS-1$ //$NON-NLS-2$
arguments.add(formatStringArgument);
if (is50OrHigher) {
arguments.addAll(formatArguments);
} else {
ArrayCreation objectArrayCreation= fAst.newArrayCreation();
Type objectType= fAst.newSimpleType(fAst.newSimpleName("Object")); //$NON-NLS-1$
ArrayType arrayType= fAst.newArrayType(objectType);
objectArrayCreation.setType(arrayType);
ArrayInitializer arrayInitializer= fAst.newArrayInitializer();
List<Expression> initializerExpressions= arrayInitializer.expressions();
initializerExpressions.addAll(formatArguments);
objectArrayCreation.setInitializer(arrayInitializer);
arguments.add(objectArrayCreation);
}
rewrite.replace(fOldInfixExpression, formatInvocation, null);
return proposal;
}
private ASTRewriteCorrectionProposal getConvertToStringFormatProposal() {
ICompilationUnit cu= fContext.getCompilationUnit();
if (!JavaModelUtil.is50OrHigher(cu.getJavaProject()))
return null;
ASTRewrite rewrite= ASTRewrite.create(fAst);
// collect operands
List<Expression> operands= new ArrayList<>();
collectInfixPlusOperands(fOldInfixExpression, operands);
List<Expression> formatArguments= new ArrayList<>();
StringBuilder formatString= new StringBuilder();
for (Expression operand : operands) {
if (operand instanceof StringLiteral) {
String value= ((StringLiteral) operand).getEscapedValue();
value= value.substring(1, value.length() - 1);
formatString.append(value);
} else {
ITypeBinding binding= operand.resolveTypeBinding();
if (binding == null)
return null;
formatString.append("%").append(stringFormatConversion(binding)); //$NON-NLS-1$
formatArguments.add((Expression) rewrite.createCopyTarget(operand));
}
}
if (formatArguments.isEmpty())
return null;
String label= CorrectionMessages.QuickAssistProcessor_convert_to_string_format;
Image image= JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE);
ASTRewriteCorrectionProposal proposal= new ASTRewriteCorrectionProposal(label, cu, rewrite, IProposalRelevance.CONVERT_TO_MESSAGE_FORMAT, image);
proposal.setCommandId(QuickAssistProcessor.CONVERT_TO_STRING_FORMAT_ID);
MethodInvocation formatInvocation= fAst.newMethodInvocation();
formatInvocation.setExpression(fAst.newName("String")); //$NON-NLS-1$
formatInvocation.setName(fAst.newSimpleName("format")); //$NON-NLS-1$
List<Expression> arguments= formatInvocation.arguments();
StringLiteral formatStringArgument= fAst.newStringLiteral();
formatStringArgument.setEscapedValue("\"" + formatString.append("\"").toString()); //$NON-NLS-1$ //$NON-NLS-2$
arguments.add(formatStringArgument);
arguments.addAll(formatArguments);
rewrite.replace(fOldInfixExpression, formatInvocation, null);
return proposal;
}
private static char stringFormatConversion(ITypeBinding type) {
switch (type.getName()) {
case "byte": //$NON-NLS-1$
case "short": //$NON-NLS-1$
case "int": //$NON-NLS-1$
case "long": //$NON-NLS-1$
return 'd';
case "float": //$NON-NLS-1$
case "double": //$NON-NLS-1$
return 'f';
case "char": //$NON-NLS-1$
return 'c';
default:
return 's';
}
}
/**
* Checks
* <ul>
* <li>whether the given infix expression is the argument of a StringBuilder#append() or
* StringBuffer#append() invocation, and</li>
* <li>the append method is called on a simple variable, and</li>
* <li>the invocation occurs in a statement (not as nested expression)</li>
* </ul>
*
* @param infixExpression the infix expression
* @return the name of the variable we were appending to, or <code>null</code> if not matching
*/
private static SimpleName getEnclosingAppendBuffer(InfixExpression infixExpression) {
if (infixExpression.getLocationInParent() == MethodInvocation.ARGUMENTS_PROPERTY) {
MethodInvocation methodInvocation= (MethodInvocation) infixExpression.getParent();
// ..not in an expression.. (e.g. not sb.append("high" + 5).append(6);)
if (methodInvocation.getParent() instanceof Statement) {
// ..of a function called append:
if ("append".equals(methodInvocation.getName().getIdentifier())) { //$NON-NLS-1$
Expression expression= methodInvocation.getExpression();
// ..and the append is being called on a Simple object:
if (expression instanceof SimpleName) {
IBinding binding= ((SimpleName) expression).resolveBinding();
if (binding instanceof IVariableBinding) {
String typeName= ((IVariableBinding) binding).getType().getQualifiedName();
// And the object's type is a StringBuilder or StringBuffer:
if ("java.lang.StringBuilder".equals(typeName) || "java.lang.StringBuffer".equals(typeName)) { //$NON-NLS-1$ //$NON-NLS-2$
return (SimpleName) expression;
}
}
}
}
}
}
return null;
}
private static void collectInfixPlusOperands(Expression expression, List<Expression> collector) {
if (expression instanceof InfixExpression && ((InfixExpression) expression).getOperator() == InfixExpression.Operator.PLUS) {
InfixExpression infixExpression= (InfixExpression) expression;
collectInfixPlusOperands(infixExpression.getLeftOperand(), collector);
collectInfixPlusOperands(infixExpression.getRightOperand(), collector);
List<Expression> extendedOperands= infixExpression.extendedOperands();
for (Expression expression2 : extendedOperands) {
collectInfixPlusOperands(expression2, collector);
}
} else {
collector.add(expression);
}
}
}