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