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