001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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 com.puppycrawl.tools.checkstyle.FileStatefulCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
027
028/**
029 * <p>
030 * Checks that the whitespace around the Generic tokens (angle brackets)
031 * "&lt;" and "&gt;" are correct to the <i>typical</i> convention.
032 * The convention is not configurable.
033 * </p>
034 * <p>
035 * Left angle bracket ("&lt;"):
036 * </p>
037 * <ul>
038 * <li> should be preceded with whitespace only
039 *   in generic methods definitions.</li>
040 * <li> should not be preceded with whitespace
041 *   when it is precede method name or following type name.</li>
042 * <li> should not be followed with whitespace in all cases.</li>
043 * </ul>
044 * <p>
045 * Right angle bracket ("&gt;"):
046 * </p>
047 * <ul>
048 * <li> should not be preceded with whitespace in all cases.</li>
049 * <li> should be followed with whitespace in almost all cases,
050 *   except diamond operators and when preceding method name.</li></ul>
051 * <p>
052 * Examples with correct spacing:
053 * </p>
054 * <pre>
055 * // Generic methods definitions
056 * public void &lt;K, V extends Number&gt; boolean foo(K, V) {}
057 * // Generic type definition
058 * class name&lt;T1, T2, ..., Tn&gt; {}
059 * // Generic type reference
060 * OrderedPair&lt;String, Box&lt;Integer&gt;&gt; p;
061 * // Generic preceded method name
062 * boolean same = Util.&lt;Integer, String&gt;compare(p1, p2);
063 * // Diamond operator
064 * Pair&lt;Integer, String&gt; p1 = new Pair&lt;&gt;(1, "apple");
065 * // Method reference
066 * List&lt;T&gt; list = ImmutableList.Builder&lt;T&gt;::new;
067 * // Method reference
068 * sort(list, Comparable::&lt;String&gt;compareTo);
069 * </pre>
070 * <p>
071 * To configure the check:
072 * </p>
073 * <pre>
074 * &lt;module name=&quot;GenericWhitespace&quot;/&gt;
075 * </pre>
076 *
077 * @since 5.0
078 */
079@FileStatefulCheck
080public class GenericWhitespaceCheck extends AbstractCheck {
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_PRECEDED = "ws.preceded";
087
088    /**
089     * A key is pointing to the warning message text in "messages.properties"
090     * file.
091     */
092    public static final String MSG_WS_FOLLOWED = "ws.followed";
093
094    /**
095     * A key is pointing to the warning message text in "messages.properties"
096     * file.
097     */
098    public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
099
100    /**
101     * A key is pointing to the warning message text in "messages.properties"
102     * file.
103     */
104    public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
105
106    /** Open angle bracket literal. */
107    private static final String OPEN_ANGLE_BRACKET = "<";
108
109    /** Close angle bracket literal. */
110    private static final String CLOSE_ANGLE_BRACKET = ">";
111
112    /** Used to count the depth of a Generic expression. */
113    private int depth;
114
115    @Override
116    public int[] getDefaultTokens() {
117        return getRequiredTokens();
118    }
119
120    @Override
121    public int[] getAcceptableTokens() {
122        return getRequiredTokens();
123    }
124
125    @Override
126    public int[] getRequiredTokens() {
127        return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
128    }
129
130    @Override
131    public void beginTree(DetailAST rootAST) {
132        // Reset for each tree, just increase there are violations in preceding
133        // trees.
134        depth = 0;
135    }
136
137    @Override
138    public void visitToken(DetailAST ast) {
139        switch (ast.getType()) {
140            case TokenTypes.GENERIC_START:
141                processStart(ast);
142                depth++;
143                break;
144            case TokenTypes.GENERIC_END:
145                processEnd(ast);
146                depth--;
147                break;
148            default:
149                throw new IllegalArgumentException("Unknown type " + ast);
150        }
151    }
152
153    /**
154     * Checks the token for the end of Generics.
155     * @param ast the token to check
156     */
157    private void processEnd(DetailAST ast) {
158        final String line = getLine(ast.getLineNo() - 1);
159        final int before = ast.getColumnNo() - 1;
160        final int after = ast.getColumnNo() + 1;
161
162        if (before >= 0 && Character.isWhitespace(line.charAt(before))
163                && !containsWhitespaceBefore(before, line)) {
164            log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
165        }
166
167        if (after < line.length()) {
168            // Check if the last Generic, in which case must be a whitespace
169            // or a '(),[.'.
170            if (depth == 1) {
171                processSingleGeneric(ast, line, after);
172            }
173            else {
174                processNestedGenerics(ast, line, after);
175            }
176        }
177    }
178
179    /**
180     * Process Nested generics.
181     * @param ast token
182     * @param line line content
183     * @param after position after
184     */
185    private void processNestedGenerics(DetailAST ast, String line, int after) {
186        // In a nested Generic type, so can only be a '>' or ',' or '&'
187
188        // In case of several extends definitions:
189        //
190        //   class IntEnumValueType<E extends Enum<E> & IntEnum>
191        //                                          ^
192        //   should be whitespace if followed by & -+
193        //
194        final int indexOfAmp = line.indexOf('&', after);
195        if (indexOfAmp >= 1
196            && containsWhitespaceBetween(after, indexOfAmp, line)) {
197            if (indexOfAmp - after == 0) {
198                log(ast, MSG_WS_NOT_PRECEDED, "&");
199            }
200            else if (indexOfAmp - after != 1) {
201                log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
202            }
203        }
204        else if (line.charAt(after) == ' ') {
205            log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
206        }
207    }
208
209    /**
210     * Process Single-generic.
211     * @param ast token
212     * @param line line content
213     * @param after position after
214     */
215    private void processSingleGeneric(DetailAST ast, String line, int after) {
216        final char charAfter = line.charAt(after);
217
218        // Need to handle a number of cases. First is:
219        //    Collections.<Object>emptySet();
220        //                        ^
221        //                        +--- whitespace not allowed
222        if (isGenericBeforeMethod(ast)) {
223            if (Character.isWhitespace(charAfter)) {
224                log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
225            }
226        }
227        else if (!isCharacterValidAfterGenericEnd(charAfter)) {
228            log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
229        }
230    }
231
232    /**
233     * Is generic before method reference.
234     * @param ast ast
235     * @return true if generic before a method ref
236     */
237    private static boolean isGenericBeforeMethod(DetailAST ast) {
238        return ast.getParent().getParent().getType() == TokenTypes.DOT
239                && ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
240                || isAfterMethodReference(ast);
241    }
242
243    /**
244     * Checks if current generic end ('>') is located after
245     * {@link TokenTypes#METHOD_REF method reference operator}.
246     * @param genericEnd {@link TokenTypes#GENERIC_END}
247     * @return true if '>' follows after method reference.
248     */
249    private static boolean isAfterMethodReference(DetailAST genericEnd) {
250        return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
251    }
252
253    /**
254     * Checks the token for the start of Generics.
255     * @param ast the token to check
256     */
257    private void processStart(DetailAST ast) {
258        final String line = getLine(ast.getLineNo() - 1);
259        final int before = ast.getColumnNo() - 1;
260        final int after = ast.getColumnNo() + 1;
261
262        // Need to handle two cases as in:
263        //
264        //   public static <T> Callable<T> callable(Runnable task, T result)
265        //                 ^           ^
266        //      ws reqd ---+           +--- whitespace NOT required
267        //
268        if (before >= 0) {
269            // Detect if the first case
270            final DetailAST parent = ast.getParent();
271            final DetailAST grandparent = parent.getParent();
272            if (grandparent.getType() == TokenTypes.CTOR_DEF
273                    || grandparent.getType() == TokenTypes.METHOD_DEF) {
274                // Require whitespace
275                if (!Character.isWhitespace(line.charAt(before))) {
276                    log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
277                }
278            }
279            // Whitespace not required
280            else if (Character.isWhitespace(line.charAt(before))
281                && !containsWhitespaceBefore(before, line)) {
282                log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
283            }
284        }
285
286        if (after < line.length()
287                && Character.isWhitespace(line.charAt(after))) {
288            log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
289        }
290    }
291
292    /**
293     * Returns whether the specified string contains only whitespace between
294     * specified indices.
295     *
296     * @param fromIndex the index to start the search from. Inclusive
297     * @param toIndex the index to finish the search. Exclusive
298     * @param line the line to check
299     * @return whether there are only whitespaces (or nothing)
300     */
301    private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, String line) {
302        boolean result = true;
303        for (int i = fromIndex; i < toIndex; i++) {
304            if (!Character.isWhitespace(line.charAt(i))) {
305                result = false;
306                break;
307            }
308        }
309        return result;
310    }
311
312    /**
313     * Returns whether the specified string contains only whitespace up to specified index.
314     *
315     * @param before the index to start the search from. Inclusive
316     * @param line   the index to finish the search. Exclusive
317     * @return {@code true} if there are only whitespaces,
318     *     false if there is nothing before or some other characters
319     */
320    private static boolean containsWhitespaceBefore(int before, String line) {
321        return before != 0 && CommonUtil.hasWhitespaceBefore(before, line);
322    }
323
324    /**
325     * Checks whether given character is valid to be right after generic ends.
326     * @param charAfter character to check
327     * @return checks if given character is valid
328     */
329    private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
330        return charAfter == '(' || charAfter == ')'
331            || charAfter == ',' || charAfter == '['
332            || charAfter == '.' || charAfter == ':'
333            || charAfter == ';'
334            || Character.isWhitespace(charAfter);
335    }
336
337}