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