1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.CommonUtil;
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   */
70  @FileStatefulCheck
71  public class GenericWhitespaceCheck extends AbstractCheck {
72  
73      /**
74       * A key is pointing to the warning message text in "messages.properties"
75       * file.
76       */
77      public static final String MSG_WS_PRECEDED = "ws.preceded";
78  
79      /**
80       * A key is pointing to the warning message text in "messages.properties"
81       * file.
82       */
83      public static final String MSG_WS_FOLLOWED = "ws.followed";
84  
85      /**
86       * A key is pointing to the warning message text in "messages.properties"
87       * file.
88       */
89      public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
90  
91      /**
92       * A key is pointing to the warning message text in "messages.properties"
93       * file.
94       */
95      public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
96  
97      /** Open angle bracket literal. */
98      private static final String OPEN_ANGLE_BRACKET = "<";
99  
100     /** Close angle bracket literal. */
101     private static final String CLOSE_ANGLE_BRACKET = ">";
102 
103     /** Used to count the depth of a Generic expression. */
104     private int depth;
105 
106     @Override
107     public int[] getDefaultTokens() {
108         return getRequiredTokens();
109     }
110 
111     @Override
112     public int[] getAcceptableTokens() {
113         return getRequiredTokens();
114     }
115 
116     @Override
117     public int[] getRequiredTokens() {
118         return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
119     }
120 
121     @Override
122     public void beginTree(DetailAST rootAST) {
123         // Reset for each tree, just increase there are errors in preceding
124         // trees.
125         depth = 0;
126     }
127 
128     @Override
129     public void visitToken(DetailAST ast) {
130         switch (ast.getType()) {
131             case TokenTypes.GENERIC_START:
132                 processStart(ast);
133                 depth++;
134                 break;
135             case TokenTypes.GENERIC_END:
136                 processEnd(ast);
137                 depth--;
138                 break;
139             default:
140                 throw new IllegalArgumentException("Unknown type " + ast);
141         }
142     }
143 
144     /**
145      * Checks the token for the end of Generics.
146      * @param ast the token to check
147      */
148     private void processEnd(DetailAST ast) {
149         final String line = getLine(ast.getLineNo() - 1);
150         final int before = ast.getColumnNo() - 1;
151         final int after = ast.getColumnNo() + 1;
152 
153         if (before >= 0 && Character.isWhitespace(line.charAt(before))
154                 && !containsWhitespaceBefore(before, line)) {
155             log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
156         }
157 
158         if (after < line.length()) {
159             // Check if the last Generic, in which case must be a whitespace
160             // or a '(),[.'.
161             if (depth == 1) {
162                 processSingleGeneric(ast, line, after);
163             }
164             else {
165                 processNestedGenerics(ast, line, after);
166             }
167         }
168     }
169 
170     /**
171      * Process Nested generics.
172      * @param ast token
173      * @param line line content
174      * @param after position after
175      */
176     private void processNestedGenerics(DetailAST ast, String line, int after) {
177         // In a nested Generic type, so can only be a '>' or ',' or '&'
178 
179         // In case of several extends definitions:
180         //
181         //   class IntEnumValueType<E extends Enum<E> & IntEnum>
182         //                                          ^
183         //   should be whitespace if followed by & -+
184         //
185         final int indexOfAmp = line.indexOf('&', after);
186         if (indexOfAmp >= 1
187             && containsWhitespaceBetween(after, indexOfAmp, line)) {
188             if (indexOfAmp - after == 0) {
189                 log(ast, MSG_WS_NOT_PRECEDED, "&");
190             }
191             else if (indexOfAmp - after != 1) {
192                 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
193             }
194         }
195         else if (line.charAt(after) == ' ') {
196             log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
197         }
198     }
199 
200     /**
201      * Process Single-generic.
202      * @param ast token
203      * @param line line content
204      * @param after position after
205      */
206     private void processSingleGeneric(DetailAST ast, String line, int after) {
207         final char charAfter = line.charAt(after);
208 
209         // Need to handle a number of cases. First is:
210         //    Collections.<Object>emptySet();
211         //                        ^
212         //                        +--- whitespace not allowed
213         if (isGenericBeforeMethod(ast)) {
214             if (Character.isWhitespace(charAfter)) {
215                 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
216             }
217         }
218         else if (!isCharacterValidAfterGenericEnd(charAfter)) {
219             log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
220         }
221     }
222 
223     /**
224      * Is generic before method reference.
225      * @param ast ast
226      * @return true if generic before a method ref
227      */
228     private static boolean isGenericBeforeMethod(DetailAST ast) {
229         return ast.getParent().getParent().getType() == TokenTypes.DOT
230                 && ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
231                 || isAfterMethodReference(ast);
232     }
233 
234     /**
235      * Checks if current generic end ('>') is located after
236      * {@link TokenTypes#METHOD_REF method reference operator}.
237      * @param genericEnd {@link TokenTypes#GENERIC_END}
238      * @return true if '>' follows after method reference.
239      */
240     private static boolean isAfterMethodReference(DetailAST genericEnd) {
241         return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
242     }
243 
244     /**
245      * Checks the token for the start of Generics.
246      * @param ast the token to check
247      */
248     private void processStart(DetailAST ast) {
249         final String line = getLine(ast.getLineNo() - 1);
250         final int before = ast.getColumnNo() - 1;
251         final int after = ast.getColumnNo() + 1;
252 
253         // Need to handle two cases as in:
254         //
255         //   public static <T> Callable<T> callable(Runnable task, T result)
256         //                 ^           ^
257         //      ws reqd ---+           +--- whitespace NOT required
258         //
259         if (before >= 0) {
260             // Detect if the first case
261             final DetailAST parent = ast.getParent();
262             final DetailAST grandparent = parent.getParent();
263             if (grandparent.getType() == TokenTypes.CTOR_DEF
264                     || grandparent.getType() == TokenTypes.METHOD_DEF) {
265                 // Require whitespace
266                 if (!Character.isWhitespace(line.charAt(before))) {
267                     log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
268                 }
269             }
270             // Whitespace not required
271             else if (Character.isWhitespace(line.charAt(before))
272                 && !containsWhitespaceBefore(before, line)) {
273                 log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
274             }
275         }
276 
277         if (after < line.length()
278                 && Character.isWhitespace(line.charAt(after))) {
279             log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
280         }
281     }
282 
283     /**
284      * Returns whether the specified string contains only whitespace between
285      * specified indices.
286      *
287      * @param fromIndex the index to start the search from. Inclusive
288      * @param toIndex the index to finish the search. Exclusive
289      * @param line the line to check
290      * @return whether there are only whitespaces (or nothing)
291      */
292     private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, String line) {
293         boolean result = true;
294         for (int i = fromIndex; i < toIndex; i++) {
295             if (!Character.isWhitespace(line.charAt(i))) {
296                 result = false;
297                 break;
298             }
299         }
300         return result;
301     }
302 
303     /**
304      * Returns whether the specified string contains only whitespace up to specified index.
305      *
306      * @param before the index to start the search from. Inclusive
307      * @param line   the index to finish the search. Exclusive
308      * @return {@code true} if there are only whitespaces,
309      *     false if there is nothing before or some other characters
310      */
311     private static boolean containsWhitespaceBefore(int before, String line) {
312         return before != 0 && CommonUtil.hasWhitespaceBefore(before, line);
313     }
314 
315     /**
316      * Checks whether given character is valid to be right after generic ends.
317      * @param charAfter character to check
318      * @return checks if given character is valid
319      */
320     private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
321         return charAfter == '(' || charAfter == ')'
322             || charAfter == ',' || charAfter == '['
323             || charAfter == '.' || charAfter == ':'
324             || charAfter == ';'
325             || Character.isWhitespace(charAfter);
326     }
327 
328 }