View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
31  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32  import com.puppycrawl.tools.checkstyle.api.DetailAST;
33  import com.puppycrawl.tools.checkstyle.api.FullIdent;
34  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  
37  /**
38   * Abstract class that endeavours to maintain type information for the Java
39   * file being checked. It provides helper methods for performing type
40   * information functions.
41   *
42   * @author Oliver Burn
43   * @deprecated Checkstyle is not type aware tool and all Checks derived from this
44   *     class are potentially unstable.
45   * @noinspection DeprecatedIsStillUsed, AbstractClassWithOnlyOneDirectInheritor
46   */
47  @Deprecated
48  @FileStatefulCheck
49  public abstract class AbstractTypeAwareCheck extends AbstractCheck {
50      /** Stack of maps for type params. */
51      private final Deque<Map<String, AbstractClassInfo>> typeParams = new ArrayDeque<>();
52  
53      /** Imports details. **/
54      private final Set<String> imports = new HashSet<>();
55  
56      /** Full identifier for package of the method. **/
57      private FullIdent packageFullIdent;
58  
59      /** Name of current class. */
60      private String currentClassName;
61  
62      /** {@code ClassResolver} instance for current tree. */
63      private ClassResolver classResolver;
64  
65      /**
66       * Whether to log class loading errors to the checkstyle report
67       * instead of throwing a RTE.
68       *
69       * <p>Logging errors will avoid stopping checkstyle completely
70       * because of a typo in javadoc. However, with modern IDEs that
71       * support automated refactoring and generate javadoc this will
72       * occur rarely, so by default we assume a configuration problem
73       * in the checkstyle classpath and throw an exception.
74       *
75       * <p>This configuration option was triggered by bug 1422462.
76       */
77      private boolean logLoadErrors = true;
78  
79      /**
80       * Whether to show class loading errors in the checkstyle report.
81       * Request ID 1491630
82       */
83      private boolean suppressLoadErrors;
84  
85      /**
86       * Called to process an AST when visiting it.
87       * @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or
88       *             IMPORT tokens.
89       */
90      protected abstract void processAST(DetailAST ast);
91  
92      /**
93       * Logs error if unable to load class information.
94       * Abstract, should be overridden in subclasses.
95       * @param ident class name for which we can no load class.
96       */
97      protected abstract void logLoadError(Token ident);
98  
99      /**
100      * Controls whether to log class loading errors to the checkstyle report
101      * instead of throwing a RTE.
102      *
103      * @param logLoadErrors true if errors should be logged
104      */
105     public final void setLogLoadErrors(boolean logLoadErrors) {
106         this.logLoadErrors = logLoadErrors;
107     }
108 
109     /**
110      * Controls whether to show class loading errors in the checkstyle report.
111      *
112      * @param suppressLoadErrors true if errors shouldn't be shown
113      */
114     public final void setSuppressLoadErrors(boolean suppressLoadErrors) {
115         this.suppressLoadErrors = suppressLoadErrors;
116     }
117 
118     @Override
119     public final int[] getRequiredTokens() {
120         return new int[] {
121             TokenTypes.PACKAGE_DEF,
122             TokenTypes.IMPORT,
123             TokenTypes.CLASS_DEF,
124             TokenTypes.INTERFACE_DEF,
125             TokenTypes.ENUM_DEF,
126         };
127     }
128 
129     @Override
130     public void beginTree(DetailAST rootAST) {
131         packageFullIdent = FullIdent.createFullIdent(null);
132         imports.clear();
133         // add java.lang.* since it's always imported
134         imports.add("java.lang.*");
135         classResolver = null;
136         currentClassName = "";
137         typeParams.clear();
138     }
139 
140     @Override
141     public final void visitToken(DetailAST ast) {
142         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
143             processPackage(ast);
144         }
145         else if (ast.getType() == TokenTypes.IMPORT) {
146             processImport(ast);
147         }
148         else if (ast.getType() == TokenTypes.CLASS_DEF
149                  || ast.getType() == TokenTypes.INTERFACE_DEF
150                  || ast.getType() == TokenTypes.ENUM_DEF) {
151             processClass(ast);
152         }
153         else {
154             if (ast.getType() == TokenTypes.METHOD_DEF) {
155                 processTypeParams(ast);
156             }
157             processAST(ast);
158         }
159     }
160 
161     @Override
162     public final void leaveToken(DetailAST ast) {
163         if (ast.getType() == TokenTypes.CLASS_DEF
164             || ast.getType() == TokenTypes.INTERFACE_DEF
165             || ast.getType() == TokenTypes.ENUM_DEF) {
166             // perhaps it was inner class
167             int dotIdx = currentClassName.lastIndexOf('$');
168             if (dotIdx == -1) {
169                 // perhaps just a class
170                 dotIdx = currentClassName.lastIndexOf('.');
171             }
172             if (dotIdx == -1) {
173                 // looks like a topmost class
174                 currentClassName = "";
175             }
176             else {
177                 currentClassName = currentClassName.substring(0, dotIdx);
178             }
179             typeParams.pop();
180         }
181         else if (ast.getType() == TokenTypes.METHOD_DEF) {
182             typeParams.pop();
183         }
184     }
185 
186     /**
187      * Is exception is unchecked (subclass of {@code RuntimeException}
188      * or {@code Error}.
189      *
190      * @param exception {@code Class} of exception to check
191      * @return true  if exception is unchecked
192      *         false if exception is checked
193      */
194     protected static boolean isUnchecked(Class<?> exception) {
195         return isSubclass(exception, RuntimeException.class)
196             || isSubclass(exception, Error.class);
197     }
198 
199     /**
200      * Checks if one class is subclass of another.
201      *
202      * @param child {@code Class} of class
203      *               which should be child
204      * @param parent {@code Class} of class
205      *                which should be parent
206      * @return true  if aChild is subclass of aParent
207      *         false otherwise
208      */
209     protected static boolean isSubclass(Class<?> child, Class<?> parent) {
210         return parent != null && child != null
211             && parent.isAssignableFrom(child);
212     }
213 
214     /**
215      * Returns the current tree's ClassResolver.
216      * @return {@code ClassResolver} for current tree.
217      */
218     private ClassResolver getClassResolver() {
219         if (classResolver == null) {
220             classResolver =
221                 new ClassResolver(getClassLoader(),
222                                   packageFullIdent.getText(),
223                                   imports);
224         }
225         return classResolver;
226     }
227 
228     /**
229      * Attempts to resolve the Class for a specified name.
230      * @param resolvableClassName name of the class to resolve
231      * @param className name of surrounding class.
232      * @return the resolved class or {@code null}
233      *          if unable to resolve the class.
234      * @noinspection WeakerAccess
235      */
236     // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
237     protected final Class<?> resolveClass(String resolvableClassName,
238                                           String className) {
239         Class<?> clazz;
240         try {
241             clazz = getClassResolver().resolve(resolvableClassName, className);
242         }
243         catch (final ClassNotFoundException ignored) {
244             clazz = null;
245         }
246         return clazz;
247     }
248 
249     /**
250      * Tries to load class. Logs error if unable.
251      * @param ident name of class which we try to load.
252      * @param className name of surrounding class.
253      * @return {@code Class} for a ident.
254      * @noinspection WeakerAccess
255      */
256     // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
257     protected final Class<?> tryLoadClass(Token ident, String className) {
258         final Class<?> clazz = resolveClass(ident.getText(), className);
259         if (clazz == null) {
260             logLoadError(ident);
261         }
262         return clazz;
263     }
264 
265     /**
266      * Common implementation for logLoadError() method.
267      * @param lineNo line number of the problem.
268      * @param columnNo column number of the problem.
269      * @param msgKey message key to use.
270      * @param values values to fill the message out.
271      */
272     protected final void logLoadErrorImpl(int lineNo, int columnNo,
273                                           String msgKey, Object... values) {
274         if (!logLoadErrors) {
275             final LocalizedMessage msg = new LocalizedMessage(lineNo,
276                                                     columnNo,
277                                                     getMessageBundle(),
278                                                     msgKey,
279                                                     values,
280                                                     getSeverityLevel(),
281                                                     getId(),
282                                                     getClass(),
283                                                     null);
284             throw new IllegalStateException(msg.getMessage());
285         }
286 
287         if (!suppressLoadErrors) {
288             log(lineNo, columnNo, msgKey, values);
289         }
290     }
291 
292     /**
293      * Collects the details of a package.
294      * @param ast node containing the package details
295      */
296     private void processPackage(DetailAST ast) {
297         final DetailAST nameAST = ast.getLastChild().getPreviousSibling();
298         packageFullIdent = FullIdent.createFullIdent(nameAST);
299     }
300 
301     /**
302      * Collects the details of imports.
303      * @param ast node containing the import details
304      */
305     private void processImport(DetailAST ast) {
306         final FullIdent name = FullIdent.createFullIdentBelow(ast);
307         imports.add(name.getText());
308     }
309 
310     /**
311      * Process type params (if any) for given class, enum or method.
312      * @param ast class, enum or method to process.
313      */
314     private void processTypeParams(DetailAST ast) {
315         final DetailAST params =
316             ast.findFirstToken(TokenTypes.TYPE_PARAMETERS);
317 
318         final Map<String, AbstractClassInfo> paramsMap = new HashMap<>();
319         typeParams.push(paramsMap);
320 
321         if (params != null) {
322             for (DetailAST child = params.getFirstChild();
323                  child != null;
324                  child = child.getNextSibling()) {
325                 if (child.getType() == TokenTypes.TYPE_PARAMETER) {
326                     final DetailAST bounds =
327                         child.findFirstToken(TokenTypes.TYPE_UPPER_BOUNDS);
328                     if (bounds != null) {
329                         final FullIdent name =
330                             FullIdent.createFullIdentBelow(bounds);
331                         final AbstractClassInfo classInfo =
332                             createClassInfo(new Token(name), currentClassName);
333                         final String alias =
334                                 child.findFirstToken(TokenTypes.IDENT).getText();
335                         paramsMap.put(alias, classInfo);
336                     }
337                 }
338             }
339         }
340     }
341 
342     /**
343      * Processes class definition.
344      * @param ast class definition to process.
345      */
346     private void processClass(DetailAST ast) {
347         final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
348         String innerClass = ident.getText();
349 
350         if (!currentClassName.isEmpty()) {
351             innerClass = "$" + innerClass;
352         }
353         currentClassName += innerClass;
354         processTypeParams(ast);
355     }
356 
357     /**
358      * Returns current class.
359      * @return name of current class.
360      */
361     protected final String getCurrentClassName() {
362         return currentClassName;
363     }
364 
365     /**
366      * Creates class info for given name.
367      * @param name name of type.
368      * @param surroundingClass name of surrounding class.
369      * @return class info for given name.
370      */
371     protected final AbstractClassInfo createClassInfo(final Token name,
372                                               final String surroundingClass) {
373         final AbstractClassInfo result;
374         final AbstractClassInfo classInfo = findClassAlias(name.getText());
375         if (classInfo == null) {
376             result = new RegularClass(name, surroundingClass, this);
377         }
378         else {
379             result = new ClassAlias(name, classInfo);
380         }
381         return result;
382     }
383 
384     /**
385      * Looking if a given name is alias.
386      * @param name given name
387      * @return ClassInfo for alias if it exists, null otherwise
388      * @noinspection WeakerAccess
389      */
390     protected final AbstractClassInfo findClassAlias(final String name) {
391         AbstractClassInfo classInfo = null;
392         final Iterator<Map<String, AbstractClassInfo>> iterator = typeParams.descendingIterator();
393         while (iterator.hasNext()) {
394             final Map<String, AbstractClassInfo> paramMap = iterator.next();
395             classInfo = paramMap.get(name);
396             if (classInfo != null) {
397                 break;
398             }
399         }
400         return classInfo;
401     }
402 
403     /**
404      * Contains class's {@code Token}.
405      * @noinspection ProtectedInnerClass
406      */
407     protected abstract static class AbstractClassInfo {
408         /** {@code FullIdent} associated with this class. */
409         private final Token name;
410 
411         /**
412          * Creates new instance of class information object.
413          * @param className token which represents class name.
414          */
415         protected AbstractClassInfo(final Token className) {
416             if (className == null) {
417                 throw new IllegalArgumentException(
418                     "ClassInfo's name should be non-null");
419             }
420             name = className;
421         }
422 
423         /**
424          * Returns class associated with that object.
425          * @return {@code Class} associated with an object.
426          */
427         // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
428         public abstract Class<?> getClazz();
429 
430         /**
431          * Gets class name.
432          * @return class name
433          */
434         public final Token getName() {
435             return name;
436         }
437     }
438 
439     /** Represents regular classes/enums. */
440     private static final class RegularClass extends AbstractClassInfo {
441         /** Name of surrounding class. */
442         private final String surroundingClass;
443         /** The check we use to resolve classes. */
444         private final AbstractTypeAwareCheck check;
445         /** Is class loadable. */
446         private boolean loadable = true;
447         /** {@code Class} object of this class if it's loadable. */
448         private Class<?> classObj;
449 
450         /**
451          * Creates new instance of of class information object.
452          * @param name {@code FullIdent} associated with new object.
453          * @param surroundingClass name of current surrounding class.
454          * @param check the check we use to load class.
455          */
456         RegularClass(final Token name,
457                              final String surroundingClass,
458                              final AbstractTypeAwareCheck check) {
459             super(name);
460             this.surroundingClass = surroundingClass;
461             this.check = check;
462         }
463 
464         @Override
465         public Class<?> getClazz() {
466             if (loadable && classObj == null) {
467                 setClazz(check.tryLoadClass(getName(), surroundingClass));
468             }
469             return classObj;
470         }
471 
472         /**
473          * Associates {@code Class} with an object.
474          * @param clazz {@code Class} to associate with.
475          */
476         private void setClazz(Class<?> clazz) {
477             classObj = clazz;
478             loadable = clazz != null;
479         }
480 
481         @Override
482         public String toString() {
483             return "RegularClass[name=" + getName()
484                     + ", in class='" + surroundingClass + '\''
485                     + ", check=" + check.hashCode()
486                     + ", loadable=" + loadable
487                     + ", class=" + classObj
488                     + ']';
489         }
490     }
491 
492     /** Represents type param which is "alias" for real type. */
493     private static class ClassAlias extends AbstractClassInfo {
494         /** Class information associated with the alias. */
495         private final AbstractClassInfo classInfo;
496 
497         /**
498          * Creates new instance of the class.
499          * @param name token which represents name of class alias.
500          * @param classInfo class information associated with the alias.
501          */
502         ClassAlias(final Token name, AbstractClassInfo classInfo) {
503             super(name);
504             this.classInfo = classInfo;
505         }
506 
507         @Override
508         public final Class<?> getClazz() {
509             return classInfo.getClazz();
510         }
511 
512         @Override
513         public String toString() {
514             return "ClassAlias[alias " + getName() + " for " + classInfo.getName() + "]";
515         }
516     }
517 
518     /**
519      * Represents text element with location in the text.
520      * @noinspection ProtectedInnerClass
521      */
522     protected static class Token {
523         /** Token's column number. */
524         private final int columnNo;
525         /** Token's line number. */
526         private final int lineNo;
527         /** Token's text. */
528         private final String text;
529 
530         /**
531          * Creates token.
532          * @param text token's text
533          * @param lineNo token's line number
534          * @param columnNo token's column number
535          */
536         public Token(String text, int lineNo, int columnNo) {
537             this.text = text;
538             this.lineNo = lineNo;
539             this.columnNo = columnNo;
540         }
541 
542         /**
543          * Converts FullIdent to Token.
544          * @param fullIdent full ident to convert.
545          */
546         public Token(FullIdent fullIdent) {
547             text = fullIdent.getText();
548             lineNo = fullIdent.getLineNo();
549             columnNo = fullIdent.getColumnNo();
550         }
551 
552         /**
553          * Gets line number of the token.
554          * @return line number of the token
555          */
556         public int getLineNo() {
557             return lineNo;
558         }
559 
560         /**
561          * Gets column number of the token.
562          * @return column number of the token
563          */
564         public int getColumnNo() {
565             return columnNo;
566         }
567 
568         /**
569          * Gets text of the token.
570          * @return text of the token
571          */
572         public String getText() {
573             return text;
574         }
575 
576         @Override
577         public String toString() {
578             return "Token[" + text + "(" + lineNo
579                 + "x" + columnNo + ")]";
580         }
581     }
582 }