View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.blocks;
21  
22  import java.util.Locale;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
29  
30  /**
31   * <p>
32   * Checks the placement of left curly braces on types, methods and
33   * other blocks:
34   *  {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH},  {@link
35   * TokenTypes#LITERAL_DO LITERAL_DO},  {@link TokenTypes#LITERAL_ELSE
36   * LITERAL_ELSE},  {@link TokenTypes#LITERAL_FINALLY LITERAL_FINALLY},  {@link
37   * TokenTypes#LITERAL_FOR LITERAL_FOR},  {@link TokenTypes#LITERAL_IF
38   * LITERAL_IF},  {@link TokenTypes#LITERAL_SWITCH LITERAL_SWITCH},  {@link
39   * TokenTypes#LITERAL_SYNCHRONIZED LITERAL_SYNCHRONIZED},  {@link
40   * TokenTypes#LITERAL_TRY LITERAL_TRY},  {@link TokenTypes#LITERAL_WHILE
41   * LITERAL_WHILE},  {@link TokenTypes#STATIC_INIT STATIC_INIT},
42   * {@link TokenTypes#LAMBDA LAMBDA}.
43   * </p>
44   *
45   * <p>
46   * The policy to verify is specified using the {@link LeftCurlyOption} class and
47   * defaults to {@link LeftCurlyOption#EOL}.
48   * </p>
49   * <p>
50   * An example of how to configure the check is:
51   * </p>
52   * <pre>
53   * &lt;module name="LeftCurly"/&gt;
54   * </pre>
55   * <p>
56   * An example of how to configure the check with policy
57   * {@link LeftCurlyOption#NLOW} is:
58   * </p>
59   * <pre>
60   * &lt;module name="LeftCurly"&gt;
61   *      &lt;property name="option" value="nlow"/&gt;
62   * &lt;/module&gt;
63   * </pre>
64   * <p>
65   * An example of how to configure the check to validate enum definitions:
66   * </p>
67   * <pre>
68   * &lt;module name="LeftCurly"&gt;
69   *      &lt;property name="ignoreEnums" value="false"/&gt;
70   * &lt;/module&gt;
71   * </pre>
72   *
73   * @author Oliver Burn
74   * @author lkuehne
75   * @author maxvetrenko
76   */
77  @StatelessCheck
78  public class LeftCurlyCheck
79      extends AbstractCheck {
80      /**
81       * A key is pointing to the warning message text in "messages.properties"
82       * file.
83       */
84      public static final String MSG_KEY_LINE_NEW = "line.new";
85  
86      /**
87       * A key is pointing to the warning message text in "messages.properties"
88       * file.
89       */
90      public static final String MSG_KEY_LINE_PREVIOUS = "line.previous";
91  
92      /**
93       * A key is pointing to the warning message text in "messages.properties"
94       * file.
95       */
96      public static final String MSG_KEY_LINE_BREAK_AFTER = "line.break.after";
97  
98      /** Open curly brace literal. */
99      private static final String OPEN_CURLY_BRACE = "{";
100 
101     /** If true, Check will ignore enums. */
102     private boolean ignoreEnums = true;
103 
104     /** The policy to enforce. */
105     private LeftCurlyOption option = LeftCurlyOption.EOL;
106 
107     /**
108      * Set the option to enforce.
109      * @param optionStr string to decode option from
110      * @throws IllegalArgumentException if unable to decode
111      */
112     public void setOption(String optionStr) {
113         try {
114             option = LeftCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
115         }
116         catch (IllegalArgumentException iae) {
117             throw new IllegalArgumentException("unable to parse " + optionStr, iae);
118         }
119     }
120 
121     /**
122      * Sets whether check should ignore enums when left curly brace policy is EOL.
123      * @param ignoreEnums check's option for ignoring enums.
124      */
125     public void setIgnoreEnums(boolean ignoreEnums) {
126         this.ignoreEnums = ignoreEnums;
127     }
128 
129     @Override
130     public int[] getDefaultTokens() {
131         return getAcceptableTokens();
132     }
133 
134     @Override
135     public int[] getAcceptableTokens() {
136         return new int[] {
137             TokenTypes.INTERFACE_DEF,
138             TokenTypes.CLASS_DEF,
139             TokenTypes.ANNOTATION_DEF,
140             TokenTypes.ENUM_DEF,
141             TokenTypes.CTOR_DEF,
142             TokenTypes.METHOD_DEF,
143             TokenTypes.ENUM_CONSTANT_DEF,
144             TokenTypes.LITERAL_WHILE,
145             TokenTypes.LITERAL_TRY,
146             TokenTypes.LITERAL_CATCH,
147             TokenTypes.LITERAL_FINALLY,
148             TokenTypes.LITERAL_SYNCHRONIZED,
149             TokenTypes.LITERAL_SWITCH,
150             TokenTypes.LITERAL_DO,
151             TokenTypes.LITERAL_IF,
152             TokenTypes.LITERAL_ELSE,
153             TokenTypes.LITERAL_FOR,
154             TokenTypes.STATIC_INIT,
155             TokenTypes.OBJBLOCK,
156             TokenTypes.LAMBDA,
157         };
158     }
159 
160     @Override
161     public int[] getRequiredTokens() {
162         return CommonUtils.EMPTY_INT_ARRAY;
163     }
164 
165     @Override
166     public void visitToken(DetailAST ast) {
167         final DetailAST startToken;
168         DetailAST brace;
169 
170         switch (ast.getType()) {
171             case TokenTypes.CTOR_DEF:
172             case TokenTypes.METHOD_DEF:
173                 startToken = skipAnnotationOnlyLines(ast);
174                 brace = ast.findFirstToken(TokenTypes.SLIST);
175                 break;
176             case TokenTypes.INTERFACE_DEF:
177             case TokenTypes.CLASS_DEF:
178             case TokenTypes.ANNOTATION_DEF:
179             case TokenTypes.ENUM_DEF:
180             case TokenTypes.ENUM_CONSTANT_DEF:
181                 startToken = skipAnnotationOnlyLines(ast);
182                 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
183                 brace = objBlock;
184 
185                 if (objBlock != null) {
186                     brace = objBlock.getFirstChild();
187                 }
188                 break;
189             case TokenTypes.LITERAL_WHILE:
190             case TokenTypes.LITERAL_CATCH:
191             case TokenTypes.LITERAL_SYNCHRONIZED:
192             case TokenTypes.LITERAL_FOR:
193             case TokenTypes.LITERAL_TRY:
194             case TokenTypes.LITERAL_FINALLY:
195             case TokenTypes.LITERAL_DO:
196             case TokenTypes.LITERAL_IF:
197             case TokenTypes.STATIC_INIT:
198             case TokenTypes.LAMBDA:
199                 startToken = ast;
200                 brace = ast.findFirstToken(TokenTypes.SLIST);
201                 break;
202             case TokenTypes.LITERAL_ELSE:
203                 startToken = ast;
204                 final DetailAST candidate = ast.getFirstChild();
205                 brace = null;
206 
207                 if (candidate.getType() == TokenTypes.SLIST) {
208                     brace = candidate;
209                 }
210                 break;
211             default:
212                 // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
213                 // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
214                 // It has been done to improve coverage to 100%. I couldn't replace it with
215                 // if-else-if block because code was ugly and didn't pass pmd check.
216 
217                 startToken = ast;
218                 brace = ast.findFirstToken(TokenTypes.LCURLY);
219                 break;
220         }
221 
222         if (brace != null) {
223             verifyBrace(brace, startToken);
224         }
225     }
226 
227     /**
228      * Skip lines that only contain {@code TokenTypes.ANNOTATION}s.
229      * If the received {@code DetailAST}
230      * has annotations within its modifiers then first token on the line
231      * of the first token after all annotations is return. This might be
232      * an annotation.
233      * Otherwise, the received {@code DetailAST} is returned.
234      * @param ast {@code DetailAST}.
235      * @return {@code DetailAST}.
236      */
237     private static DetailAST skipAnnotationOnlyLines(DetailAST ast) {
238         DetailAST resultNode = ast;
239         final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
240 
241         if (modifiers != null) {
242             final DetailAST lastAnnotation = findLastAnnotation(modifiers);
243 
244             if (lastAnnotation != null) {
245                 final DetailAST tokenAfterLast;
246 
247                 if (lastAnnotation.getNextSibling() == null) {
248                     tokenAfterLast = modifiers.getNextSibling();
249                 }
250                 else {
251                     tokenAfterLast = lastAnnotation.getNextSibling();
252                 }
253 
254                 if (tokenAfterLast.getLineNo() > lastAnnotation.getLineNo()) {
255                     resultNode = tokenAfterLast;
256                 }
257                 else {
258                     resultNode = getFirstAnnotationOnSameLine(lastAnnotation);
259                 }
260             }
261         }
262         return resultNode;
263     }
264 
265     /**
266      * Returns first annotation on same line.
267      * @param annotation
268      *            last annotation on the line
269      * @return first annotation on same line.
270      */
271     private static DetailAST getFirstAnnotationOnSameLine(DetailAST annotation) {
272         DetailAST previousAnnotation = annotation;
273         final int lastAnnotationLineNumber = previousAnnotation.getLineNo();
274         while (previousAnnotation.getPreviousSibling() != null
275                 && previousAnnotation.getPreviousSibling().getLineNo()
276                     == lastAnnotationLineNumber) {
277 
278             previousAnnotation = previousAnnotation.getPreviousSibling();
279         }
280         return previousAnnotation;
281     }
282 
283     /**
284      * Find the last token of type {@code TokenTypes.ANNOTATION}
285      * under the given set of modifiers.
286      * @param modifiers {@code DetailAST}.
287      * @return {@code DetailAST} or null if there are no annotations.
288      */
289     private static DetailAST findLastAnnotation(DetailAST modifiers) {
290         DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
291         while (annotation != null && annotation.getNextSibling() != null
292                && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
293             annotation = annotation.getNextSibling();
294         }
295         return annotation;
296     }
297 
298     /**
299      * Verifies that a specified left curly brace is placed correctly
300      * according to policy.
301      * @param brace token for left curly brace
302      * @param startToken token for start of expression
303      */
304     private void verifyBrace(final DetailAST brace,
305                              final DetailAST startToken) {
306         final String braceLine = getLine(brace.getLineNo() - 1);
307 
308         // Check for being told to ignore, or have '{}' which is a special case
309         if (braceLine.length() <= brace.getColumnNo() + 1
310                 || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
311             if (option == LeftCurlyOption.NL) {
312                 if (!CommonUtils.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
313                     log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
314                 }
315             }
316             else if (option == LeftCurlyOption.EOL) {
317 
318                 validateEol(brace, braceLine);
319             }
320             else if (startToken.getLineNo() != brace.getLineNo()) {
321 
322                 validateNewLinePosition(brace, startToken, braceLine);
323 
324             }
325         }
326     }
327 
328     /**
329      * Validate EOL case.
330      * @param brace brace AST
331      * @param braceLine line content
332      */
333     private void validateEol(DetailAST brace, String braceLine) {
334         if (CommonUtils.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
335             log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
336         }
337         if (!hasLineBreakAfter(brace)) {
338             log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
339         }
340     }
341 
342     /**
343      * Validate token on new Line position.
344      * @param brace brace AST
345      * @param startToken start Token
346      * @param braceLine content of line with Brace
347      */
348     private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
349         // not on the same line
350         if (startToken.getLineNo() + 1 == brace.getLineNo()) {
351             if (CommonUtils.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
352                 log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
353             }
354             else {
355                 log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
356             }
357         }
358         else if (!CommonUtils.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
359             log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
360         }
361     }
362 
363     /**
364      * Checks if left curly has line break after.
365      * @param leftCurly
366      *        Left curly token.
367      * @return
368      *        True, left curly has line break after.
369      */
370     private boolean hasLineBreakAfter(DetailAST leftCurly) {
371         DetailAST nextToken = null;
372         if (leftCurly.getType() == TokenTypes.SLIST) {
373             nextToken = leftCurly.getFirstChild();
374         }
375         else {
376             if (!ignoreEnums
377                     && leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
378                 nextToken = leftCurly.getNextSibling();
379             }
380         }
381         return nextToken == null
382                 || nextToken.getType() == TokenTypes.RCURLY
383                 || leftCurly.getLineNo() != nextToken.getLineNo();
384     }
385 }