blob: c747bc64a22f9fefac09b94585ad0583577c14bd [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2018 GK Software AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stephan Herrmann - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.correction.proposals;
import java.util.Collection;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EmptyStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
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.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
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.corext.codemanipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.ScopeAnalyzer;
import org.eclipse.jdt.internal.corext.fix.FixMessages;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalPositionGroup;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.preferences.formatter.FormatterProfileManager;
/**
* Fix for field related null-issues:
* <ol>
* <li>{@link IProblem#NullableFieldReference}</li>
* <li>{@link IProblem#RequiredNonNullButProvidedSpecdNullable} <em>if relating to a field</em></li>
* <li>{@link IProblem#RequiredNonNullButProvidedUnknown} <em>if relating to a field</em></li>
* </ol>
* Extract the field reference to a fresh local variable.
* Add a null check for that local variable and move
* the dereference into the then-block of this null-check:
* <pre>
* {@code @Nullable Exception e;}
* void test() {
* e.printStackTrace();
* }</pre>
* will be converted to:
* <pre>
* {@code @Nullable Exception e;}
* void test() {
* final Exception e2 = e;
* if (e2 != null) {
* e2.printStackTrace();
* } else {
* // TODO handle null value
* }
* }</pre>
* <p>
* The <code>final</code> keyword is added to remind the user that writing
* to the local variable has no effect on the original field.</p>
* <p>Rrespects scoping if the problem occurs inside the initialization
* of a local variable (by moving statements into the new then block).</p>
*
* @since 3.9
*/
public class ExtractToNullCheckedLocalProposal extends LinkedCorrectionProposal {
private static final String LOCAL_NAME_POSITION_GROUP = "localName"; //$NON-NLS-1$
/** Protocol for rearranging the bits and pieces into a new code structure. */
private static abstract class RearrangeStrategy {
final Statement origStmt;
final Block block;
final TextEditGroup group;
RearrangeStrategy(Statement origStmt, Block block, TextEditGroup group) {
this.origStmt= origStmt;
this.block= block;
this.group= group;
}
/** Step 1 of the protocol: insert the new local variable.
* @param localDecl new local variable initialized with the original expression */
public abstract void insertLocalDecl(VariableDeclarationStatement localDecl);
/** Step 2 of the protocol: create a move target for repositioning the original enclosing statement.
* @return a move target representing the original statement */
public abstract Statement createMoveTargetForOrigStmt();
/** Step 3 of the protocol: integrate the new if statement into the existing structure.
* @param ifStmt the new if statement, fully created
* @param thenBlock the then statement of the given if statement (is a block by construction) */
public abstract void insertIfStatement(IfStatement ifStmt, Block thenBlock);
public static RearrangeStrategy create(Statement origStmt, ASTRewrite rewrite, TextEditGroup group) {
ASTNode parent = origStmt.getParent();
if (parent instanceof Block) {
Block block= (Block)parent;
if (origStmt instanceof VariableDeclarationStatement)
return new ModifyBlockWithLocalDecl(origStmt, block, rewrite, group);
else
return new ModifyBlock(origStmt, block, rewrite, group);
} else {
return new ReplaceStatement(origStmt, rewrite, group);
}
}
/** Strategy implementation for modifying statement list of the parent block. */
private static class ModifyBlock extends RearrangeStrategy {
final ListRewrite blockRewrite;
ModifyBlock(Statement origStmt, Block enclosingBlock, ASTRewrite rewrite, TextEditGroup group) {
super(origStmt, enclosingBlock, group);
// we're going to modify this block, create the rewrite for this task:
this.blockRewrite= rewrite.getListRewrite(enclosingBlock, Block.STATEMENTS_PROPERTY);
}
@Override
public void insertLocalDecl(VariableDeclarationStatement localDecl) {
this.blockRewrite.insertBefore(localDecl, this.origStmt, this.group);
}
@Override
public Statement createMoveTargetForOrigStmt() {
return (Statement) this.blockRewrite.createMoveTarget(this.origStmt, this.origStmt, null, this.group);
}
@Override
public void insertIfStatement(IfStatement ifStmt, Block thenBlock) {
// inside a block replace old statement with wrapping if-statement
this.blockRewrite.replace(this.origStmt, ifStmt, this.group);
}
}
/** Variant that also respects scoping of an existing local variable declaration. */
private static class ModifyBlockWithLocalDecl extends ModifyBlock {
ModifyBlockWithLocalDecl(Statement origStmt, Block enclosingBlock, ASTRewrite rewrite, TextEditGroup group) {
super(origStmt, enclosingBlock, rewrite, group);
}
@Override
public void insertIfStatement(IfStatement ifStmt, Block thenBlock) {
// when stmt declares a local variable (see RearrangeStrategy.create(..)) we need to move all
// subsequent statements into the then-block to ensure that the existing declared local is visible:
List<ASTNode> blockStmts= this.block.statements();
int stmtIdx= blockStmts.indexOf(this.origStmt);
int lastIdx= blockStmts.size()-1;
if (stmtIdx != -1 && stmtIdx < lastIdx) {
thenBlock.statements()
.add(this.blockRewrite.createMoveTarget(blockStmts.get(stmtIdx+1), blockStmts.get(lastIdx), null, this.group));
}
super.insertIfStatement(ifStmt, thenBlock);
}
}
/** Strategy implementation for replacing a single statement with a new block. */
private static class ReplaceStatement extends RearrangeStrategy {
final ASTRewrite rewrite;
ReplaceStatement(Statement origStmt, ASTRewrite rewrite, TextEditGroup group) {
// did not have a block, create one now to hold new statements:
super(origStmt, rewrite.getAST().newBlock(), group);
this.rewrite = rewrite;
}
@Override
public void insertLocalDecl(VariableDeclarationStatement localDecl) {
this.block.statements().add(localDecl);
}
@Override
public Statement createMoveTargetForOrigStmt() {
return (Statement) this.rewrite.createMoveTarget(this.origStmt); // group is unused
}
@Override
public void insertIfStatement(IfStatement ifStmt, Block thenBlock) {
// did not have a block: add if-statement to new block
this.block.statements().add(ifStmt);
// and replace the single statement with this block
this.rewrite.replace(this.origStmt, this.block, this.group);
}
}
}
private SimpleName fieldReference;
private CompilationUnit compilationUnit;
private ASTNode enclosingMethod; // MethodDeclaration or Initializer
public ExtractToNullCheckedLocalProposal(ICompilationUnit cu, CompilationUnit compilationUnit, SimpleName fieldReference, ASTNode enclosingMethod) {
super(FixMessages.ExtractToNullCheckedLocalProposal_extractToCheckedLocal_proposalName, cu, null, 100, JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE));
this.compilationUnit= compilationUnit;
this.fieldReference= fieldReference;
this.enclosingMethod= enclosingMethod;
}
@Override
protected ASTRewrite getRewrite() throws CoreException {
// infrastructure:
AST ast= this.compilationUnit.getAST();
ASTRewrite rewrite= ASTRewrite.create(ast);
ImportRewrite imports= ImportRewrite.create(this.compilationUnit, true);
TextEditGroup group= new TextEditGroup(FixMessages.ExtractToNullCheckedLocalProposal_extractCheckedLocal_editName);
LinkedProposalPositionGroup localNameGroup= new LinkedProposalPositionGroup(LOCAL_NAME_POSITION_GROUP);
getLinkedProposalModel().addPositionGroup(localNameGroup);
// AST context:
Statement origStmt= ASTNodes.getParent(this.fieldReference, Statement.class);
// determine suitable strategy for rearranging elements towards a new code structure:
RearrangeStrategy rearrangeStrategy= RearrangeStrategy.create(origStmt, rewrite, group);
Expression toReplace;
ASTNode directParent= this.fieldReference.getParent();
if (directParent instanceof FieldAccess) {
toReplace= (Expression) directParent;
} else if (directParent instanceof QualifiedName && this.fieldReference.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
toReplace= (Expression) directParent;
} else {
toReplace= this.fieldReference;
}
// new local declaration initialized from the field reference
VariableDeclarationFragment localFrag = ast.newVariableDeclarationFragment();
VariableDeclarationStatement localDecl= ast.newVariableDeclarationStatement(localFrag);
// ... type
localDecl.setType(newType(toReplace.resolveTypeBinding(), ast, imports));
localDecl.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.FINAL_KEYWORD));
// ... name
String localName= proposeLocalName(this.fieldReference, this.compilationUnit, getCompilationUnit().getJavaProject());
localFrag.setName(ast.newSimpleName(localName));
// ... initialization
localFrag.setInitializer((Expression) ASTNode.copySubtree(ast, toReplace));
rearrangeStrategy.insertLocalDecl(localDecl);
// if statement:
IfStatement ifStmt= ast.newIfStatement();
// condition:
InfixExpression nullCheck= ast.newInfixExpression();
nullCheck.setLeftOperand(ast.newSimpleName(localName));
nullCheck.setRightOperand(ast.newNullLiteral());
nullCheck.setOperator(InfixExpression.Operator.NOT_EQUALS);
ifStmt.setExpression(nullCheck);
// then block: the original statement
Block thenBlock = ast.newBlock();
thenBlock.statements().add(rearrangeStrategy.createMoveTargetForOrigStmt());
ifStmt.setThenStatement(thenBlock);
// ... but with the field reference replaced by the new local:
SimpleName dereferencedName= ast.newSimpleName(localName);
rewrite.replace(toReplace, dereferencedName, group);
// else block: a Todo comment
Block elseBlock = ast.newBlock();
String elseStatement= "// TODO "+FixMessages.ExtractToNullCheckedLocalProposal_todoHandleNullDescription; //$NON-NLS-1$
if (origStmt instanceof ReturnStatement) {
Type returnType= newType(((ReturnStatement)origStmt).getExpression().resolveTypeBinding(), ast, imports);
ReturnStatement returnStatement= ast.newReturnStatement();
returnStatement.setExpression(ASTNodeFactory.newDefaultExpression(ast, returnType, 0));
elseStatement+= '\n' + ASTNodes.asFormattedString(returnStatement, 0, String.valueOf('\n'), FormatterProfileManager.getProjectSettings(getCompilationUnit().getJavaProject()));
}
EmptyStatement todoNode= (EmptyStatement) rewrite.createStringPlaceholder(elseStatement, ASTNode.EMPTY_STATEMENT);
elseBlock.statements().add(todoNode);
ifStmt.setElseStatement(elseBlock);
// link all three occurrences of the new local variable:
addLinkedPosition(rewrite.track(localFrag.getName()), true/*first*/, LOCAL_NAME_POSITION_GROUP);
addLinkedPosition(rewrite.track(nullCheck.getLeftOperand()), false, LOCAL_NAME_POSITION_GROUP);
addLinkedPosition(rewrite.track(dereferencedName), false, LOCAL_NAME_POSITION_GROUP);
rearrangeStrategy.insertIfStatement(ifStmt, thenBlock);
return rewrite;
}
String proposeLocalName(SimpleName fieldName, CompilationUnit root, IJavaProject javaProject) {
// don't propose names that are already in use:
Collection<String> variableNames= new ScopeAnalyzer(root).getUsedVariableNames(this.enclosingMethod.getStartPosition(), this.enclosingMethod.getLength());
String[] names = new String[variableNames.size()+1];
variableNames.toArray(names);
// don't propose the field name itself, either:
String identifier= fieldName.getIdentifier();
names[names.length-1] = identifier;
return StubUtility.getLocalNameSuggestions(javaProject, identifier, 0, names)[0];
}
/**
* Create a fresh type reference
* @param typeBinding the type we want to refer to
* @param ast AST for creating new nodes
* @param imports use this for optimal type names
* @return a fully features non-null type reference (can be parameterized and/or array).
*/
public static Type newType(ITypeBinding typeBinding, AST ast, ImportRewrite imports) {
// unwrap array type:
int dimensions= typeBinding.getDimensions();
if (dimensions > 0)
typeBinding= typeBinding.getElementType();
// unwrap parameterized type:
ITypeBinding[] typeArguments= typeBinding.getTypeArguments();
typeBinding= typeBinding.getErasure();
// create leaf type:
Type elementType = (typeBinding.isPrimitive())
? ast.newPrimitiveType(PrimitiveType.toCode(typeBinding.getName()))
: ast.newSimpleType(ast.newName(imports.addImport(typeBinding)));
// re-wrap as parameterized type:
if (typeArguments.length > 0) {
ParameterizedType parameterizedType= ast.newParameterizedType(elementType);
for (ITypeBinding typeArgument : typeArguments)
parameterizedType.typeArguments().add(newType(typeArgument, ast, imports));
elementType = parameterizedType;
}
// re-wrap as array type:
if (dimensions > 0)
return ast.newArrayType(elementType, dimensions);
else
return elementType;
}
}