Bug 576502 - Add try-with-resources quick fix for unclosed resource
- add new static getTryWithResourceProposals() method
- add quick fix support for unclosed and potentially unclosed
resource errors/warnings to QuickFixProcessor
- refactor some static methods in QuickAssistProcessor to
QuickAssistProcessorUtil
- add new tests to QuickFixTest1d8
Change-Id: If345868a875aecfe2f5e907221e3d07528e877c0
Reviewed-on: https://git.eclipse.org/r/c/jdt/eclipse.jdt.ui/+/186461
Tested-by: JDT Bot <jdt-bot@eclipse.org>
Tested-by: Jeff Johnston <jjohnstn@redhat.com>
Reviewed-by: Jeff Johnston <jjohnstn@redhat.com>
diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessorUtil.java b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessorUtil.java
index c54919f..96c6c13 100644
--- a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessorUtil.java
+++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessorUtil.java
@@ -16,8 +16,11 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.jface.text.link.LinkedPositionGroup;
@@ -25,6 +28,8 @@
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaModelException;
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.ArrayCreation;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Block;
@@ -37,6 +42,7 @@
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.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.MethodReference;
@@ -53,12 +59,14 @@
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclaration;
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.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
+import org.eclipse.jdt.internal.corext.dom.LinkedNodeFinder;
import org.eclipse.jdt.internal.corext.dom.ScopeAnalyzer;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
@@ -385,4 +393,87 @@
rewrite.set(lambda, LambdaExpression.BODY_PROPERTY, blockBody, null);
}
+ /**
+ * Return the first auto closable nodes. When a node that isn't Autoclosable is found the method
+ * returns.
+ *
+ * @param astNodes The nodes covered.
+ * @return List of the first AutoClosable nodes found
+ */
+ public static List<ASTNode> getCoveredAutoClosableNodes(List<ASTNode> astNodes) {
+ List<ASTNode> autoClosableNodes= new ArrayList<>();
+ for (ASTNode astNode : astNodes) {
+ if (isAutoClosable(astNode)) {
+ autoClosableNodes.add(astNode);
+ } else {
+ return autoClosableNodes;
+ }
+ }
+ return autoClosableNodes;
+ }
+
+ public static int findEndPostion(ASTNode node) {
+ int end= node.getStartPosition() + node.getLength();
+ Map<SimpleName, IVariableBinding> nodeSimpleNameBindings= getVariableStatementBinding(node);
+ List<SimpleName> nodeNames= new ArrayList<>(nodeSimpleNameBindings.keySet());
+ if (nodeNames.isEmpty()) {
+ return -1;
+ }
+ SimpleName nodeSimpleName= nodeNames.get(0);
+ SimpleName[] coveredNodeBindings= LinkedNodeFinder.findByNode(node.getRoot(), nodeSimpleName);
+ if (coveredNodeBindings.length == 0) {
+ return -1;
+ }
+ for (ASTNode astNode : coveredNodeBindings) {
+ end= Math.max(end, (astNode.getStartPosition() + astNode.getLength()));
+ }
+ return end;
+ }
+
+ public static Map<SimpleName, IVariableBinding> getVariableStatementBinding(ASTNode astNode) {
+ Map<SimpleName, IVariableBinding> variableBindings= new HashMap<>();
+ astNode.accept(new ASTVisitor() {
+ @Override
+ public boolean visit(VariableDeclarationStatement node) {
+ for (Object o : node.fragments()) {
+ if (o instanceof VariableDeclarationFragment) {
+ VariableDeclarationFragment vdf= (VariableDeclarationFragment) o;
+ SimpleName name= vdf.getName();
+ IBinding binding= name.resolveBinding();
+ if (binding instanceof IVariableBinding) {
+ variableBindings.put(name, (IVariableBinding) binding);
+ break;
+ }
+ }
+ }
+ return false;
+ }
+ });
+ return variableBindings;
+ }
+
+ public static boolean isAutoClosable(ITypeBinding typeBinding) {
+ return Bindings.findTypeInHierarchy(typeBinding, "java.lang.AutoCloseable") != null; //$NON-NLS-1$
+ }
+
+ public static boolean isAutoClosable(ASTNode astNode) {
+ Map<SimpleName, IVariableBinding> simpleNames= getVariableStatementBinding(astNode);
+ for (Entry<SimpleName, IVariableBinding> entry : simpleNames.entrySet()) {
+ ITypeBinding typeBinding= null;
+ switch (entry.getKey().getParent().getNodeType()) {
+ case ASTNode.VARIABLE_DECLARATION_FRAGMENT:
+ case ASTNode.VARIABLE_DECLARATION_STATEMENT:
+ case ASTNode.ASSIGNMENT:
+ typeBinding= entry.getValue().getType();
+ break;
+ default:
+ continue;
+ }
+ if (typeBinding != null && isAutoClosable(typeBinding)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest1d8.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest1d8.java
index 17e8a99..1ba051a 100644
--- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest1d8.java
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest1d8.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2013, 2020 IBM Corporation and others.
+ * Copyright (c) 2013, 2021 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -40,6 +40,7 @@
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.core.manipulation.CodeTemplateContextType;
import org.eclipse.jdt.internal.core.manipulation.StubUtility;
@@ -71,7 +72,6 @@
options.put(DefaultCodeFormatterConstants.FORMATTER_INSERT_NEW_LINE_AFTER_ANNOTATION_ON_LOCAL_VARIABLE, JavaCore.DO_NOT_INSERT);
options.put(DefaultCodeFormatterConstants.FORMATTER_ALIGNMENT_FOR_ANNOTATIONS_ON_LOCAL_VARIABLE, DefaultCodeFormatterConstants.createAlignmentValue(false, DefaultCodeFormatterConstants.WRAP_NO_SPLIT));
-
JavaCore.setOptions(options);
IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
@@ -2374,4 +2374,140 @@
assertExpectedExistInProposals(proposals, new String[] {expected});
}
+ // bug 576502 : try-with-resources quick fix should be offered for resource leaks
+ @Test
+ public void testBug576502_potentiallyUnclosedCloseable() throws Exception {
+ Hashtable<String, String> options = JavaCore.getOptions();
+ options.put(CompilerOptions.OPTION_ReportPotentiallyUnclosedCloseable, CompilerOptions.ERROR);
+ JavaCore.setOptions(options);
+ IPackageFragment pack2= fSourceFolder.createPackageFragment("test1", false, null);
+
+
+ StringBuilder buf= new StringBuilder();
+ buf.append("package test1;\n");
+ buf.append("import java.io.File;\n");
+ buf.append("import java.io.FileReader;\n");
+ buf.append("import java.io.IOException;\n");
+ buf.append("class E {\n");
+ buf.append(" public void foo() throws IOException {\n");
+ buf.append(" final File file = new File(\"somefile\");\n");
+ buf.append(" FileReader fileReader = new FileReader(file);\n");
+ buf.append(" char[] in = new char[50];\n");
+ buf.append(" fileReader.read(in);\n");
+ buf.append(" new Runnable() {\n");
+ buf.append(" public void run() { \n");
+ buf.append(" try {\n");
+ buf.append(" fileReader.close();\n");
+ buf.append(" } catch (IOException ex) { /* nop */ }\n");
+ buf.append(" }}.run();\n");
+ buf.append(" }\n");
+ buf.append("}\n");
+ buf.append("");
+
+
+ ICompilationUnit cu= pack2.createCompilationUnit("E.java", buf.toString(), false, null);
+
+ CompilationUnit astRoot= getASTRoot(cu);
+ ArrayList<IJavaCompletionProposal> proposals= collectCorrections(cu, astRoot);
+ assertCorrectLabels(proposals);
+
+ buf= new StringBuilder();
+ buf.append("package test1;\n");
+ buf.append("import java.io.File;\n");
+ buf.append("import java.io.FileReader;\n");
+ buf.append("import java.io.IOException;\n");
+ buf.append("class E {\n");
+ buf.append(" public void foo() throws IOException {\n");
+ buf.append(" final File file = new File(\"somefile\");\n");
+ buf.append(" try (FileReader fileReader = new FileReader(file)) {\n");
+ buf.append(" char[] in = new char[50];\n");
+ buf.append(" fileReader.read(in);\n");
+ buf.append(" new Runnable() {\n");
+ buf.append(" public void run() { \n");
+ buf.append(" try {\n");
+ buf.append(" fileReader.close();\n");
+ buf.append(" } catch (IOException ex) { /* nop */ }\n");
+ buf.append(" }}.run();\n");
+ buf.append(" }\n");
+ buf.append(" }\n");
+ buf.append("}\n");
+ buf.append("");
+ String expected= buf.toString();
+
+ assertExpectedExistInProposals(proposals, new String[] {expected});
+ }
+
+ // bug 576502 : try-with-resources quick fix should be offered for resource leaks
+ @Test
+ public void testBug576502_unclosedCloseable() throws Exception {
+ Hashtable<String, String> options = JavaCore.getOptions();
+ options.put(CompilerOptions.OPTION_ReportUnclosedCloseable, CompilerOptions.ERROR);
+ JavaCore.setOptions(options);
+ IPackageFragment pack2= fSourceFolder.createPackageFragment("test1", false, null);
+
+
+ StringBuilder buf= new StringBuilder();
+ buf.append("package test1;\n");
+ buf.append("import java.io.File;\n");
+ buf.append("import java.io.FileReader;\n");
+ buf.append("import java.io.IOException;\n");
+ buf.append("class E {\n");
+ buf.append(" public void foo(boolean f1, boolean f2) throws IOException {\n");
+ buf.append(" File file = new File(\"somefile\");\n");
+ buf.append(" if (f1) {\n");
+ buf.append(" FileReader fileReader = new FileReader(file); // err: not closed\n");
+ buf.append(" char[] in = new char[50];\n");
+ buf.append(" fileReader.read(in);\n");
+ buf.append(" while (true) {\n");
+ buf.append(" FileReader loopReader = new FileReader(file); // don't warn, properly closed\n");
+ buf.append(" loopReader.close();\n");
+ buf.append(" break;\n");
+ buf.append(" }\n");
+ buf.append(" } else {\n");
+ buf.append(" FileReader fileReader = new FileReader(file); // warn: not closed on all paths\n");
+ buf.append(" if (f2)\n");
+ buf.append(" fileReader.close();\n");
+ buf.append(" }\n");
+ buf.append(" }\n");
+ buf.append("}\n");
+ buf.append("");
+
+
+ ICompilationUnit cu= pack2.createCompilationUnit("E.java", buf.toString(), false, null);
+
+ CompilationUnit astRoot= getASTRoot(cu);
+ ArrayList<IJavaCompletionProposal> proposals= collectCorrections(cu, astRoot);
+ assertCorrectLabels(proposals);
+
+ buf= new StringBuilder();
+ buf.append("package test1;\n");
+ buf.append("import java.io.File;\n");
+ buf.append("import java.io.FileReader;\n");
+ buf.append("import java.io.IOException;\n");
+ buf.append("class E {\n");
+ buf.append(" public void foo(boolean f1, boolean f2) throws IOException {\n");
+ buf.append(" File file = new File(\"somefile\");\n");
+ buf.append(" if (f1) {\n");
+ buf.append(" try (FileReader fileReader = new FileReader(file)) {\n");
+ buf.append(" char[] in = new char[50];\n");
+ buf.append(" fileReader.read(in);\n");
+ buf.append(" }\n");
+ buf.append(" while (true) {\n");
+ buf.append(" FileReader loopReader = new FileReader(file); // don't warn, properly closed\n");
+ buf.append(" loopReader.close();\n");
+ buf.append(" break;\n");
+ buf.append(" }\n");
+ buf.append(" } else {\n");
+ buf.append(" FileReader fileReader = new FileReader(file); // warn: not closed on all paths\n");
+ buf.append(" if (f2)\n");
+ buf.append(" fileReader.close();\n");
+ buf.append(" }\n");
+ buf.append(" }\n");
+ buf.append("}\n");
+ buf.append("");
+ String expected= buf.toString();
+
+ assertExpectedExistInProposals(proposals, new String[] {expected});
+ }
+
}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/LocalCorrectionsSubProcessor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/LocalCorrectionsSubProcessor.java
index 7821c95..2f96238 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/LocalCorrectionsSubProcessor.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/LocalCorrectionsSubProcessor.java
@@ -2443,6 +2443,18 @@
}
}
+ public static void getTryWithResourceProposals(IInvocationContext context, IProblemLocation problem, Collection<ICommandAccess> proposals) {
+ ASTNode coveringNode= problem.getCoveringNode(context.getASTRoot());
+ if (coveringNode != null) {
+ try {
+ ArrayList<ASTNode> coveredNodes= AdvancedQuickAssistProcessor.getFullyCoveredNodes(context, coveringNode);
+ QuickAssistProcessor.getTryWithResourceProposals(context, coveringNode, coveredNodes, proposals);
+ } catch (IllegalArgumentException | CoreException e) {
+ // do nothing
+ }
+ }
+ }
+
public static void getConvertLambdaToAnonymousClassCreationsProposals(IInvocationContext context, IProblemLocation problem, Collection<ICommandAccess> proposals) {
ASTNode coveringNode= problem.getCoveringNode(context.getASTRoot());
if (coveringNode != null) {
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessor.java
index 4d500e3..9c8c068 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessor.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickAssistProcessor.java
@@ -30,7 +30,6 @@
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import org.eclipse.swt.graphics.Image;
@@ -2227,7 +2226,7 @@
localProposal.setCommandId(ASSIGN_TO_LOCAL_ID);
resultingCollections.add(localProposal);
- if (isAutoClosable(typeBinding)) {
+ if (QuickAssistProcessorUtil.isAutoClosable(typeBinding)) {
AssignToVariableAssistProposal tryWithResourcesProposal= new AssignToVariableAssistProposal(cu, AssignToVariableAssistProposal.TRY_WITH_RESOURCES, expressionStatement, typeBinding,
IProposalRelevance.ASSIGN_IN_TRY_WITH_RESOURCES);
tryWithResourcesProposal.setCommandId(ASSIGN_IN_TRY_WITH_RESOURCES_ID);
@@ -3080,7 +3079,7 @@
}
}
}
- List<ASTNode> coveredAutoClosableNodes= getCoveredAutoClosableNodes(coveredStatements);
+ List<ASTNode> coveredAutoClosableNodes= QuickAssistProcessorUtil.getCoveredAutoClosableNodes(coveredStatements);
if (coveredAutoClosableNodes.isEmpty()) {
return false;
}
@@ -3093,7 +3092,7 @@
int end= start;
for (ASTNode astNode : coveredAutoClosableNodes) {
- int endPosition= findEndPostion(astNode);
+ int endPosition= QuickAssistProcessorUtil.findEndPostion(astNode);
end= Math.max(end, endPosition);
}
@@ -3103,7 +3102,7 @@
while (true) {
int newEnd= oldEnd;
for (ASTNode astNode : nodesInRange) {
- int endPosition= findEndPostion(astNode);
+ int endPosition= QuickAssistProcessorUtil.findEndPostion(astNode);
newEnd= Math.max(newEnd, endPosition);
}
if (newEnd > oldEnd) {
@@ -3302,89 +3301,6 @@
return true;
}
- private static int findEndPostion(ASTNode node) {
- int end= node.getStartPosition() + node.getLength();
- Map<SimpleName, IVariableBinding> nodeSimpleNameBindings= getVariableStatementBinding(node);
- List<SimpleName> nodeNames= new ArrayList<>(nodeSimpleNameBindings.keySet());
- if (nodeNames.isEmpty()) {
- return -1;
- }
- SimpleName nodeSimpleName= nodeNames.get(0);
- SimpleName[] coveredNodeBindings= LinkedNodeFinder.findByNode(node.getRoot(), nodeSimpleName);
- if (coveredNodeBindings.length == 0) {
- return -1;
- }
- for (ASTNode astNode : coveredNodeBindings) {
- end= Math.max(end, (astNode.getStartPosition() + astNode.getLength()));
- }
- return end;
- }
-
- /**
- * Return the first auto closable nodes. When a node that isn't Autoclosable is found the method
- * returns.
- *
- * @param astNodes The nodes covered.
- * @return List of the first AutoClosable nodes found
- */
- private static List<ASTNode> getCoveredAutoClosableNodes(List<ASTNode> astNodes) {
- List<ASTNode> autoClosableNodes= new ArrayList<>();
- for (ASTNode astNode : astNodes) {
- if (isAutoClosable(astNode)) {
- autoClosableNodes.add(astNode);
- } else {
- return autoClosableNodes;
- }
- }
- return autoClosableNodes;
- }
-
- private static boolean isAutoClosable(ASTNode astNode) {
- Map<SimpleName, IVariableBinding> simpleNames= getVariableStatementBinding(astNode);
- for (Entry<SimpleName, IVariableBinding> entry : simpleNames.entrySet()) {
- ITypeBinding typeBinding= null;
- switch (entry.getKey().getParent().getNodeType()) {
- case ASTNode.VARIABLE_DECLARATION_FRAGMENT:
- case ASTNode.VARIABLE_DECLARATION_STATEMENT:
- case ASTNode.ASSIGNMENT:
- typeBinding= entry.getValue().getType();
- break;
- default:
- continue;
- }
- if (typeBinding != null && isAutoClosable(typeBinding)) {
- return true;
- }
- }
- return false;
- }
-
- private static boolean isAutoClosable(ITypeBinding typeBinding) {
- return Bindings.findTypeInHierarchy(typeBinding, "java.lang.AutoCloseable") != null; //$NON-NLS-1$
- }
-
- private static Map<SimpleName, IVariableBinding> getVariableStatementBinding(ASTNode astNode) {
- Map<SimpleName, IVariableBinding> variableBindings= new HashMap<>();
- astNode.accept(new ASTVisitor() {
- @Override
- public boolean visit(VariableDeclarationStatement node) {
- for (Object o : node.fragments()) {
- if (o instanceof VariableDeclarationFragment) {
- VariableDeclarationFragment vdf= (VariableDeclarationFragment) o;
- SimpleName name= vdf.getName();
- IBinding binding= name.resolveBinding();
- if (binding instanceof IVariableBinding) {
- variableBindings.put(name, (IVariableBinding) binding);
- break;
- }
- }
- }
- return false;
- }
- });
- return variableBindings;
- }
-
private static boolean getAddBlockProposals(IInvocationContext context, ASTNode node, Collection<ICommandAccess> resultingCollections) {
if (!(node instanceof Statement)) {
return false;
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 dbea1b0..6dc44e3 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
@@ -320,6 +320,8 @@
case IProblem.FeatureNotSupported:
case IProblem.SwitchExpressionsReturnWithinSwitchExpression:
case IProblem.DanglingReference:
+ case IProblem.UnclosedCloseable:
+ case IProblem.PotentiallyUnclosedCloseable:
return true;
default:
return SuppressWarningsSubProcessor.hasSuppressWarningsProposal(cu.getJavaProject(), problemId)
@@ -903,6 +905,10 @@
case IProblem.SwitchExpressionsReturnWithinSwitchExpression:
ReturnTypeSubProcessor.replaceReturnWithYieldStatementProposals(context, problem, proposals);
break;
+ case IProblem.UnclosedCloseable:
+ case IProblem.PotentiallyUnclosedCloseable:
+ LocalCorrectionsSubProcessor.getTryWithResourceProposals(context, problem, proposals);
+ break;
default:
}
if (JavaModelUtil.is50OrHigher(context.getCompilationUnit().getJavaProject())) {