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