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