001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 * "{@literal <}" and "{@literal >}" 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 ("{@literal <}"):
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 ("{@literal >}"):
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    /**
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}