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 * Left angle bracket ("&lt;"):
040 * </p>
041 * <ul>
042 * <li> should be preceded with whitespace only
043 *   in generic methods definitions.</li>
044 * <li> should not be preceded with whitespace
045 *   when it is preceded method name or constructor.</li>
046 * <li> should not be preceded with whitespace when following type name.</li>
047 * <li> should not be followed with whitespace in all cases.</li>
048 * </ul>
049 *
050 * <p>
051 * Right angle bracket ("&gt;"):
052 * </p>
053 * <ul>
054 * <li> should not be preceded with whitespace in all cases.</li>
055 * <li> should be followed with whitespace in almost all cases,
056 *   except diamond operators and when preceding a method name, constructor, or record header.</li>
057 * </ul>
058 *
059 * @since 5.0
060 */
061@FileStatefulCheck
062public class GenericWhitespaceCheck extends AbstractCheck {
063
064    /**
065     * A key is pointing to the warning message text in "messages.properties"
066     * file.
067     */
068    public static final String MSG_WS_PRECEDED = "ws.preceded";
069
070    /**
071     * A key is pointing to the warning message text in "messages.properties"
072     * file.
073     */
074    public static final String MSG_WS_FOLLOWED = "ws.followed";
075
076    /**
077     * A key is pointing to the warning message text in "messages.properties"
078     * file.
079     */
080    public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
081
082    /**
083     * A key is pointing to the warning message text in "messages.properties"
084     * file.
085     */
086    public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
087
088    /** Open angle bracket literal. */
089    private static final String OPEN_ANGLE_BRACKET = "<";
090
091    /** Close angle bracket literal. */
092    private static final String CLOSE_ANGLE_BRACKET = ">";
093
094    /** Used to count the depth of a Generic expression. */
095    private int depth;
096
097    @Override
098    public int[] getDefaultTokens() {
099        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}