View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.design;
5   
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.HashSet;
9   import java.util.List;
10  import java.util.Set;
11  
12  import net.sourceforge.pmd.lang.ast.Node;
13  import net.sourceforge.pmd.lang.java.ast.ASTArgumentList;
14  import net.sourceforge.pmd.lang.java.ast.ASTBlock;
15  import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement;
16  import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
17  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
18  import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
19  import net.sourceforge.pmd.lang.java.ast.ASTIfStatement;
20  import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration;
21  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
22  import net.sourceforge.pmd.lang.java.ast.ASTName;
23  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
24  import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix;
25  import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
26  import net.sourceforge.pmd.lang.java.ast.ASTReferenceType;
27  import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
28  import net.sourceforge.pmd.lang.java.ast.ASTStatementExpression;
29  import net.sourceforge.pmd.lang.java.ast.ASTTryStatement;
30  import net.sourceforge.pmd.lang.java.ast.ASTType;
31  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
32  import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer;
33  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
34  import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
35  import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
36  
37  import org.jaxen.JaxenException;
38  
39  /**
40   * Makes sure you close your database connections. It does this by looking for
41   * code patterned like this:
42   * 
43   * <pre>
44   *  Connection c = X;
45   *  try {
46   *   // do stuff, and maybe catch something
47   *  } finally {
48   *   c.close();
49   *  }
50   * 
51   *  @author original author unknown
52   *  @author Contribution from Pierre Mathien
53   * </pre>
54   */
55  public class CloseResourceRule extends AbstractJavaRule {
56  
57      private Set<String> types = new HashSet<>();
58      private Set<String> simpleTypes = new HashSet<>();
59  
60      private Set<String> closeTargets = new HashSet<>();
61      private static final StringMultiProperty CLOSE_TARGETS_DESCRIPTOR = new StringMultiProperty("closeTargets",
62              "Methods which may close this resource", new String[] {}, 1.0f, ',');
63  
64      private static final StringMultiProperty TYPES_DESCRIPTOR = new StringMultiProperty("types", "Affected types",
65              new String[] { "java.sql.Connection", "java.sql.Statement", "java.sql.ResultSet" }, 2.0f, ',');
66  
67      private static final BooleanProperty USE_CLOSE_AS_DEFAULT_TARGET = new BooleanProperty("closeAsDefaultTarget",
68              "Consider 'close' as a target by default", true, 3.0f);
69  
70      public CloseResourceRule() {
71          definePropertyDescriptor(CLOSE_TARGETS_DESCRIPTOR);
72          definePropertyDescriptor(TYPES_DESCRIPTOR);
73          definePropertyDescriptor(USE_CLOSE_AS_DEFAULT_TARGET);
74      }
75  
76      @Override
77      public Object visit(ASTCompilationUnit node, Object data) {
78          if (closeTargets.isEmpty() && getProperty(CLOSE_TARGETS_DESCRIPTOR) != null) {
79              closeTargets.addAll(Arrays.asList(getProperty(CLOSE_TARGETS_DESCRIPTOR)));
80          }
81          if (getProperty(USE_CLOSE_AS_DEFAULT_TARGET) && !closeTargets.contains("close")) {
82              closeTargets.add("close");
83          }
84          if (types.isEmpty() && getProperty(TYPES_DESCRIPTOR) != null) {
85              types.addAll(Arrays.asList(getProperty(TYPES_DESCRIPTOR)));
86          }
87          if (simpleTypes.isEmpty() && getProperty(TYPES_DESCRIPTOR) != null) {
88              for (String type : getProperty(TYPES_DESCRIPTOR)) {
89                  simpleTypes.add(toSimpleType(type));
90              }
91          }
92          return super.visit(node, data);
93      }
94  
95      private static String toSimpleType(String fullyQualifiedClassName) {
96          int lastIndexOf = fullyQualifiedClassName.lastIndexOf('.');
97          if (lastIndexOf > -1) {
98              return fullyQualifiedClassName.substring(lastIndexOf + 1);
99          } else {
100             return fullyQualifiedClassName;
101         }
102     }
103 
104     @Override
105     public Object visit(ASTConstructorDeclaration node, Object data) {
106         checkForResources(node, data);
107         return data;
108     }
109 
110     @Override
111     public Object visit(ASTMethodDeclaration node, Object data) {
112         checkForResources(node, data);
113         return data;
114     }
115 
116     private void checkForResources(Node node, Object data) {
117         List<ASTLocalVariableDeclaration> vars = node.findDescendantsOfType(ASTLocalVariableDeclaration.class);
118         List<ASTVariableDeclaratorId> ids = new ArrayList<>();
119 
120         // find all variable references to Connection objects
121         for (ASTLocalVariableDeclaration var : vars) {
122             ASTType type = var.getTypeNode();
123 
124             if (type.jjtGetChild(0) instanceof ASTReferenceType) {
125                 ASTReferenceType ref = (ASTReferenceType) type.jjtGetChild(0);
126                 if (ref.jjtGetChild(0) instanceof ASTClassOrInterfaceType) {
127                     ASTClassOrInterfaceType clazz = (ASTClassOrInterfaceType) ref.jjtGetChild(0);
128 
129                     if (clazz.getType() != null && types.contains(clazz.getType().getName()) || clazz.getType() == null
130                             && simpleTypes.contains(toSimpleType(clazz.getImage())) || types.contains(clazz.getImage())) {
131 
132                         ASTVariableDeclaratorId id = var.getFirstDescendantOfType(ASTVariableDeclaratorId.class);
133                         ids.add(id);
134                     }
135                 }
136             }
137         }
138 
139         // if there are connections, ensure each is closed.
140         for (ASTVariableDeclaratorId x : ids) {
141             ensureClosed((ASTLocalVariableDeclaration) x.jjtGetParent().jjtGetParent(), x, data);
142         }
143     }
144 
145     private boolean hasNullInitializer(ASTLocalVariableDeclaration var) {
146         ASTVariableInitializer init = var.getFirstDescendantOfType(ASTVariableInitializer.class);
147         if (init != null) {
148             try {
149                 List<?> nulls = init
150                         .findChildNodesWithXPath("Expression/PrimaryExpression/PrimaryPrefix/Literal/NullLiteral");
151                 return !nulls.isEmpty();
152             } catch (JaxenException e) {
153                 return false;
154             }
155         }
156         return false;
157     }
158 
159     private void ensureClosed(ASTLocalVariableDeclaration var, ASTVariableDeclaratorId id, Object data) {
160         // What are the chances of a Connection being instantiated in a
161         // for-loop init block? Anyway, I'm lazy!
162         String variableToClose = id.getImage();
163         Node n = var;
164 
165         while (!(n instanceof ASTBlock) && !(n instanceof ASTConstructorDeclaration)) {
166             n = n.jjtGetParent();
167         }
168 
169         Node top = n;
170 
171         List<ASTTryStatement> tryblocks = top.findDescendantsOfType(ASTTryStatement.class);
172 
173         boolean closed = false;
174 
175         ASTBlockStatement parentBlock = id.getFirstParentOfType(ASTBlockStatement.class);
176 
177         // look for try blocks below the line the variable was
178         // introduced and make sure there is a .close call in a finally
179         // block.
180         for (ASTTryStatement t : tryblocks) {
181 
182             // verifies that there are no critical statements between the
183             // variable declaration and
184             // the beginning of the try block.
185             ASTBlockStatement tryBlock = t.getFirstParentOfType(ASTBlockStatement.class);
186             if (!hasNullInitializer(var) // no need to check for critical statements, if
187                                          // the variable has been initialized with null
188                 && parentBlock.jjtGetParent() == tryBlock.jjtGetParent()) {
189 
190                 List<ASTBlockStatement> blocks = parentBlock.jjtGetParent().findChildrenOfType(ASTBlockStatement.class);
191                 int parentBlockIndex = blocks.indexOf(parentBlock);
192                 int tryBlockIndex = blocks.indexOf(tryBlock);
193                 boolean criticalStatements = false;
194 
195                 for (int i = parentBlockIndex + 1; i < tryBlockIndex; i++) {
196                     // assume variable declarations are not critical
197                     ASTLocalVariableDeclaration varDecl = blocks.get(i).getFirstDescendantOfType(
198                             ASTLocalVariableDeclaration.class);
199                     if (varDecl == null) {
200                         criticalStatements = true;
201                         break;
202                     }
203                 }
204                 if (criticalStatements) {
205                     break;
206                 }
207             }
208 
209             if (t.getBeginLine() > id.getBeginLine() && t.hasFinally()) {
210                 ASTBlock f = (ASTBlock) t.getFinally().jjtGetChild(0);
211                 List<ASTName> names = f.findDescendantsOfType(ASTName.class);
212                 for (ASTName oName : names) {
213                     String name = oName.getImage();
214                     if (name != null && name.contains(".")) {
215                         String[] parts = name.split("\\.");
216                         if (parts.length == 2) {
217                             String methodName = parts[1];
218                             String varName = parts[0];
219                             if (varName.equals(variableToClose) && closeTargets.contains(methodName)
220                                     && nullCheckIfCondition(f, oName, varName)) {
221                                 closed = true;
222                                 break;
223                             }
224 
225                         }
226                     }
227                 }
228                 if (closed) {
229                     break;
230                 }
231 
232                 List<ASTStatementExpression> exprs = new ArrayList<>();
233                 f.findDescendantsOfType(ASTStatementExpression.class, exprs, true);
234                 for (ASTStatementExpression stmt : exprs) {
235                     ASTPrimaryExpression expr = stmt.getFirstChildOfType(ASTPrimaryExpression.class);
236                     if (expr != null) {
237                         ASTPrimaryPrefix prefix = expr.getFirstChildOfType(ASTPrimaryPrefix.class);
238                         ASTPrimarySuffix suffix = expr.getFirstChildOfType(ASTPrimarySuffix.class);
239                         if (prefix != null && suffix != null) {
240                             if (prefix.getImage() == null) {
241                                 ASTName prefixName = prefix.getFirstChildOfType(ASTName.class);
242                                 if (prefixName != null && closeTargets.contains(prefixName.getImage())) {
243                                     // Found a call to a "close target" that is
244                                     // a direct
245                                     // method call without a "ClassName."
246                                     // prefix.
247                                     closed = variableIsPassedToMethod(expr, variableToClose);
248                                     if (closed) {
249                                         break;
250                                     }
251                                 }
252                             } else if (suffix.getImage() != null) {
253                                 String prefixPlusSuffix = prefix.getImage() + "." + suffix.getImage();
254                                 if (closeTargets.contains(prefixPlusSuffix)) {
255                                     // Found a call to a "close target" that is
256                                     // a method call
257                                     // in the form "ClassName.methodName".
258                                     closed = variableIsPassedToMethod(expr, variableToClose);
259                                     if (closed) {
260                                         break;
261                                     }
262                                 }
263                             }
264                             // look for primary suffix containing the close
265                             // Targets elements.
266                             // If the .close is executed in another class
267                             // accessed by a method
268                             // this form :
269                             // getProviderInstance().closeConnexion(connexion)
270                             // For this use case, we assume the variable is
271                             // correctly closed
272                             // in the other class since there is no way to
273                             // really check it.
274                             if (!closed) {
275                                 List<ASTPrimarySuffix> suffixes = new ArrayList<>();
276                                 expr.findDescendantsOfType(ASTPrimarySuffix.class, suffixes, true);
277                                 for (ASTPrimarySuffix oSuffix : suffixes) {
278                                     String suff = oSuffix.getImage();
279                                     if (closeTargets.contains(suff)) {
280                                         closed = variableIsPassedToMethod(expr, variableToClose);
281                                         if (closed) {
282                                             break;
283                                         }
284                                     }
285 
286                                 }
287                             }
288                         }
289                     }
290                 }
291                 if (closed) {
292                     break;
293                 }
294             }
295         }
296 
297         if (!closed) {
298             // See if the variable is returned by the method, which means the
299             // method is a utility for creating the db resource, which means of
300             // course it can't be closed by the method, so it isn't an error.
301             List<ASTReturnStatement> returns = new ArrayList<>();
302             top.findDescendantsOfType(ASTReturnStatement.class, returns, true);
303             for (ASTReturnStatement returnStatement : returns) {
304                 ASTName name = returnStatement.getFirstDescendantOfType(ASTName.class);
305                 if (name != null && name.getImage().equals(variableToClose)) {
306                     closed = true;
307                     break;
308                 }
309             }
310         }
311 
312         // if all is not well, complain
313         if (!closed) {
314             ASTType type = var.getFirstChildOfType(ASTType.class);
315             ASTReferenceType ref = (ASTReferenceType) type.jjtGetChild(0);
316             ASTClassOrInterfaceType clazz = (ASTClassOrInterfaceType) ref.jjtGetChild(0);
317             addViolation(data, id, clazz.getImage());
318         }
319     }
320 
321     private boolean variableIsPassedToMethod(ASTPrimaryExpression expr, String variable) {
322         List<ASTName> methodParams = new ArrayList<>();
323         expr.findDescendantsOfType(ASTName.class, methodParams, true);
324         for (ASTName pName : methodParams) {
325             String paramName = pName.getImage();
326             // also check if we've got the a parameter (i.e if it's an argument
327             // !)
328             ASTArgumentList parentParam = pName.getFirstParentOfType(ASTArgumentList.class);
329             if (paramName.equals(variable) && parentParam != null) {
330                 return true;
331             }
332         }
333         return false;
334     }
335 
336     private ASTIfStatement findIfStatement(ASTBlock enclosingBlock, Node node) {
337         ASTIfStatement ifStatement = node.getFirstParentOfType(ASTIfStatement.class);
338         List<ASTIfStatement> allIfStatements = enclosingBlock.findDescendantsOfType(ASTIfStatement.class);
339         if (ifStatement != null && allIfStatements.contains(ifStatement)) {
340             return ifStatement;
341         }
342         return null;
343     }
344 
345     /**
346      * Checks, whether the given node is inside a if condition, and if so,
347      * whether this is a null check for the given varName.
348      *
349      * @param enclosingBlock
350      *            where to search for if statements
351      * @param node
352      *            the node, where the call for the close is done
353      * @param varName
354      *            the variable, that is maybe null-checked
355      * @return <code>true</code> if no if condition is involved or if the if
356      *         condition is a null-check.
357      */
358     private boolean nullCheckIfCondition(ASTBlock enclosingBlock, Node node, String varName) {
359         ASTIfStatement ifStatement = findIfStatement(enclosingBlock, node);
360         if (ifStatement != null) {
361             try {
362                 // find expressions like: varName != null or null != varName
363                 List<?> nodes = ifStatement.findChildNodesWithXPath("Expression/EqualityExpression[@Image='!=']"
364                         + "  [PrimaryExpression/PrimaryPrefix/Name[@Image='" + varName + "']]"
365                         + "  [PrimaryExpression/PrimaryPrefix/Literal/NullLiteral]");
366                 return !nodes.isEmpty();
367             } catch (JaxenException e) {
368                 // no boolean literals or other condition
369             }
370         }
371         return true;
372     }
373 }