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