View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.plsql.rule.codesize;
5   
6   import java.util.ArrayList;
7   import java.util.List;
8   import java.util.logging.Level;
9   import java.util.logging.Logger;
10  
11  import net.sourceforge.pmd.lang.plsql.ast.ASTCaseStatement;
12  import net.sourceforge.pmd.lang.plsql.ast.ASTCaseWhenClause;
13  import net.sourceforge.pmd.lang.plsql.ast.ASTConditionalAndExpression;
14  import net.sourceforge.pmd.lang.plsql.ast.ASTConditionalOrExpression;
15  import net.sourceforge.pmd.lang.plsql.ast.ASTElseClause;
16  import net.sourceforge.pmd.lang.plsql.ast.ASTElsifClause;
17  import net.sourceforge.pmd.lang.plsql.ast.ASTExpression;
18  import net.sourceforge.pmd.lang.plsql.ast.ASTForStatement;
19  import net.sourceforge.pmd.lang.plsql.ast.ASTIfStatement;
20  import net.sourceforge.pmd.lang.plsql.ast.ASTLoopStatement;
21  import net.sourceforge.pmd.lang.plsql.ast.ASTMethodDeclaration;
22  import net.sourceforge.pmd.lang.plsql.ast.ASTProgramUnit;
23  import net.sourceforge.pmd.lang.plsql.ast.ASTReturnStatement;
24  import net.sourceforge.pmd.lang.plsql.ast.ASTStatement;
25  import net.sourceforge.pmd.lang.plsql.ast.ASTTriggerTimingPointSection;
26  import net.sourceforge.pmd.lang.plsql.ast.ASTTriggerUnit;
27  import net.sourceforge.pmd.lang.plsql.ast.ASTTypeMethod;
28  import net.sourceforge.pmd.lang.plsql.ast.ASTWhileStatement;
29  import net.sourceforge.pmd.lang.plsql.ast.ExecutableCode;
30  import net.sourceforge.pmd.lang.plsql.ast.PLSQLNode;
31  import net.sourceforge.pmd.lang.plsql.rule.AbstractStatisticalPLSQLRule;
32  import net.sourceforge.pmd.stat.DataPoint;
33  import net.sourceforge.pmd.util.NumericConstants;
34  
35  /**
36   * NPath complexity is a measurement of the acyclic execution paths through a
37   * function. See Nejmeh, Communications of the ACM Feb 1988 pp 188-200.
38   *
39   * @author Jason Bennett
40   */
41  public class NPathComplexityRule extends AbstractStatisticalPLSQLRule {
42      private final static String CLASS_NAME = NPathComplexityRule.class.getCanonicalName();
43      private final static Logger LOGGER = Logger.getLogger(NPathComplexityRule.class.getName());
44  
45      public NPathComplexityRule() {
46          super();
47          setProperty(MINIMUM_DESCRIPTOR, 200d);
48      }
49  
50      private int complexityMultipleOf(PLSQLNode node, int npathStart, Object data) {
51          LOGGER.entering(CLASS_NAME, "complexityMultipleOf(SimpleNode)");
52  
53          int npath = npathStart;
54          PLSQLNode n;
55  
56          for (int i = 0; i < node.jjtGetNumChildren(); i++) {
57              n = (PLSQLNode) node.jjtGetChild(i);
58              npath *= (Integer) n.jjtAccept(this, data);
59          }
60  
61          LOGGER.exiting(CLASS_NAME, "complexityMultipleOf(SimpleNode)", npath);
62          return npath;
63      }
64  
65      @Override
66      public Object visit(ASTMethodDeclaration node, Object data) {
67          LOGGER.entering(CLASS_NAME, "visit(ASTMethodDeclaration)");
68          int npath = complexityMultipleOf(node, 1, data);
69  
70          DataPoint point = new DataPoint();
71          point.setNode(node);
72          point.setScore(1.0 * npath);
73          point.setMessage(getMessage());
74          addDataPoint(point);
75  
76          if (LOGGER.isLoggable(Level.FINEST)) {
77              LOGGER.finest("NPath complexity:  " + npath + " for line " + node.getBeginLine() + ", column "
78                      + node.getBeginColumn());
79          }
80          LOGGER.exiting(CLASS_NAME, "visit(ASTMethodDeclaration)", npath);
81          return Integer.valueOf(npath);
82      }
83  
84      @Override
85      public Object visit(ASTProgramUnit node, Object data) {
86          LOGGER.entering(CLASS_NAME, "visit(ASTProgramUnit)");
87          int npath = complexityMultipleOf(node, 1, data);
88  
89          DataPoint point = new DataPoint();
90          point.setNode(node);
91          point.setScore(1.0 * npath);
92          point.setMessage(getMessage());
93          addDataPoint(point);
94  
95          if (LOGGER.isLoggable(Level.FINEST)) {
96              LOGGER.finest("NPath complexity:  " + npath + " for line " + node.getBeginLine() + ", column "
97                      + node.getBeginColumn());
98          }
99          LOGGER.exiting(CLASS_NAME, "visit(ASTProgramUnit)", npath);
100         return Integer.valueOf(npath);
101     }
102 
103     @Override
104     public Object visit(ASTTypeMethod node, Object data) {
105         LOGGER.entering(CLASS_NAME, "visit(ASTTypeMethod)");
106         int npath = complexityMultipleOf(node, 1, data);
107 
108         DataPoint point = new DataPoint();
109         point.setNode(node);
110         point.setScore(1.0 * npath);
111         point.setMessage(getMessage());
112         addDataPoint(point);
113 
114         if (LOGGER.isLoggable(Level.FINEST)) {
115             LOGGER.finest("NPath complexity:  " + npath + " for line " + node.getBeginLine() + ", column "
116                     + node.getBeginColumn());
117         }
118         LOGGER.exiting(CLASS_NAME, "visit(ASTTypeMethod)", npath);
119         return Integer.valueOf(npath);
120     }
121 
122     @Override
123     public Object visit(ASTTriggerUnit node, Object data) {
124         LOGGER.entering(CLASS_NAME, "visit(ASTTriggerUnit)");
125         int npath = complexityMultipleOf(node, 1, data);
126 
127         DataPoint point = new DataPoint();
128         point.setNode(node);
129         point.setScore(1.0 * npath);
130         point.setMessage(getMessage());
131         addDataPoint(point);
132 
133         if (LOGGER.isLoggable(Level.FINEST)) {
134             LOGGER.finest("NPath complexity:  " + npath + " for line " + node.getBeginLine() + ", column "
135                     + node.getBeginColumn());
136         }
137         LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerUnit)", npath);
138         return Integer.valueOf(npath);
139     }
140 
141     @Override
142     public Object visit(ASTTriggerTimingPointSection node, Object data) {
143         LOGGER.entering(CLASS_NAME, "visit(ASTTriggerTimingPointSection)");
144         int npath = complexityMultipleOf(node, 1, data);
145 
146         DataPoint point = new DataPoint();
147         point.setNode(node);
148         point.setScore(1.0 * npath);
149         point.setMessage(getMessage());
150         addDataPoint(point);
151 
152         if (LOGGER.isLoggable(Level.FINEST)) {
153             LOGGER.finest("NPath complexity:  " + npath + " for line " + node.getBeginLine() + ", column "
154                     + node.getBeginColumn());
155         }
156         LOGGER.exiting(CLASS_NAME, "visit(ASTTriggerTimingPointSection)", npath);
157         return Integer.valueOf(npath);
158     }
159 
160     @Override
161     public Object visit(PLSQLNode node, Object data) {
162         LOGGER.entering(CLASS_NAME, "visit(SimpleNode)");
163         int npath = complexityMultipleOf(node, 1, data);
164         LOGGER.exiting(CLASS_NAME, "visit(SimpleNode)", npath);
165         return Integer.valueOf(npath);
166     }
167 
168     @Override
169     public Object visit(ASTIfStatement node, Object data) {
170         LOGGER.entering(CLASS_NAME, "visit(ASTIfStatement)");
171         // (npath of if + npath of else (or 1) + bool_comp of if) * npath of
172         // next
173 
174         int boolCompIf = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
175 
176         int complexity = 0;
177 
178         List<PLSQLNode> statementChildren = new ArrayList<>();
179         for (int i = 0; i < node.jjtGetNumChildren(); i++) {
180             if (node.jjtGetChild(i).getClass() == ASTStatement.class
181                     || node.jjtGetChild(i).getClass() == ASTElsifClause.class
182                     || node.jjtGetChild(i).getClass() == ASTElseClause.class) {
183                 statementChildren.add((PLSQLNode) node.jjtGetChild(i));
184             }
185         }
186         if (LOGGER.isLoggable(Level.FINEST)) {
187             LOGGER.finest(statementChildren.size() + " statementChildren found for IF statement " + node.getBeginLine()
188                     + ", column " + node.getBeginColumn());
189         }
190 
191         /*
192          * SRT if (statementChildren.isEmpty() || statementChildren.size() == 1
193          * && ( null != node.getFirstChildOfType(ASTElseClause.class) )
194          * //.hasElse() || statementChildren.size() != 1 && ( null ==
195          * node.getFirstChildOfType(ASTElseClause.class) ) // !node.hasElse() )
196          * { throw new
197          * IllegalStateException("If node has wrong number of children"); }
198          */
199 
200         /*
201          * @TODO Any explicit Elsif clause(s) and Else clause are included in
202          * the list of statements // add path for not taking if if (null ==
203          * node.getFirstChildOfType(ASTElsifClause.class) ) //
204          * !node.hasElse()!node.hasElse()) { complexity++; }
205          * 
206          * if (null == node.getFirstChildOfType(ASTElseClause.class) ) //
207          * !node.hasElse()!node.hasElse()) { complexity++; }
208          */
209 
210         for (PLSQLNode element : statementChildren) {
211             complexity += (Integer) element.jjtAccept(this, data);
212         }
213 
214         LOGGER.exiting(CLASS_NAME, "visit(ASTIfStatement)", boolCompIf + complexity);
215         return Integer.valueOf(boolCompIf + complexity);
216     }
217 
218     @Override
219     public Object visit(ASTElsifClause node, Object data) {
220         LOGGER.entering(CLASS_NAME, "visit(ASTElsifClause)");
221         // (npath of if + npath of else (or 1) + bool_comp of if) * npath of
222         // next
223 
224         int boolCompIf = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
225 
226         int complexity = 0;
227 
228         List<PLSQLNode> statementChildren = new ArrayList<>();
229         for (int i = 0; i < node.jjtGetNumChildren(); i++) {
230             if (node.jjtGetChild(i).getClass() == ASTStatement.class) {
231                 statementChildren.add((PLSQLNode) node.jjtGetChild(i));
232             }
233         }
234         if (LOGGER.isLoggable(Level.FINEST)) {
235             LOGGER.finest(statementChildren.size() + " statementChildren found for ELSIF statement "
236                     + node.getBeginLine() + ", column " + node.getBeginColumn());
237         }
238 
239         /*
240          * SRT if (statementChildren.isEmpty() || statementChildren.size() == 1
241          * && ( null != node.getFirstChildOfType(ASTElseClause.class) )
242          * //.hasElse() || statementChildren.size() != 1 && ( null ==
243          * node.getFirstChildOfType(ASTElseClause.class) ) // !node.hasElse() )
244          * { throw new
245          * IllegalStateException("If node has wrong number of children"); }
246          */
247 
248         for (PLSQLNode element : statementChildren) {
249             complexity += (Integer) element.jjtAccept(this, data);
250         }
251 
252         LOGGER.exiting(CLASS_NAME, "visit(ASTElsifClause)", boolCompIf + complexity);
253         return Integer.valueOf(boolCompIf + complexity);
254     }
255 
256     @Override
257     public Object visit(ASTElseClause node, Object data) {
258         LOGGER.entering(CLASS_NAME, "visit(ASTElseClause)");
259         // (npath of if + npath of else (or 1) + bool_comp of if) * npath of
260         // next
261 
262         int complexity = 0;
263 
264         List<PLSQLNode> statementChildren = new ArrayList<>();
265         for (int i = 0; i < node.jjtGetNumChildren(); i++) {
266             if (node.jjtGetChild(i).getClass() == ASTStatement.class) {
267                 statementChildren.add((PLSQLNode) node.jjtGetChild(i));
268             }
269         }
270         if (LOGGER.isLoggable(Level.FINEST)) {
271             LOGGER.finest(statementChildren.size() + " statementChildren found for ELSE clause statement "
272                     + node.getBeginLine() + ", column " + node.getBeginColumn());
273         }
274 
275         for (PLSQLNode element : statementChildren) {
276             complexity += (Integer) element.jjtAccept(this, data);
277         }
278 
279         LOGGER.exiting(CLASS_NAME, "visit(ASTElseClause)", complexity);
280         return Integer.valueOf(complexity);
281     }
282 
283     @Override
284     public Object visit(ASTWhileStatement node, Object data) {
285         LOGGER.entering(CLASS_NAME, "visit(ASTWhileStatement)");
286         // (npath of while + bool_comp of while + 1) * npath of next
287 
288         int boolCompWhile = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
289 
290         Integer nPathWhile = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data);
291 
292         LOGGER.exiting(CLASS_NAME, "visit(ASTWhileStatement)", boolCompWhile + nPathWhile + 1);
293         return Integer.valueOf(boolCompWhile + nPathWhile + 1);
294     }
295 
296     @Override
297     public Object visit(ASTLoopStatement node, Object data) {
298         LOGGER.entering(CLASS_NAME, "visit(ASTLoopStatement)");
299         // (npath of do + bool_comp of do + 1) * npath of next
300 
301         int boolCompDo = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
302 
303         Integer nPathDo = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data);
304 
305         LOGGER.exiting(CLASS_NAME, "visit(ASTLoopStatement)", boolCompDo + nPathDo + 1);
306         return Integer.valueOf(boolCompDo + nPathDo + 1);
307     }
308 
309     @Override
310     public Object visit(ASTForStatement node, Object data) {
311         LOGGER.entering(CLASS_NAME, "visit(ASTForStatement)");
312         // (npath of for + bool_comp of for + 1) * npath of next
313 
314         int boolCompFor = sumExpressionComplexity(node.getFirstDescendantOfType(ASTExpression.class));
315 
316         Integer nPathFor = (Integer) ((PLSQLNode) node.getFirstChildOfType(ASTStatement.class)).jjtAccept(this, data);
317 
318         LOGGER.exiting(CLASS_NAME, "visit(ASTForStatement)", boolCompFor + nPathFor + 1);
319         return Integer.valueOf(boolCompFor + nPathFor + 1);
320     }
321 
322     @Override
323     public Object visit(ASTReturnStatement node, Object data) {
324         LOGGER.entering(CLASS_NAME, "visit(ASTReturnStatement)");
325         // return statements are valued at 1, or the value of the boolean
326         // expression
327 
328         ASTExpression expr = node.getFirstChildOfType(ASTExpression.class);
329 
330         if (expr == null) {
331             return NumericConstants.ONE;
332         }
333 
334         int boolCompReturn = sumExpressionComplexity(expr);
335         int conditionalExpressionComplexity = complexityMultipleOf(expr, 1, data);
336 
337         if (conditionalExpressionComplexity > 1) {
338             boolCompReturn += conditionalExpressionComplexity;
339         }
340 
341         if (boolCompReturn > 0) {
342             return Integer.valueOf(boolCompReturn);
343         }
344         LOGGER.entering(CLASS_NAME, "visit(ASTReturnStatement)", NumericConstants.ONE);
345         return NumericConstants.ONE;
346     }
347 
348     @Override
349     public Object visit(ASTCaseWhenClause node, Object data) {
350         LOGGER.entering(CLASS_NAME, "visit(ASTCaseWhenClause)");
351         // bool_comp of switch + sum(npath(case_range))
352 
353         int boolCompSwitch = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
354 
355         int npath = 1;
356         int caseRange = 0;
357         for (int i = 0; i < node.jjtGetNumChildren(); i++) {
358             PLSQLNode n = (PLSQLNode) node.jjtGetChild(i);
359 
360             // Fall-through labels count as 1 for complexity
361             Integer complexity = (Integer) n.jjtAccept(this, data);
362             caseRange *= complexity;
363         }
364         // add in npath of last label
365         npath += caseRange;
366         LOGGER.exiting(CLASS_NAME, "visit(ASTCaseWhenClause)", (boolCompSwitch + npath));
367         return Integer.valueOf(boolCompSwitch + npath);
368     }
369 
370     @Override
371     public Object visit(ASTCaseStatement node, Object data) {
372         LOGGER.entering(CLASS_NAME, "visit(ASTCaseStatement)");
373         // bool_comp of switch + sum(npath(case_range))
374 
375         int boolCompSwitch = sumExpressionComplexity(node.getFirstChildOfType(ASTExpression.class));
376 
377         int npath = 0;
378         int caseRange = 0;
379         for (int i = 0; i < node.jjtGetNumChildren(); i++) {
380             PLSQLNode n = (PLSQLNode) node.jjtGetChild(i);
381 
382             // Fall-through labels count as 1 for complexity
383             Integer complexity = (Integer) n.jjtAccept(this, data);
384             caseRange *= complexity;
385         }
386         // add in npath of last label
387         npath += caseRange;
388         LOGGER.exiting(CLASS_NAME, "visit(ASTCaseStatement)", (boolCompSwitch + npath));
389         return Integer.valueOf(boolCompSwitch + npath);
390     }
391 
392     @Override
393     public Object visit(ASTConditionalOrExpression node, Object data) {
394         return NumericConstants.ONE;
395     }
396 
397     /**
398      * Calculate the boolean complexity of the given expression. NPath boolean
399      * complexity is the sum of && and || tokens. This is calculated by summing
400      * the number of children of the &&'s (minus one) and the children of the
401      * ||'s (minus one).
402      * <p>
403      * Note that this calculation applies to Cyclomatic Complexity as well.
404      *
405      * @param expr
406      *            control structure expression
407      * @return complexity of the boolean expression
408      */
409     public static int sumExpressionComplexity(ASTExpression expr) {
410         LOGGER.entering(CLASS_NAME, "visit(ASTExpression)");
411         if (expr == null) {
412             LOGGER.exiting(CLASS_NAME, "visit(ASTExpression)", 0);
413             return 0;
414         }
415 
416         List<ASTConditionalAndExpression> andNodes = expr.findDescendantsOfType(ASTConditionalAndExpression.class);
417         List<ASTConditionalOrExpression> orNodes = expr.findDescendantsOfType(ASTConditionalOrExpression.class);
418 
419         int children = 0;
420 
421         for (ASTConditionalOrExpression element : orNodes) {
422             children += element.jjtGetNumChildren();
423             children--;
424         }
425 
426         for (ASTConditionalAndExpression element : andNodes) {
427             children += element.jjtGetNumChildren();
428             children--;
429         }
430 
431         LOGGER.exiting(CLASS_NAME, "visit(ASTExpression)", children);
432         return children;
433     }
434 
435     @Override
436     public Object[] getViolationParameters(DataPoint point) {
437         return new String[] { ((ExecutableCode) point.getNode()).getMethodName(),
438                 String.valueOf((int) point.getScore()) };
439     }
440 }