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