1 package org.incava.doctorj;
2 
3 import java.util.*;
4 import net.sourceforge.pmd.ast.*;
5 import org.incava.analysis.Report;
6 import org.incava.java.*;
7 import org.incava.javadoc.*;
8 import org.incava.text.SpellChecker;
9 
10 
11 /**
12  * Checks for violations of rules applying to exceptions.
13  */
14 public class ExceptionDocAnalyzer extends DocAnalyzer
15 {
16     public final static String MSG_EXCEPTION_WITHOUT_CLASS_NAME = "Exception without class name";
17 
18     public final static String MSG_EXCEPTION_WITHOUT_DESCRIPTION = "Exception without description";
19 
20     public final static String MSG_EXCEPTIONS_NOT_ALPHABETICAL = "Exceptions not alphabetical";
21 
22     public final static String MSG_EXCEPTION_NOT_IN_THROWS_LIST = "Exception not in throws list";
23 
24     public final static String MSG_EXCEPTION_MISSPELLED = "Exception misspelled";
25 
26     public final static String MSG_EXCEPTION_NOT_DOCUMENTED = "Exception not documented";
27 
28     protected final static int CHKLVL_EXCEPTIONS_ALPHABETICAL = 2;
29 
30     protected final static int CHKLVL_EXCEPTION_DOC_EXISTS = 1;
31 
32     private static Map excToRuntime = new HashMap();
33 
34     private static Collection reportedExceptions = new ArrayList();
35 
36     protected final static String[] KNOWN_RUNTIME_EXCEPTIONS = new String[] {
37         "java.awt.color.CMMException",
38         "java.awt.color.ProfileDataException",
39 
40         "java.awt.geom.IllegalPathStateException",
41 
42         "java.awt.image.ImagingOpException",
43         "java.awt.image.RasterFormatException",
44 
45         "java.lang.ArithmeticException",
46         "java.lang.ArrayStoreException",
47         "java.lang.ClassCastException",
48         "java.lang.EnumConstantNotPresentException",
49         "java.lang.IllegalArgumentException",
50         "java.lang.IllegalMonitorStateException",
51         "java.lang.IllegalStateException",
52         "java.lang.IndexOutOfBoundsException",
53         "java.lang.NegativeArraySizeException",
54         "java.lang.NullPointerException",
55         "java.lang.SecurityException",
56         "java.lang.TypeNotPresentException",
57         "java.lang.UnsupportedOperationException",
58 
59         "java.lang.annotation.AnnotationTypeMismatchException",
60         "java.lang.annotation.IncompleteAnnotationException",
61 
62         "java.lang.reflect.MalformedParameterizedTypeException",
63         "java.lang.reflect.UndeclaredThrowableException",
64 
65         "java.nio.BufferOverflowException",
66         "java.nio.BufferUnderflowException",
67 
68         "java.security.ProviderException",
69 
70         "java.util.ConcurrentModificationException",
71         "java.util.EmptyStackException",
72         "java.util.MissingResourceException",
73         "java.util.NoSuchElementException",
74 
75         "java.util.concurrent.RejectedExecutionException",
76 
77         "javax.management.JMRuntimeException",
78 
79         "javax.print.attribute.UnmodifiableSetException",
80 
81         "javax.swing.undo.CannotRedoException",
82         "javax.swing.undo.CannotUndoException",
83 
84         "org.omg.CORBA.SystemException",
85 
86         "org.w3c.dom.DOMException",
87 
88         "org.w3c.dom.events.EventException",
89 
90         "org.w3c.dom.ls.LSException",
91     };
92 
93     static {
94         for (int ki = 0; ki < KNOWN_RUNTIME_EXCEPTIONS.length; ++ki) {
95             String ke = KNOWN_RUNTIME_EXCEPTIONS[ki];
excToRuntime.put(ke, Boolean.TRUE)96             excToRuntime.put(ke, Boolean.TRUE);
97         }
98     }
99 
100     private JavadocNode _javadoc;
101 
102     private ASTNameList _throwsList;
103 
104     private SimpleNode _function;
105 
106     private List _documentedExceptions = new ArrayList();
107 
108     private int _nodeLevel;
109 
110     private Map _importMap;
111 
112     /**
113      * Creates and runs the exception documentation analyzer.
114      *
115      * @param report   The report to which to send violations.
116      * @param javadoc  The javadoc for the function. Should not be null.
117      * @param function The constructor or method.
118      */
ExceptionDocAnalyzer(Report report, JavadocNode javadoc, SimpleNode function, int nodeLevel)119     public ExceptionDocAnalyzer(Report report, JavadocNode javadoc, SimpleNode function, int nodeLevel)
120     {
121         super(report);
122 
123         _javadoc    = javadoc;
124         _throwsList = FunctionUtil.getThrowsList(function);
125         _function   = function;
126         _nodeLevel  = nodeLevel;
127         _importMap  = null;
128     }
129 
run()130     public void run()
131     {
132         // foreach @throws / @exception tag:
133         //  - check for target
134         //  - check for description
135         //  - check that target is declared, or is subclass of RuntimeException
136         //  - in alphabetical order
137 
138         boolean alphabeticalReported = false;
139         String  previousException    = null;
140 
141         SimpleNode node = _function;
142         while (node != null && !(node instanceof ASTCompilationUnit)) {
143             node = SimpleNodeUtil.getParent(node);
144         }
145 
146         ASTCompilationUnit     cu      = (ASTCompilationUnit)node;
147         ASTImportDeclaration[] imports = CompilationUnitUtil.getImports(cu);
148         _importMap = makeImportMap(imports);
149 
150         JavadocTaggedNode[] taggedComments = _javadoc.getTaggedComments();
151         for (int ti = 0; ti < taggedComments.length; ++ti) {
152             JavadocTaggedNode jtn = taggedComments[ti];
153             JavadocTag        tag = jtn.getTag();
154 
155             if (tag.text.equals(JavadocTags.EXCEPTION) || tag.text.equals(JavadocTags.THROWS)) {
156                 JavadocElement tgt = jtn.getTarget();
157 
158                 if (tgt == null) {
159                     if (Options.warningLevel >= CHKLVL_TAG_CONTENT + _nodeLevel) {
160                         addViolation(MSG_EXCEPTION_WITHOUT_CLASS_NAME, tag.start, tag.end);
161                     }
162                 }
163                 else {
164                     if (jtn.getDescriptionNonTarget() == null && Options.warningLevel >= CHKLVL_TAG_CONTENT + _nodeLevel) {
165                         addViolation(MSG_EXCEPTION_WITHOUT_DESCRIPTION, tgt.start, tgt.end);
166                     }
167 
168                     String shortName = getShortName(tgt.text);
169                     String fullName  = tgt.text;
170                     Class  cls       = null;
171 
172                     if (fullName.indexOf('.') >= 0) {
173                         cls = loadClass(fullName);
174                     }
175                     else {
176                         fullName = getExactMatch(fullName);
177 
178                         if (fullName == null) {
179                             Iterator iit = _importMap.keySet().iterator();
180                             while (cls == null && iit.hasNext()) {
181                                 String impName   = (String)iit.next();
182                                 String shImpName = getShortName(impName);
183                                 if (shImpName.equals("*")) {
184                                     // try to load pkg.name
185                                     fullName = impName.substring(0, impName.indexOf("*")) + shortName;
186                                     cls = loadClass(fullName);
187                                 }
188                                 else {
189                                     // skip it.
190                                 }
191                             }
192 
193                             if (cls == null) {
194                                 // maybe java.lang....
195                                 fullName = "java.lang." + shortName;
196                                 cls = loadClass(fullName);
197                             }
198                         }
199                         else {
200                             cls = loadClass(fullName);
201                         }
202                     }
203 
204                     checkAgainstCode(tag, tgt, shortName, fullName, cls);
205 
206                     if (!alphabeticalReported &&
207                         Options.warningLevel >= CHKLVL_EXCEPTIONS_ALPHABETICAL + _nodeLevel &&
208                         previousException != null && previousException.compareTo(shortName) > 0) {
209 
210                         addViolation(MSG_EXCEPTIONS_NOT_ALPHABETICAL, tgt.start, tgt.end);
211                         alphabeticalReported = true;
212                     }
213 
214                     previousException = shortName;
215                 }
216             }
217         }
218 
219         if (_throwsList != null && Options.warningLevel >= CHKLVL_EXCEPTION_DOC_EXISTS + _nodeLevel) {
220             reportUndocumentedExceptions();
221         }
222     }
223 
makeImportMap(ASTImportDeclaration[] imports)224     protected Map makeImportMap(ASTImportDeclaration[] imports)
225     {
226         Map namesToImp = new HashMap();
227 
228         for (int ii = 0; ii < imports.length; ++ii) {
229             ASTImportDeclaration imp = imports[ii];
230             StringBuffer         buf = new StringBuffer();
231             Token                tk  = imp.getFirstToken().next;
232 
233             while (tk != null) {
234                 if (tk == imp.getLastToken()) {
235                     break;
236                 }
237                 else {
238                     buf.append(tk.image);
239                     tk = tk.next;
240                 }
241             }
242 
243             namesToImp.put(buf.toString(), imp);
244         }
245 
246         return namesToImp;
247     }
248 
249     /**
250      * Returns the short name of the class, e.g., Integer instead of
251      * java.lang.Integer.
252      */
getShortName(String name)253     protected String getShortName(String name)
254     {
255         int    lastDot = name.lastIndexOf('.');
256         String shName  = lastDot == -1 ? name : name.substring(lastDot + 1);
257         return shName;
258     }
259 
getExactMatch(String name)260     protected String getExactMatch(String name)
261     {
262         Iterator iit = _importMap.keySet().iterator();
263         while (iit.hasNext()) {
264             String impName   = (String)iit.next();
265             String shImpName = getShortName(impName);
266             if (shImpName.equals("*")) {
267                 // skip it.
268             }
269             else if (shImpName.equals(name)) {
270                 return impName;
271             }
272         }
273 
274         return null;
275     }
276 
loadClass(String clsName)277     protected Class loadClass(String clsName)
278     {
279         try {
280             Class cls = Class.forName(clsName);
281             return cls;
282         }
283         catch (Exception e) {
284             // e.printStackTrace();
285             return null;
286         }
287     }
288 
289     /**
290      * Returns whether the given class is derived from Runtimeexception or
291      * Error.
292      */
isRuntimeException(Class excClass)293     protected boolean isRuntimeException(Class excClass)
294     {
295         if (excClass == null) {
296             return false;
297         }
298         else {
299             String  excName = excClass.getName();
300             Boolean val     = (Boolean)excToRuntime.get(excName);
301             if (val == null) {
302                 val = new Boolean(RuntimeException.class.isAssignableFrom(excClass) ||
303                                   Error.class.isAssignableFrom(excClass));
304                 excToRuntime.put(excName, val);
305             }
306             return val.booleanValue();
307         }
308     }
309 
checkAgainstCode(JavadocTag tag, JavadocElement tgt, String shortExcName, String fullExcName, Class excClass)310     protected void checkAgainstCode(JavadocTag tag, JavadocElement tgt, String shortExcName, String fullExcName, Class excClass)
311     {
312         ASTName name = getMatchingException(shortExcName);
313         if (name == null) {
314             name = getClosestMatchingException(shortExcName);
315             if (name == null) {
316                 if (isRuntimeException(excClass)) {
317                     // don't report it.
318                 }
319                 else if (excClass != null || !reportedExceptions.contains(fullExcName)) {
320                     // this violation is an error, not a warning:
321                     addViolation(MSG_EXCEPTION_NOT_IN_THROWS_LIST, tgt.start, tgt.end);
322 
323                     // report it only once.
324                     reportedExceptions.add(fullExcName);
325                 }
326                 else {
327                     // we don't report exceptions when we don't know if they're
328                     // run-time exceptions, or when they've already been
329                     // reported.
330                 }
331             }
332             else {
333                 // this violation is an error:
334                 addViolation(MSG_EXCEPTION_MISSPELLED, tgt.start, tgt.end);
335                 _documentedExceptions.add(name.getLastToken().image);
336             }
337         }
338         else {
339             _documentedExceptions.add(shortExcName);
340         }
341     }
342 
reportUndocumentedExceptions()343     protected void reportUndocumentedExceptions()
344     {
345         ASTName[] names = ThrowsUtil.getNames(_throwsList);
346 
347         for (int ni = 0; ni < names.length; ++ni) {
348             ASTName name      = names[ni];
349 
350             // by using the last token, we disregard the package:
351             Token   nameToken = name.getLastToken();
352 
353             tr.Ace.log("considering name: " + name + " (" + nameToken + ")");
354             if (!_documentedExceptions.contains(nameToken.image)) {
355                 addViolation(MSG_EXCEPTION_NOT_DOCUMENTED,
356                              nameToken.beginLine, nameToken.beginColumn,
357                              nameToken.beginLine, nameToken.beginColumn + nameToken.image.length() - 1);
358             }
359         }
360     }
361 
362     /**
363      * Returns the first name in the list that matches the given string.
364      */
getMatchingException(String str)365     protected ASTName getMatchingException(String str)
366     {
367         if (_throwsList == null) {
368             return null;
369         }
370         else {
371             ASTName[] names = ThrowsUtil.getNames(_throwsList);
372 
373             for (int ni = 0; ni < names.length; ++ni) {
374                 ASTName name      = names[ni];
375                 // (again) by using the last token, we disregard the package:
376                 Token   nameToken = name.getLastToken();
377                 tr.Ace.log("considering name: " + name + " (" + nameToken + ")");
378                 if (nameToken.image.equals(str)) {
379                     return name;
380                 }
381             }
382             tr.Ace.log("no exact match for '" + str + "'");
383             return null;
384         }
385     }
386 
387     /**
388      * Returns the name in the list that most closely matches the given string.
389      */
getClosestMatchingException(String str)390     protected ASTName getClosestMatchingException(String str)
391     {
392         if (_throwsList == null) {
393             return null;
394         }
395         else {
396             SpellChecker spellChecker = new SpellChecker();
397             int          bestDistance = -1;
398             ASTName      bestName     = null;
399             ASTName[]    names        = ThrowsUtil.getNames(_throwsList);
400 
401             for (int ni = 0; ni < names.length; ++ni) {
402                 ASTName name      = names[ni];
403                 Token   nameToken = name.getLastToken();
404                 int     dist      = spellChecker.editDistance(nameToken.image, str);
405 
406                 if (dist >= 0 && dist <= SpellChecker.DEFAULT_MAX_DISTANCE && (bestDistance == -1 || dist < bestDistance)) {
407                     bestDistance = dist;
408                     bestName     = name;
409                 }
410             }
411 
412             return bestName;
413         }
414     }
415 
416 }
417