blob: 24cdf2a373cce7e9ca4e03141247e9798b16e33c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019, 2020 IBM Corporation 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.correction.proposals;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
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.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.ui.text.java.IProblemLocation;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.text.correction.CorrectionMessages;
import org.eclipse.jdt.internal.ui.util.ASTHelper;
public class InitializeFinalFieldProposal extends LinkedCorrectionProposal {
private IProblemLocation fProblem;
private ASTNode fAstNode;
private final IVariableBinding fVariableBinding;
private int fupdateType;
public static final int UPDATE_AT_DECLARATION= 0;
public static final int UPDATE_AT_CONSTRUCTOR= 1;
public static final int UPDATE_CONSTRUCTOR_NEW_PARAMETER= 2;
public InitializeFinalFieldProposal(IProblemLocation problem, ICompilationUnit cu, ASTNode astNode, IVariableBinding variableBinding, int relevance) {
super(Messages.format(CorrectionMessages.InitializeFieldAtDeclarationCorrectionProposal_description, problem.getProblemArguments()[0]), cu, null, relevance, null);
fProblem= problem;
fAstNode= astNode;
fVariableBinding= variableBinding;
fupdateType= UPDATE_AT_DECLARATION;
setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
}
public InitializeFinalFieldProposal(IProblemLocation problem, ICompilationUnit cu, ASTNode astNode, int relevance, int updateType) {
super(Messages.format(CorrectionMessages.InitializeFieldInConstructorCorrectionProposal_description, problem.getProblemArguments()[0]), cu, null, relevance, null);
if (updateType == UPDATE_CONSTRUCTOR_NEW_PARAMETER) {
setDisplayName(Messages.format(CorrectionMessages.InitializeFieldWithConstructorParameterCorrectionProposal_description, problem.getProblemArguments()[0]));
}
fProblem= problem;
fAstNode= astNode;
fVariableBinding= null;
fupdateType= updateType;
setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
}
public boolean hasProposal() throws CoreException {
return getRewrite() != null;
}
@Override
protected ASTRewrite getRewrite() throws CoreException {
switch (fupdateType) {
case UPDATE_AT_CONSTRUCTOR:
if (fAstNode != null
&& fAstNode.getParent() instanceof RecordDeclaration
&& ASTHelper.isRecordDeclarationNodeSupportedInAST(fAstNode.getAST())) {
return doInitRecordComponentsInConstructor();
}
return doInitFieldInConstructor();
case UPDATE_AT_DECLARATION:
return doInitField();
case UPDATE_CONSTRUCTOR_NEW_PARAMETER:
return doUpdateConstructorWithParameter();
default:
break;
}
return null;
}
private ASTRewrite doInitField() {
FieldDeclaration field= getFieldDeclaration(fVariableBinding.getName());
if (field == null) {
return null;
}
AST ast= fAstNode.getAST();
ITypeBinding type= fVariableBinding.getType();
String variableTypeName= type.getName();
VariableDeclarationFragment newFragment= ast.newVariableDeclarationFragment();
newFragment.setName(ast.newSimpleName(fVariableBinding.getName()));
FieldDeclaration declaration= ast.newFieldDeclaration(newFragment);
declaration.modifiers().addAll(ast.newModifiers(fVariableBinding.getModifiers()));
Expression expression= null;
if (type.isPrimitive()) {
declaration.setType(ast.newPrimitiveType(PrimitiveType.toCode(variableTypeName)));
expression= ast.newNumberLiteral();
} else if ("String".equals(variableTypeName)) { //$NON-NLS-1$
declaration.setType(ast.newSimpleType(ast.newName(variableTypeName)));
expression= ast.newStringLiteral();
} else {
Type newType= null;
if (type.isParameterizedType()) {
newType= createParameterizedType(ast, type);
} else {
newType= ast.newSimpleType(ast.newName(variableTypeName));
}
declaration.setType(newType);
if (hasDefaultConstructor()) {
ClassInstanceCreation cic= ast.newClassInstanceCreation();
cic.setType((Type) ASTNode.copySubtree(ast, newType));
expression= cic;
} else {
expression= ast.newNullLiteral();
}
}
newFragment.setInitializer(expression);
ASTRewrite rewrite= ASTRewrite.create(ast);
rewrite.replace(field, declaration, null);
setEndPosition(rewrite.track(expression)); // set cursor after expression statement
return rewrite;
}
private Type createParameterizedType(AST ast, ITypeBinding typeBinding) {
if (typeBinding.isParameterizedType() && !typeBinding.isRawType()) {
Type baseType= ast.newSimpleType(ASTNodeFactory.newName(ast, typeBinding.getErasure().getName()));
ParameterizedType newType= ast.newParameterizedType(baseType);
for (int i= 0; i < typeBinding.getTypeArguments().length; i++) {
ITypeBinding typeArg= typeBinding.getTypeArguments()[i];
Type argType= createParameterizedType(ast, typeArg); // recursive call
newType.typeArguments().add(argType);
}
return newType;
} else {
if (!typeBinding.isTypeVariable()) {
if (typeBinding.isWildcardType()) {
ImportRewrite importRewrite= createImportRewrite((CompilationUnit) fAstNode.getRoot());
return importRewrite.addImport(typeBinding, ast);
}
return ast.newSimpleType(ASTNodeFactory.newName(ast, typeBinding.getErasure().getName()));
}
return ast.newSimpleType(ast.newSimpleName(typeBinding.getName()));
}
}
private ASTRewrite doInitRecordComponentsInConstructor() {
String variableName= fProblem.getProblemArguments()[0];
SingleVariableDeclaration svd= getRecordComponentDeclaration(variableName);
if (svd == null) {
return null;
}
AST ast= svd.getAST();
FieldAccess fieldAccess= ast.newFieldAccess();
fieldAccess.setName(ast.newSimpleName(variableName));
fieldAccess.setExpression(ast.newThisExpression());
Assignment assignment= ast.newAssignment();
assignment.setLeftHandSide(fieldAccess);
assignment.setOperator(Assignment.Operator.ASSIGN);
Type fieldType= svd.getType();
if (fieldType.isPrimitiveType()) {
assignment.setRightHandSide(ast.newNumberLiteral());
} else if ("String".equals(fieldType.toString())) { //$NON-NLS-1$
assignment.setRightHandSide(ast.newStringLiteral());
} else {
if (hasDefaultConstructor(fieldType.resolveBinding())) {
ClassInstanceCreation cic= ast.newClassInstanceCreation();
cic.setType(ast.newSimpleType(ast.newName(fieldType.toString())));
assignment.setRightHandSide(cic);
} else {
assignment.setRightHandSide(ast.newNullLiteral());
}
}
ExpressionStatement statement= ast.newExpressionStatement(assignment);
ASTRewrite rewrite= ASTRewrite.create(ast);
// find all constructors (methods with same name as the type name)
ConstructorVisitor cv= new ConstructorVisitor(((RecordDeclaration) svd.getParent()).getName().toString());
fAstNode.getRoot().accept(cv);
for (MethodDeclaration md : cv.getNodes()) {
if (!isCanonical(md)) {
continue;
}
Block body= md.getBody();
rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, 0, null);
setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
}
return rewrite;
}
private boolean isCanonical(MethodDeclaration md) {
IMethodBinding methodBinding= md.resolveBinding();
return ((methodBinding == null) ? false : methodBinding.isCanonicalConstructor());
}
private ASTRewrite doUpdateConstructorWithParameter() {
String variableName= fProblem.getProblemArguments()[0];
FieldDeclaration field= getFieldDeclaration(variableName);
if (field == null) {
return null;
}
Type fieldType= field.getType();
ITypeBinding fieldBinding= fieldType.resolveBinding();
if (fieldBinding == null) {
return null;
}
// find all constructors (methods with same name as the type name)
ConstructorVisitor cv= new ConstructorVisitor(((AbstractTypeDeclaration) field.getParent()).getName().toString());
fAstNode.getRoot().accept(cv);
if (cv.getNodes().size() != 1) { // we only handle the simple case of one constructor
return null;
}
MethodDeclaration methodDeclaration= cv.getNodes().get(0); // only one constructor
IMethodBinding methodBinding= methodDeclaration.resolveBinding();
if (methodBinding == null) {
return null;
}
AST ast= field.getAST();
ASTRewrite rewrite= ASTRewrite.create(ast);
SingleVariableDeclaration newSingleVariableDeclaration= ast.newSingleVariableDeclaration();
String[] excludedNames= collectParameterNames(methodDeclaration);
String newName= StubUtility.suggestArgumentName(getCompilationUnit().getJavaProject(), variableName, excludedNames);
newSingleVariableDeclaration.setName(ast.newSimpleName(newName));
Type copyType= ASTNodes.copySubtree(ast, fieldType);
newSingleVariableDeclaration.setType(copyType);
FieldAccess fieldAccess= ast.newFieldAccess();
fieldAccess.setName(ast.newSimpleName(variableName));
fieldAccess.setExpression(ast.newThisExpression());
Assignment assignment= ast.newAssignment();
assignment.setLeftHandSide(fieldAccess);
assignment.setOperator(Assignment.Operator.ASSIGN);
assignment.setRightHandSide(ast.newSimpleName(newName));
ExpressionStatement statement= ast.newExpressionStatement(assignment);
rewrite.getListRewrite(methodDeclaration, MethodDeclaration.PARAMETERS_PROPERTY).insertLast(newSingleVariableDeclaration, null);
Block body= methodDeclaration.getBody();
// check if we call this(), then we can't add initialization here
if (!hasThisCall(body)) {
List<ASTNode> decls= ((AbstractTypeDeclaration) methodDeclaration.getParent()).bodyDeclarations();
List<String> finalFieldList= getFinalFieldList(decls);
int insertIndex= 0;
if (finalFieldList.size() > 1) {
int findFirstFinalFieldReferenceIndex= findFirstFinalFieldReferenceIndex(body.statements(), variableName);
int findFinalFieldInsertIndex= findFinalFieldAssignmentInsertIndex(body.statements(), variableName, finalFieldList);
int index= findFirstFinalFieldReferenceIndex == -1 ? findFinalFieldInsertIndex : findFirstFinalFieldReferenceIndex;
insertIndex= Math.min(body.statements().size(), Math.min(findFinalFieldInsertIndex, index));
}
rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, insertIndex, null);
setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
}
return rewrite;
}
private String[] collectParameterNames(MethodDeclaration methodDeclaration) {
final List<String> names= new ArrayList<>();
for (Object element : methodDeclaration.parameters()) {
SingleVariableDeclaration svd= (SingleVariableDeclaration) element;
names.add(svd.getName().getIdentifier());
}
ASTVisitor v = new ASTVisitor() {
@Override
public boolean visit(VariableDeclarationFragment node) {
names.add(node.getName().getIdentifier());
return super.visit(node);
}
};
methodDeclaration.accept(v);
return names.toArray(new String[names.size()]);
}
private ASTRewrite doInitFieldInConstructor() {
String variableName= fProblem.getProblemArguments()[0];
FieldDeclaration field= getFieldDeclaration(variableName);
if (field == null) {
return null;
}
AST ast= field.getAST();
FieldAccess fieldAccess= ast.newFieldAccess();
fieldAccess.setName(ast.newSimpleName(variableName));
fieldAccess.setExpression(ast.newThisExpression());
Assignment assignment= ast.newAssignment();
assignment.setLeftHandSide(fieldAccess);
assignment.setOperator(Assignment.Operator.ASSIGN);
Type fieldType= field.getType();
if (fieldType.isPrimitiveType()) {
assignment.setRightHandSide(ast.newNumberLiteral());
} else if ("String".equals(fieldType.toString())) { //$NON-NLS-1$
assignment.setRightHandSide(ast.newStringLiteral());
} else {
if (hasDefaultConstructor(fieldType.resolveBinding())) {
ClassInstanceCreation cic= ast.newClassInstanceCreation();
cic.setType(ast.newSimpleType(ast.newName(fieldType.toString())));
assignment.setRightHandSide(cic);
} else {
assignment.setRightHandSide(ast.newNullLiteral());
}
}
ExpressionStatement statement= ast.newExpressionStatement(assignment);
ASTRewrite rewrite= ASTRewrite.create(ast);
// find all constructors (methods with same name as the type name)
ConstructorVisitor cv= new ConstructorVisitor(((AbstractTypeDeclaration) field.getParent()).getName().toString());
fAstNode.getRoot().accept(cv);
for (MethodDeclaration md : cv.getNodes()) {
Block body= md.getBody();
// check if we call this(), then we can't add initialization here
if (!hasThisCall(body)) {
if (hasFieldInitialization(body, variableName)) {
continue;
}
List<ASTNode> decls= ((AbstractTypeDeclaration) md.getParent()).bodyDeclarations();
List<String> finalFieldList= getFinalFieldList(decls);
int insertIndex= 0;
if (finalFieldList.size() > 1) {
int findFirstFinalFieldReferenceIndex= findFirstFinalFieldReferenceIndex(body.statements(), variableName);
int findFinalFieldInsertIndex= findFinalFieldAssignmentInsertIndex(body.statements(), variableName, finalFieldList);
int index= findFirstFinalFieldReferenceIndex == -1 ? findFinalFieldInsertIndex : findFirstFinalFieldReferenceIndex;
insertIndex= Math.min(body.statements().size(), Math.min(findFinalFieldInsertIndex, index));
}
rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, insertIndex, null);
setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
}
}
return rewrite;
}
private boolean hasFieldInitialization(Block body, final String variableName) {
boolean[] hasInit= new boolean[1];
ASTVisitor v= new ASTVisitor() {
@Override
public boolean visit(SimpleName node) {
if (!node.getIdentifier().equals(variableName)) {
return true;
}
ASTNode assignNode= ASTNodes.getFirstAncestorOrNull(node, Assignment.class);
if (assignNode != null) {
Expression lhs= ((Assignment) assignNode).getLeftHandSide();
IBinding resolveBinding= node.resolveBinding();
if (resolveBinding != null && ((IVariableBinding) resolveBinding).isField()) {
int nodeType= lhs.getNodeType();
if (nodeType == ASTNode.SIMPLE_NAME) {
String name= ((SimpleName) lhs).getIdentifier();
if (variableName.equals(name)) {
hasInit[0]= true;
return false;
}
} else if (nodeType == ASTNode.FIELD_ACCESS) {
String name= ((FieldAccess) lhs).getName().getIdentifier();
if (variableName.equals(name)) {
hasInit[0]= true;
return false;
}
}
}
}
return true;
}
};
body.accept(v);
return hasInit[0];
}
private List<String> getFinalFieldList(List<ASTNode> fieldDeclarations) {
List<String> list= new ArrayList<>();
for (ASTNode astNode : fieldDeclarations) {
if (astNode instanceof FieldDeclaration) {
int modifiers= ((FieldDeclaration) astNode).getModifiers();
if (!Modifier.isFinal(modifiers)) {
continue;
}
for (Object object : ((FieldDeclaration) astNode).fragments()) {
list.add(((VariableDeclarationFragment) object).getName().getIdentifier());
}
}
}
return list;
}
/*
* Find insertion index in statements based on declaration order.
*/
private int findFinalFieldAssignmentInsertIndex(List<ASTNode> astNodes, String variableName, List<String> finalFieldList) {
int index= 0;
int fieldIndex= 0;
int findFinalFieldDeclarationIndex= finalFieldList.indexOf(variableName);
String[] fieldAccess= new String[1];
for (ASTNode astNode : astNodes) {
fieldAccess[0]= null;
ASTVisitor v= new ASTVisitor() {
@Override
public boolean visit(FieldAccess node) {
fieldAccess[0]= node.getName().getIdentifier();
return false;
}
@Override
public boolean visit(SimpleName node) {
ASTNode assignNode= ASTNodes.getFirstAncestorOrNull(node, Assignment.class);
if (assignNode != null) {
IBinding resolveBinding= node.resolveBinding();
if (resolveBinding instanceof IVariableBinding
&& ((IVariableBinding) resolveBinding).isField()
&& resolveBinding.getKind() == IBinding.VARIABLE
&& finalFieldList.contains(node.getIdentifier())) {
fieldAccess[0]= node.getIdentifier();
return false;
}
}
return true;
}
};
astNode.accept(v);
if (fieldAccess[0] == null) {
index++;
continue;
}
String fieldId= fieldAccess[0];
int indexOfField= finalFieldList.indexOf(fieldId);
fieldIndex= index;
if (indexOfField > findFinalFieldDeclarationIndex) {
return fieldIndex;
}
fieldIndex++;
index++;
}
return fieldIndex;
}
private int findFirstFinalFieldReferenceIndex(List<ASTNode> astNodes, String variableName) {
int index= 0;
String[] fieldAccess= new String[1];
for (ASTNode astNode : astNodes) {
fieldAccess[0]= null;
ASTVisitor v= new ASTVisitor() {
@Override
public boolean visit(FieldAccess node) {
fieldAccess[0]= node.getName().getIdentifier();
return false;
}
@Override
public boolean visit(SimpleName node) {
IBinding resolveBinding= node.resolveBinding();
if (resolveBinding != null && resolveBinding.getKind() == IBinding.VARIABLE) {
if (((IVariableBinding) resolveBinding).isField()) {
fieldAccess[0]= node.getIdentifier();
return false;
}
}
return true;
}
};
astNode.accept(v);
if (fieldAccess[0] != null && fieldAccess[0].equals(variableName)) {
return index;
}
index++;
}
return -1;
}
private boolean hasThisCall(Block body) {
for (Object object : body.statements()) {
if (((ASTNode) object).getNodeType() == ASTNode.CONSTRUCTOR_INVOCATION) {
return true;
}
}
return false;
}
private boolean hasDefaultConstructor(ITypeBinding type) {
for (IMethodBinding mb : type.getDeclaredMethods()) {
String key= mb.getKey();
if (key.contains(";.()V")) { //$NON-NLS-1$
return true;
}
}
return false;
}
private boolean hasDefaultConstructor() {
return hasDefaultConstructor(fVariableBinding.getType());
}
private FieldDeclaration getFieldDeclaration(String name) {
FieldVisitor fieldVisitor= new FieldVisitor();
fAstNode.getRoot().accept(fieldVisitor);
for (FieldDeclaration f : fieldVisitor.getNodes()) {
for (Object object : f.fragments()) {
VariableDeclarationFragment fragment= (VariableDeclarationFragment) object;
if (fragment.getName().getIdentifier().equals(name)) {
return f;
}
}
}
return null;
}
private SingleVariableDeclaration getRecordComponentDeclaration(String name) {
RecordComponentVisitor recordComponentVisitor= new RecordComponentVisitor();
fAstNode.getRoot().accept(recordComponentVisitor);
for (SingleVariableDeclaration f : recordComponentVisitor.getNodes()) {
if (f.getName().getIdentifier().equals(name)) {
return f;
}
}
return null;
}
private static final class FieldVisitor extends ASTVisitor {
private final List<FieldDeclaration> fNodes= new ArrayList<>();
public FieldVisitor() {
}
@Override
public boolean visit(FieldDeclaration node) {
fNodes.add(node);
return true;
}
public List<FieldDeclaration> getNodes() {
return fNodes;
}
}
private static final class ConstructorVisitor extends ASTVisitor {
private final List<MethodDeclaration> fNodes= new ArrayList<>();
private String fTypeName;
public ConstructorVisitor(String typeName) {
fTypeName= typeName;
}
@Override
public boolean visit(MethodDeclaration node) {
if (node.getName().toString().equals(fTypeName)) {
fNodes.add(node);
}
return true;
}
public List<MethodDeclaration> getNodes() {
return fNodes;
}
}
private static final class RecordComponentVisitor extends ASTVisitor {
private final List<SingleVariableDeclaration> fNodes= new ArrayList<>();
public RecordComponentVisitor() {
}
@Override
public boolean visit(RecordDeclaration node) {
List<SingleVariableDeclaration> decl= node.recordComponents();
if (decl != null)
fNodes.addAll(decl);
return true;
}
public List<SingleVariableDeclaration> getNodes() {
return fNodes;
}
}
}