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