001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.whitespace;
021
022import java.util.stream.IntStream;
023
024import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
030
031/**
032 * <div>
033 * Checks that the whitespace around the Generic tokens (angle brackets)
034 * "&lt;" and "&gt;" are correct to the <i>typical</i> convention.
035 * The convention is not configurable.
036 * </div>
037 *
038 * <p>
039 * Whitespace is defined by implementation of
040 * java.lang.Character.isWhitespace(char)
041 * </p>
042 *
043 * <p>
044 * Left angle bracket ("&lt;"):
045 * </p>
046 * <ul>
047 * <li> should be preceded with whitespace only
048 *   in generic methods definitions.</li>
049 * <li> should not be preceded with whitespace
050 *   when it is preceded method name or constructor.</li>
051 * <li> should not be preceded with whitespace when following type name.</li>
052 * <li> should not be followed with whitespace in all cases.</li>
053 * </ul>
054 *
055 * <p>
056 * Right angle bracket ("&gt;"):
057 * </p>
058 * <ul>
059 * <li> should not be preceded with whitespace in all cases.</li>
060 * <li> should be followed with whitespace in almost all cases,
061 *   except diamond operators and when preceding a method name, constructor, or record header.</li>
062 * </ul>
063 *
064 * @since 5.0
065 */
066@FileStatefulCheck
067public class GenericWhitespaceCheck extends AbstractCheck {
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_WS_PRECEDED = "ws.preceded";
074
075    /**
076     * A key is pointing to the warning message text in "messages.properties"
077     * file.
078     */
079    public static final String MSG_WS_FOLLOWED = "ws.followed";
080
081    /**
082     * A key is pointing to the warning message text in "messages.properties"
083     * file.
084     */
085    public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
086
087    /**
088     * A key is pointing to the warning message text in "messages.properties"
089     * file.
090     */
091    public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
092
093    /** Open angle bracket literal. */
094    private static final String OPEN_ANGLE_BRACKET = "<";
095
096    /** Close angle bracket literal. */
097    private static final String CLOSE_ANGLE_BRACKET = ">";
098
099    /** 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}