diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/NullAnnotationQuickFixTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/NullAnnotationQuickFixTest.java index e69de29..553cd59 100644 --- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/NullAnnotationQuickFixTest.java +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/NullAnnotationQuickFixTest.java @@ -0,0 +1,474 @@ +/******************************************************************************* + * Copyright (c) 2012 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.ui.tests.quickfix; + +import java.io.File; +import java.util.ArrayList; +import java.util.Hashtable; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.eclipse.jdt.testplugin.JavaProjectHelper; +import org.eclipse.jdt.testplugin.TestOptions; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; + +import org.eclipse.jface.preference.IPreferenceStore; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; + +import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility; +import org.eclipse.jdt.internal.corext.template.java.CodeTemplateContextType; + +import org.eclipse.jdt.ui.PreferenceConstants; +import org.eclipse.jdt.ui.tests.core.ProjectTestSetup; +import org.eclipse.jdt.ui.text.java.correction.CUCorrectionProposal; + +import org.eclipse.jdt.internal.ui.JavaPlugin; + +public class NullAnnotationQuickFixTest extends QuickFixTest { + + private static final Class THIS= NullAnnotationQuickFixTest.class; + + private IJavaProject fJProject1; + private IPackageFragmentRoot fSourceFolder; + + private String ANNOTATION_JAR_PATH; + + + public NullAnnotationQuickFixTest(String name) { + super(name); + } + + public static Test suite() { + return setUpTest(new TestSuite(THIS)); + } + + public static Test setUpTest(Test test) { + return new ProjectTestSetup(test); + } + + + protected void setUp() throws Exception { + Hashtable options= TestOptions.getDefaultOptions(); + options.put(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaCore.SPACE); + options.put(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, "4"); + options.put(DefaultCodeFormatterConstants.FORMATTER_NUMBER_OF_EMPTY_LINES_TO_PRESERVE, String.valueOf(99)); + options.put(JavaCore.COMPILER_PB_STATIC_ACCESS_RECEIVER, JavaCore.ERROR); + options.put(JavaCore.COMPILER_PB_UNCHECKED_TYPE_OPERATION, JavaCore.IGNORE); + options.put(JavaCore.COMPILER_PB_MISSING_HASHCODE_METHOD, JavaCore.WARNING); + options.put(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, JavaCore.ENABLED); + options.put(JavaCore.COMPILER_PB_NULL_SPECIFICATION_VIOLATION, JavaCore.ERROR); + options.put(JavaCore.COMPILER_PB_NULL_REFERENCE, JavaCore.ERROR); + options.put(JavaCore.COMPILER_PB_POTENTIAL_NULL_REFERENCE, JavaCore.WARNING); + options.put(JavaCore.COMPILER_PB_NULL_ANNOTATION_INFERENCE_CONFLICT, JavaCore.WARNING); + options.put(JavaCore.COMPILER_PB_NULL_UNCHECKED_CONVERSION, JavaCore.WARNING); + options.put(JavaCore.COMPILER_PB_REDUNDANT_NULL_CHECK, JavaCore.WARNING); + + JavaCore.setOptions(options); + + IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore(); + store.setValue(PreferenceConstants.CODEGEN_ADD_COMMENTS, false); + + StubUtility.setCodeTemplate(CodeTemplateContextType.CATCHBLOCK_ID, "", null); + StubUtility.setCodeTemplate(CodeTemplateContextType.CONSTRUCTORSTUB_ID, "", null); + StubUtility.setCodeTemplate(CodeTemplateContextType.METHODSTUB_ID, "", null); + + fJProject1= ProjectTestSetup.getProject(); + + if (this.ANNOTATION_JAR_PATH == null) { + File bundleFile = FileLocator.getBundleFile(Platform.getBundle("org.eclipse.jdt.annotation")); + if (bundleFile.isDirectory()) + this.ANNOTATION_JAR_PATH = bundleFile.getPath()+"/bin"; + else + this.ANNOTATION_JAR_PATH = bundleFile.getPath(); + } + JavaProjectHelper.addLibrary(fJProject1, new Path(ANNOTATION_JAR_PATH)); + + + fSourceFolder= JavaProjectHelper.addSourceContainer(fJProject1, "src"); + } + + + protected void tearDown() throws Exception { + JavaProjectHelper.clear(fJProject1, ProjectTestSetup.getDefaultClasspath()); + } + + // ==== Problem: dereferencing a @Nullable field + // ==== Fix: extract field access to a fresh local variable and add a null-check + + // basic case + public void testExtractNullableField1() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" System.out.println(f.toUpperCase());\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" String f2 = f;\n"); + buf.append(" if (f2 != null) {\n"); + buf.append(" System.out.println(f2.toUpperCase());\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // statement is not element of a block - need to create a new block - local name f2 already in use + public void testExtractNullableField2() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo(boolean b) {\n"); + buf.append(" @SuppressWarnings(\"unused\") boolean f2 = false;\n"); + buf.append(" if (b)\n"); + buf.append(" System.out.println(f.toUpperCase());\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo(boolean b) {\n"); + buf.append(" @SuppressWarnings(\"unused\") boolean f2 = false;\n"); + buf.append(" if (b) {\n"); + buf.append(" String f3 = f;\n"); + buf.append(" if (f3 != null) {\n"); + buf.append(" System.out.println(f3.toUpperCase());\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // field name is part of a qualified field reference - inside a return statement (type: int) + public void testExtractNullableField3() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" int f;\n"); + buf.append(" public int foo(E that) {\n"); + buf.append(" return that.other.f;\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" int f;\n"); + buf.append(" public int foo(E that) {\n"); + buf.append(" E other2 = that.other;\n"); + buf.append(" if (other2 != null) {\n"); + buf.append(" return other2.f;\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" return 0;\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // field name is part of a this-qualified field reference - inside a return statement (type: String) + public void testExtractNullableField4() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public String foo() {\n"); + buf.append(" return this.other.f;\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public String foo() {\n"); + buf.append(" E other2 = this.other;\n"); + buf.append(" if (other2 != null) {\n"); + buf.append(" return other2.f;\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" return null;\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // field referenced inside the rhs of an assignment-as-expression + public void testExtractNullableField5() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" String lo;\n"); + buf.append(" if ((lo = this.other.f) != null)\n"); + buf.append(" System.out.println(lo);\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable E other;\n"); + buf.append(" @Nullable String f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" String lo;\n"); + buf.append(" E other2 = this.other;\n"); + buf.append(" if (other2 != null) {\n"); + buf.append(" if ((lo = other2.f) != null)\n"); + buf.append(" System.out.println(lo);\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // reference to field of array type - dereferenced by f[0] and f.length + public void testExtractNullableField6() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String[] f1;\n"); + buf.append(" @Nullable String[] f2;\n"); + buf.append(" public void foo() {\n"); + buf.append(" System.out.println(f1[0]);\n"); + buf.append(" System.out.println(f2.length);\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot, 2, 0); // get correction for first of two problems + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String[] f1;\n"); + buf.append(" @Nullable String[] f2;\n"); + buf.append(" public void foo() {\n"); + buf.append(" String[] f12 = f1;\n"); + buf.append(" if (f12 != null) {\n"); + buf.append(" System.out.println(f12[0]);\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" System.out.println(f2.length);\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + + proposals= collectCorrections(cu, astRoot, 2, 1); // get correction for second of two problems + assertNumberOfProposals(proposals, 1); + proposal= (CUCorrectionProposal) proposals.get(0); + preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable String[] f1;\n"); + buf.append(" @Nullable String[] f2;\n"); + buf.append(" public void foo() {\n"); + buf.append(" System.out.println(f1[0]);\n"); + buf.append(" String[] f22 = f2;\n"); + buf.append(" if (f22 != null) {\n"); + buf.append(" System.out.println(f22.length);\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // field has a generic type + public void testExtractNullableField7() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("import java.util.List;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable List f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" System.out.println(f.size());\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("import java.util.List;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable List f;\n"); + buf.append(" public void foo() {\n"); + buf.append(" List f2 = f;\n"); + buf.append(" if (f2 != null) {\n"); + buf.append(" System.out.println(f2.size());\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } + + // occurrences inside a class initializer + public void testExtractNullableField8() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable Exception e;\n"); + buf.append(" {\n"); + buf.append(" e.printStackTrace();\n"); + buf.append(" }\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("E.java", buf.toString(), false, null); + + CompilationUnit astRoot= getASTRoot(cu); + ArrayList proposals= collectCorrections(cu, astRoot); + assertNumberOfProposals(proposals, 1); + assertCorrectLabels(proposals); + + CUCorrectionProposal proposal= (CUCorrectionProposal) proposals.get(0); + String preview= getPreviewContent(proposal); + + buf= new StringBuffer(); + buf.append("package test1;\n"); + buf.append("import org.eclipse.jdt.annotation.*;\n"); + buf.append("public class E {\n"); + buf.append(" @Nullable Exception e;\n"); + buf.append(" {\n"); + buf.append(" Exception e2 = e;\n"); + buf.append(" if (e2 != null) {\n"); + buf.append(" e2.printStackTrace();\n"); + buf.append(" } else {\n"); + buf.append(" // TODO handle null value\n"); + buf.append(" }\n"); + buf.append(" }\n"); + buf.append("}\n"); + assertEqualString(preview, buf.toString()); + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ExtractToNullCheckedLocalProposal.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ExtractToNullCheckedLocalProposal.java index e69de29..ff57751 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ExtractToNullCheckedLocalProposal.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ExtractToNullCheckedLocalProposal.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * Copyright (c) 2012 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.fix; + +import java.util.Collection; + +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.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.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.LinkedProposalPositionGroup; + +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jdt.internal.ui.text.correction.proposals.LinkedCorrectionProposal; + +/** + * Fix for {@link IProblem#NullableFieldReference}: + * 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: + *
+ * {@code @Nullable Exception e;}
+ * void test() {
+ *     e.printStackTrace();
+ * }
+ * will be converted to: + *
+ * {@code @Nullable Exception e;}
+ * void test() {
+ *     Exception e2 = e;
+ *     if (e2 != null) {
+ *         e2.printStackTrace();
+ *     } else {
+ *         // TODO handle null value
+ *     }
+ * }
+ * + * @since 3.9 + */ +class ExtractToNullCheckedLocalProposal extends LinkedCorrectionProposal { + + private static final String LOCAL_NAME_POSITION_GROUP = "localName"; //$NON-NLS-1$ + + private SimpleName fieldReference; + private CompilationUnit compilationUnit; + private ASTNode enclosingMethod; // MethodDeclaration or Initializer + + public ExtractToNullCheckedLocalProposal(ICompilationUnit cu, CompilationUnit compilationUnit, SimpleName fieldReference, ASTNode enclosingMethod) { + super(NullFixMessages.NullQuickFixes_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(NullFixMessages.NullQuickFixes_extractCheckedLocal_editName); + LinkedProposalPositionGroup localNameGroup= new LinkedProposalPositionGroup(LOCAL_NAME_POSITION_GROUP); + getLinkedProposalModel().addPositionGroup(localNameGroup); + + // AST context: + Statement stmt= (Statement) ASTNodes.getParent(this.fieldReference, Statement.class); + ASTNode parent= stmt.getParent(); + ListRewrite blockRewrite= null; + Block block; + if (parent instanceof Block) { + // modifying statement list of the parent block + block= (Block) parent; + blockRewrite= rewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY); + } else { + // replacing statement with a new block + block= ast.newBlock(); + } + + 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 local = ast.newVariableDeclarationFragment(); + VariableDeclarationStatement localDecl= ast.newVariableDeclarationStatement(local); + // ... type + localDecl.setType(newType(toReplace.resolveTypeBinding(), ast, imports)); + // ... name + String localName= proposeLocalName(this.fieldReference, this.compilationUnit, getCompilationUnit().getJavaProject()); + local.setName(ast.newSimpleName(localName)); + // ... initialization + local.setInitializer((Expression) ASTNode.copySubtree(ast, toReplace)); + + if (blockRewrite != null) + blockRewrite.insertBefore(localDecl, stmt, group); + else + block.statements().add(localDecl); + + // prepare replacing stmt with a wrapper + Statement statementToMove = (Statement) + (blockRewrite != null ? blockRewrite.createMoveTarget(stmt, stmt) : rewrite.createMoveTarget(stmt)); + + // 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(statementToMove); + 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 "+NullFixMessages.NullQuickFixes_todoHandleNullDescription; //$NON-NLS-1$ + if (stmt instanceof ReturnStatement) { + Type returnType= newType(((ReturnStatement)stmt).getExpression().resolveTypeBinding(), ast, imports); + ReturnStatement returnStatement= ast.newReturnStatement(); + returnStatement.setExpression(ASTNodeFactory.newDefaultExpression(ast, returnType, 0)); + elseStatement+= '\n'+ASTNodes.asFormattedString(returnStatement, 0, String.valueOf('\n'), getCompilationUnit().getJavaProject().getOptions(true)); + } + + ReturnStatement todoNode= (ReturnStatement) rewrite.createStringPlaceholder(elseStatement, ASTNode.RETURN_STATEMENT); + elseBlock.statements().add(todoNode); + ifStmt.setElseStatement(elseBlock); + + // link all three occurrences of the new local variable: + addLinkedPosition(rewrite.track(local.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); + + if (blockRewrite != null) { + // inside a block replace old statement with wrapping if-statement + blockRewrite.replace(stmt, ifStmt, group); + } else { + // did not have a block: add if-statement to new block + block.statements().add(ifStmt); + // and replace the single statement with this block + rewrite.replace(stmt, block, group); + } + + return rewrite; + } + + String proposeLocalName(SimpleName fieldName, CompilationUnit root, IJavaProject javaProject) { + // don't propose names that are already in use: + Collection 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; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java index 75d4c0b..4ef1698 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java @@ -20,6 +20,11 @@ public class NullFixMessages extends NLS { public static String NullAnnotationsCleanUp_add_nullable_annotation; public static String NullAnnotationsCleanUp_add_nonnull_annotation; + public static String NullQuickFixes_extractCheckedLocal_editName; + public static String NullQuickFixes_extractToCheckedLocal_proposalName; + + public static String NullQuickFixes_todoHandleNullDescription; + public static String QuickFixes_add_annotation_change_name; public static String QuickFixes_change_method_parameter_nullness; public static String QuickFixes_change_method_return_nullness; diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties index 1e48568..9617b7f 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties @@ -11,6 +11,10 @@ ############################################################################### NullAnnotationsCleanUp_add_nullable_annotation=Add missing @Nullable annotation NullAnnotationsCleanUp_add_nonnull_annotation=Add missing @NonNull annotation +NullQuickFixes_extractCheckedLocal_editName=Extract checked local +NullQuickFixes_extractToCheckedLocal_proposalName=Extract to checked local variable +NullQuickFixes_todoHandleNullDescription=handle null value + QuickFixes_add_annotation_change_name=Add Annotations QuickFixes_change_method_parameter_nullness=Change parameter type to ''@{0}'' QuickFixes_change_method_return_nullness=Change return type of ''{0}(..)'' to ''@{1}'' diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java index 63f9092..d47e697 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java @@ -29,6 +29,8 @@ import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IBinding; import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.Initializer; +import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.VariableDeclaration; @@ -271,4 +273,24 @@ public class NullQuickFixes { return qualifiedName.substring(lastDot + 1); return qualifiedName; } + + /** + * Fix for {@link IProblem#NullableFieldReference} + * @param context context + * @param problem problem to be fixed + * @param proposals accumulator for computed proposals + */ + public static void addExtractCheckedLocalProposal(IInvocationContext context, IProblemLocation problem, Collection proposals) { + CompilationUnit compilationUnit = context.getASTRoot(); + ICompilationUnit cu= (ICompilationUnit) compilationUnit.getJavaElement(); + + ASTNode selectedNode= problem.getCoveringNode(compilationUnit); + if (selectedNode instanceof SimpleName) { + ASTNode method= ASTNodes.getParent(selectedNode, MethodDeclaration.class); + if (method == null) + method= ASTNodes.getParent(selectedNode, Initializer.class); + if (method != null) + proposals.add(new ExtractToNullCheckedLocalProposal(cu, compilationUnit, (SimpleName) selectedNode, method)); + } + } } diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java index 635ea8f..e9c1b5c 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java @@ -246,6 +246,7 @@ public class QuickFixProcessor implements IQuickFixProcessor { case IProblem.ParameterLackingNullableAnnotation: case IProblem.NonNullLocalVariableComparisonYieldsFalse: case IProblem.RedundantNullCheckOnNonNullLocalVariable: + case IProblem.NullableFieldReference: return true; default: return SuppressWarningsSubProcessor.hasSuppressWarningsProposal(cu.getJavaProject(), problemId); @@ -689,6 +690,9 @@ public class QuickFixProcessor implements IQuickFixProcessor { case IProblem.RedundantNullCheckOnNonNullLocalVariable: NullQuickFixes.addReturnAndArgumentTypeProposal(context, problem, proposals); break; + case IProblem.NullableFieldReference: + NullQuickFixes.addExtractCheckedLocalProposal(context, problem, proposals); + break; default: } if (JavaModelUtil.is50OrHigher(context.getCompilationUnit().getJavaProject())) {