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.blocks;
021
022import java.util.Locale;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
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.CommonUtil;
029
030/**
031 * <p>
032 * Checks the placement of left curly braces on types, methods and
033 * other blocks:
034 *  {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH},  {@link
035 * TokenTypes#LITERAL_DO LITERAL_DO},  {@link TokenTypes#LITERAL_ELSE
036 * LITERAL_ELSE},  {@link TokenTypes#LITERAL_FINALLY LITERAL_FINALLY},  {@link
037 * TokenTypes#LITERAL_FOR LITERAL_FOR},  {@link TokenTypes#LITERAL_IF
038 * LITERAL_IF},  {@link TokenTypes#LITERAL_SWITCH LITERAL_SWITCH},  {@link
039 * TokenTypes#LITERAL_SYNCHRONIZED LITERAL_SYNCHRONIZED},  {@link
040 * TokenTypes#LITERAL_TRY LITERAL_TRY},  {@link TokenTypes#LITERAL_WHILE
041 * LITERAL_WHILE},  {@link TokenTypes#STATIC_INIT STATIC_INIT},
042 * {@link TokenTypes#LAMBDA LAMBDA}.
043 * </p>
044 *
045 * <p>
046 * The policy to verify is specified using the {@link LeftCurlyOption} class and
047 * defaults to {@link LeftCurlyOption#EOL}.
048 * </p>
049 * <p>
050 * An example of how to configure the check is:
051 * </p>
052 * <pre>
053 * &lt;module name="LeftCurly"/&gt;
054 * </pre>
055 * <p>
056 * An example of how to configure the check with policy
057 * {@link LeftCurlyOption#NLOW} is:
058 * </p>
059 * <pre>
060 * &lt;module name="LeftCurly"&gt;
061 *      &lt;property name="option" value="nlow"/&gt;
062 * &lt;/module&gt;
063 * </pre>
064 * <p>
065 * An example of how to configure the check to validate enum definitions:
066 * </p>
067 * <pre>
068 * &lt;module name="LeftCurly"&gt;
069 *      &lt;property name="ignoreEnums" value="false"/&gt;
070 * &lt;/module&gt;
071 * </pre>
072 *
073 */
074@StatelessCheck
075public class LeftCurlyCheck
076    extends AbstractCheck {
077
078    /**
079     * A key is pointing to the warning message text in "messages.properties"
080     * file.
081     */
082    public static final String MSG_KEY_LINE_NEW = "line.new";
083
084    /**
085     * A key is pointing to the warning message text in "messages.properties"
086     * file.
087     */
088    public static final String MSG_KEY_LINE_PREVIOUS = "line.previous";
089
090    /**
091     * A key is pointing to the warning message text in "messages.properties"
092     * file.
093     */
094    public static final String MSG_KEY_LINE_BREAK_AFTER = "line.break.after";
095
096    /** Open curly brace literal. */
097    private static final String OPEN_CURLY_BRACE = "{";
098
099    /** If true, Check will ignore enums. */
100    private boolean ignoreEnums = true;
101
102    /** The policy to enforce. */
103    private LeftCurlyOption option = LeftCurlyOption.EOL;
104
105    /**
106     * Set the option to enforce.
107     * @param optionStr string to decode option from
108     * @throws IllegalArgumentException if unable to decode
109     */
110    public void setOption(String optionStr) {
111        try {
112            option = LeftCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
113        }
114        catch (IllegalArgumentException iae) {
115            throw new IllegalArgumentException("unable to parse " + optionStr, iae);
116        }
117    }
118
119    /**
120     * Sets whether check should ignore enums when left curly brace policy is EOL.
121     * @param ignoreEnums check's option for ignoring enums.
122     */
123    public void setIgnoreEnums(boolean ignoreEnums) {
124        this.ignoreEnums = ignoreEnums;
125    }
126
127    @Override
128    public int[] getDefaultTokens() {
129        return getAcceptableTokens();
130    }
131
132    @Override
133    public int[] getAcceptableTokens() {
134        return new int[] {
135            TokenTypes.INTERFACE_DEF,
136            TokenTypes.CLASS_DEF,
137            TokenTypes.ANNOTATION_DEF,
138            TokenTypes.ENUM_DEF,
139            TokenTypes.CTOR_DEF,
140            TokenTypes.METHOD_DEF,
141            TokenTypes.ENUM_CONSTANT_DEF,
142            TokenTypes.LITERAL_WHILE,
143            TokenTypes.LITERAL_TRY,
144            TokenTypes.LITERAL_CATCH,
145            TokenTypes.LITERAL_FINALLY,
146            TokenTypes.LITERAL_SYNCHRONIZED,
147            TokenTypes.LITERAL_SWITCH,
148            TokenTypes.LITERAL_DO,
149            TokenTypes.LITERAL_IF,
150            TokenTypes.LITERAL_ELSE,
151            TokenTypes.LITERAL_FOR,
152            TokenTypes.STATIC_INIT,
153            TokenTypes.OBJBLOCK,
154            TokenTypes.LAMBDA,
155        };
156    }
157
158    @Override
159    public int[] getRequiredTokens() {
160        return CommonUtil.EMPTY_INT_ARRAY;
161    }
162
163    @Override
164    public void visitToken(DetailAST ast) {
165        final DetailAST startToken;
166        DetailAST brace;
167
168        switch (ast.getType()) {
169            case TokenTypes.CTOR_DEF:
170            case TokenTypes.METHOD_DEF:
171                startToken = skipAnnotationOnlyLines(ast);
172                brace = ast.findFirstToken(TokenTypes.SLIST);
173                break;
174            case TokenTypes.INTERFACE_DEF:
175            case TokenTypes.CLASS_DEF:
176            case TokenTypes.ANNOTATION_DEF:
177            case TokenTypes.ENUM_DEF:
178            case TokenTypes.ENUM_CONSTANT_DEF:
179                startToken = skipAnnotationOnlyLines(ast);
180                final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
181                brace = objBlock;
182
183                if (objBlock != null) {
184                    brace = objBlock.getFirstChild();
185                }
186                break;
187            case TokenTypes.LITERAL_WHILE:
188            case TokenTypes.LITERAL_CATCH:
189            case TokenTypes.LITERAL_SYNCHRONIZED:
190            case TokenTypes.LITERAL_FOR:
191            case TokenTypes.LITERAL_TRY:
192            case TokenTypes.LITERAL_FINALLY:
193            case TokenTypes.LITERAL_DO:
194            case TokenTypes.LITERAL_IF:
195            case TokenTypes.STATIC_INIT:
196            case TokenTypes.LAMBDA:
197                startToken = ast;
198                brace = ast.findFirstToken(TokenTypes.SLIST);
199                break;
200            case TokenTypes.LITERAL_ELSE:
201                startToken = ast;
202                final DetailAST candidate = ast.getFirstChild();
203                brace = null;
204
205                if (candidate.getType() == TokenTypes.SLIST) {
206                    brace = candidate;
207                }
208                break;
209            default:
210                // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
211                // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
212                // It has been done to improve coverage to 100%. I couldn't replace it with
213                // if-else-if block because code was ugly and didn't pass pmd check.
214
215                startToken = ast;
216                brace = ast.findFirstToken(TokenTypes.LCURLY);
217                break;
218        }
219
220        if (brace != null) {
221            verifyBrace(brace, startToken);
222        }
223    }
224
225    /**
226     * Skip lines that only contain {@code TokenTypes.ANNOTATION}s.
227     * If the received {@code DetailAST}
228     * has annotations within its modifiers then first token on the line
229     * of the first token after all annotations is return. This might be
230     * an annotation.
231     * Otherwise, the received {@code DetailAST} is returned.
232     * @param ast {@code DetailAST}.
233     * @return {@code DetailAST}.
234     */
235    private static DetailAST skipAnnotationOnlyLines(DetailAST ast) {
236        DetailAST resultNode = ast;
237        final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
238
239        if (modifiers != null) {
240            final DetailAST lastAnnotation = findLastAnnotation(modifiers);
241
242            if (lastAnnotation != null) {
243                final DetailAST tokenAfterLast;
244
245                if (lastAnnotation.getNextSibling() == null) {
246                    tokenAfterLast = modifiers.getNextSibling();
247                }
248                else {
249                    tokenAfterLast = lastAnnotation.getNextSibling();
250                }
251
252                if (tokenAfterLast.getLineNo() > lastAnnotation.getLineNo()) {
253                    resultNode = tokenAfterLast;
254                }
255                else {
256                    resultNode = getFirstAnnotationOnSameLine(lastAnnotation);
257                }
258            }
259        }
260        return resultNode;
261    }
262
263    /**
264     * Returns first annotation on same line.
265     * @param annotation
266     *            last annotation on the line
267     * @return first annotation on same line.
268     */
269    private static DetailAST getFirstAnnotationOnSameLine(DetailAST annotation) {
270        DetailAST previousAnnotation = annotation;
271        final int lastAnnotationLineNumber = previousAnnotation.getLineNo();
272        while (previousAnnotation.getPreviousSibling() != null
273                && previousAnnotation.getPreviousSibling().getLineNo()
274                    == lastAnnotationLineNumber) {
275            previousAnnotation = previousAnnotation.getPreviousSibling();
276        }
277        return previousAnnotation;
278    }
279
280    /**
281     * Find the last token of type {@code TokenTypes.ANNOTATION}
282     * under the given set of modifiers.
283     * @param modifiers {@code DetailAST}.
284     * @return {@code DetailAST} or null if there are no annotations.
285     */
286    private static DetailAST findLastAnnotation(DetailAST modifiers) {
287        DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
288        while (annotation != null && annotation.getNextSibling() != null
289               && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
290            annotation = annotation.getNextSibling();
291        }
292        return annotation;
293    }
294
295    /**
296     * Verifies that a specified left curly brace is placed correctly
297     * according to policy.
298     * @param brace token for left curly brace
299     * @param startToken token for start of expression
300     */
301    private void verifyBrace(final DetailAST brace,
302                             final DetailAST startToken) {
303        final String braceLine = getLine(brace.getLineNo() - 1);
304
305        // Check for being told to ignore, or have '{}' which is a special case
306        if (braceLine.length() <= brace.getColumnNo() + 1
307                || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
308            if (option == LeftCurlyOption.NL) {
309                if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
310                    log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
311                }
312            }
313            else if (option == LeftCurlyOption.EOL) {
314                validateEol(brace, braceLine);
315            }
316            else if (startToken.getLineNo() != brace.getLineNo()) {
317                validateNewLinePosition(brace, startToken, braceLine);
318            }
319        }
320    }
321
322    /**
323     * Validate EOL case.
324     * @param brace brace AST
325     * @param braceLine line content
326     */
327    private void validateEol(DetailAST brace, String braceLine) {
328        if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
329            log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
330        }
331        if (!hasLineBreakAfter(brace)) {
332            log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
333        }
334    }
335
336    /**
337     * Validate token on new Line position.
338     * @param brace brace AST
339     * @param startToken start Token
340     * @param braceLine content of line with Brace
341     */
342    private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
343        // not on the same line
344        if (startToken.getLineNo() + 1 == brace.getLineNo()) {
345            if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
346                log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
347            }
348            else {
349                log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
350            }
351        }
352        else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
353            log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
354        }
355    }
356
357    /**
358     * Checks if left curly has line break after.
359     * @param leftCurly
360     *        Left curly token.
361     * @return
362     *        True, left curly has line break after.
363     */
364    private boolean hasLineBreakAfter(DetailAST leftCurly) {
365        DetailAST nextToken = null;
366        if (leftCurly.getType() == TokenTypes.SLIST) {
367            nextToken = leftCurly.getFirstChild();
368        }
369        else {
370            if (!ignoreEnums
371                    && leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
372                nextToken = leftCurly.getNextSibling();
373            }
374        }
375        return nextToken == null
376                || nextToken.getType() == TokenTypes.RCURLY
377                || leftCurly.getLineNo() != nextToken.getLineNo();
378    }
379
380}