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