View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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 java.util.stream.IntStream;
23  
24  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  
31  /**
32   * <div>
33   * Checks that the whitespace around the Generic tokens (angle brackets)
34   * "&lt;" and "&gt;" are correct to the <i>typical</i> convention.
35   * The convention is not configurable.
36   * </div>
37   *
38   * <p>
39   * Left angle bracket ("&lt;"):
40   * </p>
41   * <ul>
42   * <li> should be preceded with whitespace only
43   *   in generic methods definitions.</li>
44   * <li> should not be preceded with whitespace
45   *   when it is preceded method name or constructor.</li>
46   * <li> should not be preceded with whitespace when following type name.</li>
47   * <li> should not be followed with whitespace in all cases.</li>
48   * </ul>
49   *
50   * <p>
51   * Right angle bracket ("&gt;"):
52   * </p>
53   * <ul>
54   * <li> should not be preceded with whitespace in all cases.</li>
55   * <li> should be followed with whitespace in almost all cases,
56   *   except diamond operators and when preceding a method name, constructor, or record header.</li>
57   * </ul>
58   *
59   * <p>
60   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
61   * </p>
62   *
63   * <p>
64   * Violation Message Keys:
65   * </p>
66   * <ul>
67   * <li>
68   * {@code ws.followed}
69   * </li>
70   * <li>
71   * {@code ws.illegalFollow}
72   * </li>
73   * <li>
74   * {@code ws.notPreceded}
75   * </li>
76   * <li>
77   * {@code ws.preceded}
78   * </li>
79   * </ul>
80   *
81   * @since 5.0
82   */
83  @FileStatefulCheck
84  public class GenericWhitespaceCheck extends AbstractCheck {
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_PRECEDED = "ws.preceded";
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_FOLLOWED = "ws.followed";
97  
98      /**
99       * A key is pointing to the warning message text in "messages.properties"
100      * file.
101      */
102     public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
103 
104     /**
105      * A key is pointing to the warning message text in "messages.properties"
106      * file.
107      */
108     public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
109 
110     /** Open angle bracket literal. */
111     private static final String OPEN_ANGLE_BRACKET = "<";
112 
113     /** Close angle bracket literal. */
114     private static final String CLOSE_ANGLE_BRACKET = ">";
115 
116     /** Used to count the depth of a Generic expression. */
117     private int depth;
118 
119     @Override
120     public int[] getDefaultTokens() {
121         return getRequiredTokens();
122     }
123 
124     @Override
125     public int[] getAcceptableTokens() {
126         return getRequiredTokens();
127     }
128 
129     @Override
130     public int[] getRequiredTokens() {
131         return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
132     }
133 
134     @Override
135     public void beginTree(DetailAST rootAST) {
136         // Reset for each tree, just increase there are violations in preceding
137         // trees.
138         depth = 0;
139     }
140 
141     @Override
142     public void visitToken(DetailAST ast) {
143         switch (ast.getType()) {
144             case TokenTypes.GENERIC_START:
145                 processStart(ast);
146                 depth++;
147                 break;
148             case TokenTypes.GENERIC_END:
149                 processEnd(ast);
150                 depth--;
151                 break;
152             default:
153                 throw new IllegalArgumentException("Unknown type " + ast);
154         }
155     }
156 
157     /**
158      * Checks the token for the end of Generics.
159      *
160      * @param ast the token to check
161      */
162     private void processEnd(DetailAST ast) {
163         final int[] line = getLineCodePoints(ast.getLineNo() - 1);
164         final int before = ast.getColumnNo() - 1;
165         final int after = ast.getColumnNo() + 1;
166 
167         if (before >= 0 && CommonUtil.isCodePointWhitespace(line, before)
168                 && !containsWhitespaceBefore(before, line)) {
169             log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
170         }
171 
172         if (after < line.length) {
173             // Check if the last Generic, in which case must be a whitespace
174             // or a '(),[.'.
175             if (depth == 1) {
176                 processSingleGeneric(ast, line, after);
177             }
178             else {
179                 processNestedGenerics(ast, line, after);
180             }
181         }
182     }
183 
184     /**
185      * Process Nested generics.
186      *
187      * @param ast token
188      * @param line unicode code points array of line
189      * @param after position after
190      */
191     private void processNestedGenerics(DetailAST ast, int[] line, int after) {
192         // In a nested Generic type, so can only be a '>' or ',' or '&'
193 
194         // In case of several extends definitions:
195         //
196         //   class IntEnumValueType<E extends Enum<E> & IntEnum>
197         //                                          ^
198         //   should be whitespace if followed by & -+
199         //
200         final int indexOfAmp = IntStream.range(after, line.length)
201                 .filter(index -> line[index] == '&')
202                 .findFirst()
203                 .orElse(-1);
204         if (indexOfAmp >= 1
205             && containsWhitespaceBetween(after, indexOfAmp, line)) {
206             if (indexOfAmp - after == 0) {
207                 log(ast, MSG_WS_NOT_PRECEDED, "&");
208             }
209             else if (indexOfAmp - after != 1) {
210                 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
211             }
212         }
213         else if (line[after] == ' ') {
214             log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
215         }
216     }
217 
218     /**
219      * Process Single-generic.
220      *
221      * @param ast token
222      * @param line unicode code points array of line
223      * @param after position after
224      */
225     private void processSingleGeneric(DetailAST ast, int[] line, int after) {
226         final char charAfter = Character.toChars(line[after])[0];
227         if (isGenericBeforeMethod(ast)
228                 || isGenericBeforeCtorInvocation(ast)
229                 || isGenericBeforeRecordHeader(ast)) {
230             if (Character.isWhitespace(charAfter)) {
231                 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
232             }
233         }
234         else if (!isCharacterValidAfterGenericEnd(charAfter)) {
235             log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
236         }
237     }
238 
239     /**
240      * Checks if generic is before record header. Identifies two cases:
241      * <ol>
242      *     <li>In record def, eg: {@code record Session<T>()}</li>
243      *     <li>In record pattern def, eg: {@code o instanceof Session<String>(var s)}</li>
244      * </ol>
245      *
246      * @param ast ast
247      * @return true if generic is before record header
248      */
249     private static boolean isGenericBeforeRecordHeader(DetailAST ast) {
250         final DetailAST grandParent = ast.getParent().getParent();
251         return grandParent.getType() == TokenTypes.RECORD_DEF
252                 || grandParent.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF;
253     }
254 
255     /**
256      * Checks if generic is before constructor invocation. Identifies two cases:
257      * <ol>
258      *     <li>{@code new ArrayList<>();}</li>
259      *     <li>{@code new Outer.Inner<>();}</li>
260      * </ol>
261      *
262      * @param ast ast
263      * @return true if generic is before constructor invocation
264      */
265     private static boolean isGenericBeforeCtorInvocation(DetailAST ast) {
266         final DetailAST grandParent = ast.getParent().getParent();
267         return grandParent.getType() == TokenTypes.LITERAL_NEW
268                 || grandParent.getParent().getType() == TokenTypes.LITERAL_NEW;
269     }
270 
271     /**
272      * Checks if generic is after {@code LITERAL_NEW}. Identifies three cases:
273      * <ol>
274      *     <li>{@code new <String>Object();}</li>
275      *     <li>{@code new <String>Outer.Inner();}</li>
276      *     <li>{@code new <@A Outer>@B Inner();}</li>
277      * </ol>
278      *
279      * @param ast ast
280      * @return true if generic after {@code LITERAL_NEW}
281      */
282     private static boolean isGenericAfterNew(DetailAST ast) {
283         final DetailAST parent = ast.getParent();
284         return parent.getParent().getType() == TokenTypes.LITERAL_NEW
285                 && (parent.getNextSibling().getType() == TokenTypes.IDENT
286                     || parent.getNextSibling().getType() == TokenTypes.DOT
287                     || parent.getNextSibling().getType() == TokenTypes.ANNOTATIONS);
288     }
289 
290     /**
291      * Is generic before method reference.
292      *
293      * @param ast ast
294      * @return true if generic before a method ref
295      */
296     private static boolean isGenericBeforeMethod(DetailAST ast) {
297         return ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
298                 || isAfterMethodReference(ast);
299     }
300 
301     /**
302      * Checks if current generic end ('&gt;') is located after
303      * {@link TokenTypes#METHOD_REF method reference operator}.
304      *
305      * @param genericEnd {@link TokenTypes#GENERIC_END}
306      * @return true if '&gt;' follows after method reference.
307      */
308     private static boolean isAfterMethodReference(DetailAST genericEnd) {
309         return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
310     }
311 
312     /**
313      * Checks the token for the start of Generics.
314      *
315      * @param ast the token to check
316      */
317     private void processStart(DetailAST ast) {
318         final int[] line = getLineCodePoints(ast.getLineNo() - 1);
319         final int before = ast.getColumnNo() - 1;
320         final int after = ast.getColumnNo() + 1;
321 
322         // Checks if generic needs to be preceded by a whitespace or not.
323         // Handles 3 cases as in:
324         //
325         //   public static <T> Callable<T> callable(Runnable task, T result)
326         //                 ^           ^
327         //   1. ws reqd ---+        2. +--- whitespace NOT required
328         //
329         //   new <String>Object()
330         //       ^
331         //    3. +--- ws required
332         if (before >= 0) {
333             final DetailAST parent = ast.getParent();
334             final DetailAST grandparent = parent.getParent();
335             // cases (1, 3) where whitespace is required:
336             if (grandparent.getType() == TokenTypes.CTOR_DEF
337                     || grandparent.getType() == TokenTypes.METHOD_DEF
338                     || isGenericAfterNew(ast)) {
339 
340                 if (!CommonUtil.isCodePointWhitespace(line, before)) {
341                     log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
342                 }
343             }
344             // case 2 where whitespace is not required:
345             else if (CommonUtil.isCodePointWhitespace(line, before)
346                 && !containsWhitespaceBefore(before, line)) {
347                 log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
348             }
349         }
350 
351         if (after < line.length
352                 && CommonUtil.isCodePointWhitespace(line, after)) {
353             log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
354         }
355     }
356 
357     /**
358      * Returns whether the specified string contains only whitespace between
359      * specified indices.
360      *
361      * @param fromIndex the index to start the search from. Inclusive
362      * @param toIndex the index to finish the search. Exclusive
363      * @param line the unicode code points array of line to check
364      * @return whether there are only whitespaces (or nothing)
365      */
366     private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, int... line) {
367         boolean result = true;
368         for (int i = fromIndex; i < toIndex; i++) {
369             if (!CommonUtil.isCodePointWhitespace(line, i)) {
370                 result = false;
371                 break;
372             }
373         }
374         return result;
375     }
376 
377     /**
378      * Returns whether the specified string contains only whitespace up to specified index.
379      *
380      * @param before the index to finish the search. Exclusive
381      * @param line   the unicode code points array of line to check
382      * @return {@code true} if there are only whitespaces,
383      *     false if there is nothing before or some other characters
384      */
385     private static boolean containsWhitespaceBefore(int before, int... line) {
386         return before != 0 && CodePointUtil.hasWhitespaceBefore(before, line);
387     }
388 
389     /**
390      * Checks whether given character is valid to be right after generic ends.
391      *
392      * @param charAfter character to check
393      * @return checks if given character is valid
394      */
395     private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
396         return charAfter == ')' || charAfter == ','
397             || charAfter == '[' || charAfter == '.'
398             || charAfter == ':' || charAfter == ';'
399             || Character.isWhitespace(charAfter);
400     }
401 
402 }