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