View Javadoc
1   package com.github.sevntu.checkstyle.checks.design;
2   
3   import java.util.HashSet;
4   import java.util.LinkedList;
5   import java.util.List;
6   import java.util.Set;
7   import java.util.regex.Pattern;
8   
9   import antlr.collections.AST;
10  
11  import com.puppycrawl.tools.checkstyle.api.Check;
12  import com.puppycrawl.tools.checkstyle.api.DetailAST;
13  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
14  
15  /**
16   * Prevents using wildcards as return type of methods.
17   * <p>
18   * <i>Joshua Bloch, "Effective Java (2nd edition)" Item 28: page 137 :</i>
19   * </p>
20   * <p>
21   * "Do not use wildcard types as return types. Rather than providing
22   * additional flexibility for your users,
23   * it would force them to use wildcard types in client code. Properly used,
24   * wildcard types are nearly invisible to users of a class. They cause methods
25   * to accept the parameters they should accept and reject those they should
26   * reject. If the user of a class has to think about wildcard types, there is
27   * probably something wrong with the class’s API."
28   * </p>
29   * @author <a href='mailto:barataliba@gmail.com'>Baratali Izmailov</a>
30   */
31  public class ForbidWildcardAsReturnTypeCheck extends Check
32  {
33      /**
34       * Key for error message.
35       */
36      public static final String MSG_KEY = "forbid.wildcard.as.return.type";
37      /**
38       * Token of 'extends' keyword in bounded wildcard.
39       */
40      private static final int WILDCARD_EXTENDS_IDENT =
41              TokenTypes.TYPE_UPPER_BOUNDS;
42      /**
43       * Token of 'super' keyword in bounded wildcard.
44       */
45      private static final int WILDCARD_SUPER_IDENT =
46              TokenTypes.TYPE_LOWER_BOUNDS;
47      /**
48       * Empty array of DetailAST.
49       */
50      private static final DetailAST[] EMPTY_DETAILAST_ARRAY = new DetailAST[0];
51      /**
52       * Check methods with 'public' modifier.
53       */
54      private boolean checkPublicMethods = true;
55      /**
56       * Check methods with 'protected' modifier.
57       */
58      private boolean checkProtectedMethods = true;
59      /**
60       * Check methods with 'package' modifier.
61       */
62      private boolean checkPackageMethods = true;
63      /**
64       * Check methods with 'private' modifier.
65       */
66      private boolean checkPrivateMethods;
67      /**
68       * Check methods with @Override annotation.
69       */
70      private boolean checkOverrideMethods;
71      /**
72       * Check methods with @Deprecated annotation.
73       */
74      private boolean checkDeprecatedMethods;
75      /**
76       * Allow wildcard with 'super'. Example: "? super T"
77       */
78      private boolean allowReturnWildcardWithSuper;
79      /**
80       * Allow wildcard with 'extends'. Example: "? extends T"
81       */
82      private boolean allowReturnWildcardWithExtends;
83      /**
84       * Ignore regexp for return type class names.
85       */
86      private Pattern returnTypeClassNamesIgnoreRegex = Pattern.compile(
87              "^(Comparator|Comparable)$");
88  
89      public void setCheckPublicMethods(boolean checkPublicMethods)
90      {
91          this.checkPublicMethods = checkPublicMethods;
92      }
93  
94      public void setCheckProtectedMethods(boolean checkProtectedMethods)
95      {
96          this.checkProtectedMethods = checkProtectedMethods;
97      }
98  
99      public void setCheckPackageMethods(boolean checkPackageMethods)
100     {
101         this.checkPackageMethods = checkPackageMethods;
102     }
103 
104     public void setCheckPrivateMethods(boolean checkPrivateMethods)
105     {
106         this.checkPrivateMethods = checkPrivateMethods;
107     }
108 
109     public void setCheckOverrideMethods(boolean checkOverrideMethods)
110     {
111         this.checkOverrideMethods = checkOverrideMethods;
112     }
113     
114     public void setCheckDeprecatedMethods(boolean checkDeprecatedMethods)
115     {
116         this.checkDeprecatedMethods = checkDeprecatedMethods;
117     }
118 
119     public void
120     setAllowReturnWildcardWithSuper(boolean allowReturnWildcardWithSuper)
121     {
122         this.allowReturnWildcardWithSuper = allowReturnWildcardWithSuper;
123     }
124 
125     public void
126     setAllowReturnWildcardWithExtends(boolean allowReturnWildcardWithExtends)
127     {
128         this.allowReturnWildcardWithExtends = allowReturnWildcardWithExtends;
129     }
130 
131     public void
132     setReturnTypeClassNamesIgnoreRegex(String returnTypeClassNamesIgnoreRegex)
133     {
134         this.returnTypeClassNamesIgnoreRegex = Pattern.compile(
135                 returnTypeClassNamesIgnoreRegex);
136     }
137 
138     @Override
139     public int[] getDefaultTokens()
140     {
141         return new int[] { TokenTypes.METHOD_DEF };
142     }
143 
144     @Override
145     public void visitToken(DetailAST methodDefAst)
146     {
147         final String methodScope = getVisibilityScope(methodDefAst);
148         if (((checkPublicMethods && "public".equals(methodScope))
149                 || (checkPrivateMethods && "private".equals(methodScope))
150                 || (checkProtectedMethods && "protected".equals(methodScope))
151                 || (checkPackageMethods && "package".equals(methodScope)))
152                 && (checkOverrideMethods 
153                         || !hasAnnotation(methodDefAst, "Override"))
154                 && (checkDeprecatedMethods
155                         || !hasAnnotation(methodDefAst, "Deprecated")))
156         {
157             final List<DetailAST> wildcardTypeArguments =
158                     getWildcardArgumentsAsMethodReturnType(methodDefAst);
159             if (!wildcardTypeArguments.isEmpty()
160                     && !isIgnoreCase(methodDefAst, wildcardTypeArguments))
161             {
162                 log(methodDefAst.getLineNo(), MSG_KEY);
163             }
164         }
165     }
166 
167     /**
168      * Returns the visibility scope of method.
169      * @param methodDefAst DetailAST of method definition.
170      * @return one of "public", "private", "protected", "package"
171      */
172     private static String getVisibilityScope(DetailAST methodDefAst)
173     {
174         String result = "package";
175         if (isInsideInterfaceDefinition(methodDefAst)) {
176             result = "public";
177         }
178         else {
179             final String[] visibilityScopeModifiers = {"public", "private",
180                 "protected", };
181             final Set<String> methodModifiers = getModifiers(methodDefAst);
182             for (final String modifier : visibilityScopeModifiers) {
183                 if (methodModifiers.contains(modifier)) {
184                     result = modifier;
185                     break;
186                 }
187             }
188         }
189         return result;
190     }
191 
192     /**
193      * Verify that method definition is inside interface definition.
194      * @param methodDefAst DetailAST of method definition.
195      * @return true if method definition is inside interface definition.
196      */
197     private static boolean isInsideInterfaceDefinition(DetailAST methodDefAst)
198     {
199         boolean result = false;
200         final DetailAST objBlock = methodDefAst.getParent();
201         final DetailAST interfaceDef = objBlock.getParent();
202         if (interfaceDef.getType() == TokenTypes.INTERFACE_DEF) {
203             result = true;
204         }
205         return result;
206     }
207 
208     /**
209      * Returns the set of modifier Strings for a METHOD_DEF AST.
210      * @param methodDefAst AST for a method definition
211      * @return the set of modifier Strings for aMethodDefAST
212      */
213     private static Set<String> getModifiers(DetailAST methodDefAst)
214     {
215         final AST modifiersAst = methodDefAst.getFirstChild();
216         final Set<String> modifiersSet = new HashSet<String>();
217         AST modifierAst = modifiersAst.getFirstChild();
218         while (modifierAst != null) {
219             modifiersSet.add(modifierAst.getText());
220             modifierAst = modifierAst.getNextSibling();
221         }
222         return modifiersSet;
223     }
224 
225     /**
226      * Get identifier of aAST.
227      * @param ast
228      *        DetailAST instance
229      * @return identifier of aAST, null if AST does not have identifier.
230      */
231     private static String getIdentifier(final DetailAST ast)
232     {
233         String result = null;
234         final DetailAST identifier = ast.findFirstToken(TokenTypes.IDENT);
235         if (identifier != null) {
236             result = identifier.getText();
237         }
238         return result;
239     }
240 
241     /**
242      * Verify that method definition contains specified annotation.
243      * @param methodDefAst DetailAST of method definition.
244      * @param annotationTitle Annotation title
245      * @return true if method definition contains specified annotation.
246      */
247     private static boolean hasAnnotation(DetailAST methodDefAst,
248             String annotationTitle)
249     {
250         boolean result = false;
251         final DetailAST modifiersAst = methodDefAst.getFirstChild();
252         if (hasChildToken(modifiersAst, TokenTypes.ANNOTATION)) {
253             DetailAST modifierAst  = modifiersAst.getFirstChild();
254             while (modifierAst != null) {
255                 if (modifierAst.getType() == TokenTypes.ANNOTATION
256                         && annotationTitle.equals(getIdentifier(modifierAst))) {
257                     result = true;
258                     break;
259                 }
260                 modifierAst = modifierAst.getNextSibling();
261             }
262         }
263         return result;
264     }
265 
266     /**
267      * Receive list of arguments(AST nodes) which have wildcard.
268      * @param methodDefAst
269      *        DetailAST of method definition.
270      * @return list of arguments which have wildcard.
271      */
272     private static List<DetailAST>
273     getWildcardArgumentsAsMethodReturnType(DetailAST methodDefAst)
274     {
275         final List<DetailAST> result = new LinkedList<DetailAST>();
276         final DetailAST methodTypeAst =
277                 methodDefAst.findFirstToken(TokenTypes.TYPE);
278         final DetailAST[] methodTypeArgumentTokens =
279                 getGenericTypeArguments(methodTypeAst);
280         for (DetailAST typeArgumentAst: methodTypeArgumentTokens) {
281             if (hasChildToken(typeArgumentAst, TokenTypes.WILDCARD_TYPE)) {
282                 result.add(typeArgumentAst);
283             }
284         }
285         return result;
286     }
287 
288     /**
289      * Get all type arguments of TypeAST.
290      * @param typeAst
291      *        DetailAST of type definition.
292      * @return array of type arguments.
293      */
294     private static DetailAST[] getGenericTypeArguments(DetailAST typeAst)
295     {
296         DetailAST[] result = EMPTY_DETAILAST_ARRAY;
297         if (hasChildToken(typeAst, TokenTypes.TYPE_ARGUMENTS)) {
298             final DetailAST typeArguments = typeAst
299                     .findFirstToken(TokenTypes.TYPE_ARGUMENTS);
300             final int argumentsCount = typeArguments
301                     .getChildCount(TokenTypes.TYPE_ARGUMENT);
302             result = new DetailAST[argumentsCount];
303             DetailAST firstTypeArgument = typeArguments
304                     .findFirstToken(TokenTypes.TYPE_ARGUMENT);
305             int counter = 0;
306             while (firstTypeArgument != null) {
307                 if (firstTypeArgument.getType() == TokenTypes.TYPE_ARGUMENT) {
308                     result[counter] = firstTypeArgument;
309                     counter++;
310                 }
311                 firstTypeArgument = firstTypeArgument.getNextSibling();
312             }
313         }
314         return result;
315     }
316 
317     /**
318      * Verify that aAST has token of aTokenType type.
319      * @param ast
320      *        DetailAST instance.
321      * @param tokenType
322      *        one of TokenTypes
323      * @return true if aAST has token of given type, or false otherwise.
324      */
325     private static boolean hasChildToken(DetailAST ast, int tokenType)
326     {
327         return ast.findFirstToken(tokenType) != null;
328     }
329 
330     /**
331      * Verify that method with wildcards as return type is allowed by current
332      * check configuration.
333      * @param methodDefAst DetailAST of method definition.
334      * @param wildcardTypeArguments list of wildcard type arguments.
335      * @return true if method is allowed by current check configuration.
336      */
337     private boolean isIgnoreCase(DetailAST methodDefAst,
338             List<DetailAST> wildcardTypeArguments)
339     {
340         boolean result = false;
341         if (matchesIgnoreClassNames(methodDefAst)) {
342             result = true;
343         }
344         else {
345             final boolean hasExtendsWildcardAsReturnType =
346                     hasBoundedWildcardAsReturnType(wildcardTypeArguments,
347                             WILDCARD_EXTENDS_IDENT);
348             final boolean hasSuperWildcardAsReturnType =
349                     hasBoundedWildcardAsReturnType(wildcardTypeArguments,
350                             WILDCARD_SUPER_IDENT);
351             final boolean hasOnlyExtendsWildcardAsReturnType =
352                     hasExtendsWildcardAsReturnType
353                     && !hasSuperWildcardAsReturnType;
354             final boolean hasOnlySuperWildcardAsReturnType =
355                     hasSuperWildcardAsReturnType
356                     && !hasExtendsWildcardAsReturnType;
357             final boolean hasBoundedWildcardAsReturnType =
358                     hasExtendsWildcardAsReturnType
359                     || hasSuperWildcardAsReturnType;
360             final boolean isAllowedBoundedWildcards =
361                     allowReturnWildcardWithExtends
362                     && allowReturnWildcardWithSuper;
363             result = (isAllowedBoundedWildcards
364                             && hasBoundedWildcardAsReturnType)
365                     || (allowReturnWildcardWithExtends
366                             && hasOnlyExtendsWildcardAsReturnType)
367                     || (allowReturnWildcardWithSuper
368                             && hasOnlySuperWildcardAsReturnType);
369         }
370         return result;
371     }
372 
373     /**
374      * Verify that method's return type name matches ignore regexp.
375      * @param methodDefAst DetailAST of method.
376      * @return true if aMethodDefAST's name matches ignore regexp.
377      *      false otherwise.
378      */
379     private boolean matchesIgnoreClassNames(DetailAST methodDefAst)
380     {
381         final DetailAST methodTypeAst =
382                 methodDefAst.findFirstToken(TokenTypes.TYPE);
383         final String typeIdentifier = getIdentifier(methodTypeAst);
384         return returnTypeClassNamesIgnoreRegex
385                 .matcher(typeIdentifier).matches();
386     }
387 
388     /**
389      * Verify that method has bounded wildcard in type arguments list.
390      * @param typeArgumentsList list of type arguments.
391      * @param boundedWildcardType type of bounded wildcard.
392      * @return true if aTypeArgumentsList contains bounded wildcard.
393      */
394     private static boolean hasBoundedWildcardAsReturnType(
395             final List<DetailAST> typeArgumentsList, int boundedWildcardType)
396     {
397         boolean result = false;
398         for (DetailAST typeArgumentAst: typeArgumentsList) {
399             if (hasChildToken(typeArgumentAst, boundedWildcardType)) {
400                 result = true;
401                 break;
402             }
403         }
404         return result;
405     }
406 }