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.whitespace;
21  
22  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
23  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
24  import com.puppycrawl.tools.checkstyle.api.DetailAST;
25  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
26  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
27  
28  /**
29   * <p>
30   * Checks that the whitespace around the Generic tokens (angle brackets)
31   * "&lt;" and "&gt;" are correct to the <i>typical</i> convention.
32   * The convention is not configurable.
33   * </p>
34   * <br>
35   * <p>
36   * Left angle bracket ("&lt;"):
37   * </p>
38   * <br>
39   * <ul>
40   * <li> should be preceded with whitespace only
41   *   in generic methods definitions.</li>
42   * <li> should not be preceded with whitespace
43   *   when it is precede method name or following type name.</li>
44   * <li> should not be followed with whitespace in all cases.</li>
45   * </ul>
46   * <br>
47   * <p>
48   * Right angle bracket ("&gt;"):
49   * </p>
50   * <br>
51   * <ul>
52   * <li> should not be preceded with whitespace in all cases.</li>
53   * <li> should be followed with whitespace in almost all cases,
54   *   except diamond operators and when preceding method name.</li></ul>
55   * <br>
56   * <p>
57   * Examples with correct spacing:
58   * </p>
59   * <br>
60   * <pre>
61   * public void &lt;K, V extends Number&gt; boolean foo(K, V) {}  // Generic methods definitions
62   * class name&lt;T1, T2, ..., Tn&gt; {}                          // Generic type definition
63   * OrderedPair&lt;String, Box&lt;Integer&gt;&gt; p;              // Generic type reference
64   * boolean same = Util.&lt;Integer, String&gt;compare(p1, p2);   // Generic preceded method name
65   * Pair&lt;Integer, String&gt; p1 = new Pair&lt;&gt;(1, "apple");// Diamond operator
66   * List&lt;T&gt; list = ImmutableList.Builder&lt;T&gt;::new;     // Method reference
67   * sort(list, Comparable::&lt;String&gt;compareTo);              // Method reference
68   * </pre>
69   * @author Oliver Burn
70   */
71  @FileStatefulCheck
72  public class GenericWhitespaceCheck extends AbstractCheck {
73  
74      /**
75       * A key is pointing to the warning message text in "messages.properties"
76       * file.
77       */
78      public static final String MSG_WS_PRECEDED = "ws.preceded";
79  
80      /**
81       * A key is pointing to the warning message text in "messages.properties"
82       * file.
83       */
84      public static final String MSG_WS_FOLLOWED = "ws.followed";
85  
86      /**
87       * A key is pointing to the warning message text in "messages.properties"
88       * file.
89       */
90      public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
91  
92      /**
93       * A key is pointing to the warning message text in "messages.properties"
94       * file.
95       */
96      public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
97  
98      /** Open angle bracket literal. */
99      private static final String OPEN_ANGLE_BRACKET = "<";
100 
101     /** Close angle bracket literal. */
102     private static final String CLOSE_ANGLE_BRACKET = ">";
103 
104     /** Used to count the depth of a Generic expression. */
105     private int depth;
106 
107     @Override
108     public int[] getDefaultTokens() {
109         return getRequiredTokens();
110     }
111 
112     @Override
113     public int[] getAcceptableTokens() {
114         return getRequiredTokens();
115     }
116 
117     @Override
118     public int[] getRequiredTokens() {
119         return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
120     }
121 
122     @Override
123     public void beginTree(DetailAST rootAST) {
124         // Reset for each tree, just increase there are errors in preceding
125         // trees.
126         depth = 0;
127     }
128 
129     @Override
130     public void visitToken(DetailAST ast) {
131         switch (ast.getType()) {
132             case TokenTypes.GENERIC_START:
133                 processStart(ast);
134                 depth++;
135                 break;
136             case TokenTypes.GENERIC_END:
137                 processEnd(ast);
138                 depth--;
139                 break;
140             default:
141                 throw new IllegalArgumentException("Unknown type " + ast);
142         }
143     }
144 
145     /**
146      * Checks the token for the end of Generics.
147      * @param ast the token to check
148      */
149     private void processEnd(DetailAST ast) {
150         final String line = getLine(ast.getLineNo() - 1);
151         final int before = ast.getColumnNo() - 1;
152         final int after = ast.getColumnNo() + 1;
153 
154         if (before >= 0 && Character.isWhitespace(line.charAt(before))
155                 && !containsWhitespaceBefore(before, line)) {
156             log(ast.getLineNo(), before, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
157         }
158 
159         if (after < line.length()) {
160 
161             // Check if the last Generic, in which case must be a whitespace
162             // or a '(),[.'.
163             if (depth == 1) {
164                 processSingleGeneric(ast, line, after);
165             }
166             else {
167                 processNestedGenerics(ast, line, after);
168             }
169         }
170     }
171 
172     /**
173      * Process Nested generics.
174      * @param ast token
175      * @param line line content
176      * @param after position after
177      */
178     private void processNestedGenerics(DetailAST ast, String line, int after) {
179         // In a nested Generic type, so can only be a '>' or ',' or '&'
180 
181         // In case of several extends definitions:
182         //
183         //   class IntEnumValueType<E extends Enum<E> & IntEnum>
184         //                                          ^
185         //   should be whitespace if followed by & -+
186         //
187         final int indexOfAmp = line.indexOf('&', after);
188         if (indexOfAmp >= 1
189             && containsWhitespaceBetween(after, indexOfAmp, line)) {
190             if (indexOfAmp - after == 0) {
191                 log(ast.getLineNo(), after, MSG_WS_NOT_PRECEDED, "&");
192             }
193             else if (indexOfAmp - after != 1) {
194                 log(ast.getLineNo(), after, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
195             }
196         }
197         else if (line.charAt(after) == ' ') {
198             log(ast.getLineNo(), after, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
199         }
200     }
201 
202     /**
203      * Process Single-generic.
204      * @param ast token
205      * @param line line content
206      * @param after position after
207      */
208     private void processSingleGeneric(DetailAST ast, String line, int after) {
209         final char charAfter = line.charAt(after);
210 
211         // Need to handle a number of cases. First is:
212         //    Collections.<Object>emptySet();
213         //                        ^
214         //                        +--- whitespace not allowed
215         if (isGenericBeforeMethod(ast)) {
216             if (Character.isWhitespace(charAfter)) {
217                 log(ast.getLineNo(), after, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
218             }
219         }
220         else if (!isCharacterValidAfterGenericEnd(charAfter)) {
221             log(ast.getLineNo(), after, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
222         }
223     }
224 
225     /**
226      * Is generic before method reference.
227      * @param ast ast
228      * @return true if generic before a method ref
229      */
230     private static boolean isGenericBeforeMethod(DetailAST ast) {
231         return ast.getParent().getType() == TokenTypes.TYPE_ARGUMENTS
232                 && ast.getParent().getParent().getType() == TokenTypes.DOT
233                 && ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
234                 || isAfterMethodReference(ast);
235     }
236 
237     /**
238      * Checks if current generic end ('>') is located after
239      * {@link TokenTypes#METHOD_REF method reference operator}.
240      * @param genericEnd {@link TokenTypes#GENERIC_END}
241      * @return true if '>' follows after method reference.
242      */
243     private static boolean isAfterMethodReference(DetailAST genericEnd) {
244         return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
245     }
246 
247     /**
248      * Checks the token for the start of Generics.
249      * @param ast the token to check
250      */
251     private void processStart(DetailAST ast) {
252         final String line = getLine(ast.getLineNo() - 1);
253         final int before = ast.getColumnNo() - 1;
254         final int after = ast.getColumnNo() + 1;
255 
256         // Need to handle two cases as in:
257         //
258         //   public static <T> Callable<T> callable(Runnable task, T result)
259         //                 ^           ^
260         //      ws reqd ---+           +--- whitespace NOT required
261         //
262         if (before >= 0) {
263             // Detect if the first case
264             final DetailAST parent = ast.getParent();
265             final DetailAST grandparent = parent.getParent();
266             if (parent.getType() == TokenTypes.TYPE_PARAMETERS
267                 && (grandparent.getType() == TokenTypes.CTOR_DEF
268                     || grandparent.getType() == TokenTypes.METHOD_DEF)) {
269                 // Require whitespace
270                 if (!Character.isWhitespace(line.charAt(before))) {
271                     log(ast.getLineNo(), before, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
272                 }
273             }
274             // Whitespace not required
275             else if (Character.isWhitespace(line.charAt(before))
276                 && !containsWhitespaceBefore(before, line)) {
277                 log(ast.getLineNo(), before, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
278             }
279         }
280 
281         if (after < line.length()
282                 && Character.isWhitespace(line.charAt(after))) {
283             log(ast.getLineNo(), after, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
284         }
285     }
286 
287     /**
288      * Returns whether the specified string contains only whitespace between
289      * specified indices.
290      *
291      * @param fromIndex the index to start the search from. Inclusive
292      * @param toIndex the index to finish the search. Exclusive
293      * @param line the line to check
294      * @return whether there are only whitespaces (or nothing)
295      */
296     private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, String line) {
297         boolean result = true;
298         for (int i = fromIndex; i < toIndex; i++) {
299             if (!Character.isWhitespace(line.charAt(i))) {
300                 result = false;
301                 break;
302             }
303         }
304         return result;
305     }
306 
307     /**
308      * Returns whether the specified string contains only whitespace up to specified index.
309      *
310      * @param before the index to start the search from. Inclusive
311      * @param line   the index to finish the search. Exclusive
312      * @return {@code true} if there are only whitespaces,
313      *     false if there is nothing before or some other characters
314      */
315     private static boolean containsWhitespaceBefore(int before, String line) {
316         return before != 0 && CommonUtils.hasWhitespaceBefore(before, line);
317     }
318 
319     /**
320      * Checks whether given character is valid to be right after generic ends.
321      * @param charAfter character to check
322      * @return checks if given character is valid
323      */
324     private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
325         return charAfter == '(' || charAfter == ')'
326             || charAfter == ',' || charAfter == '['
327             || charAfter == '.' || charAfter == ':'
328             || charAfter == ';'
329             || Character.isWhitespace(charAfter);
330     }
331 }