View Javadoc
1   /*
2    * Copyright (c) 2011-2024 Qulice.com
3    *
4    * All rights reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions
8    * are met: 1) Redistributions of source code must retain the above
9    * copyright notice, this list of conditions and the following
10   * disclaimer. 2) Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following
12   * disclaimer in the documentation and/or other materials provided
13   * with the distribution. 3) Neither the name of the Qulice.com nor
14   * the names of its contributors may be used to endorse or promote
15   * products derived from this software without specific prior written
16   * permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
20   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29   * OF THE POSSIBILITY OF SUCH DAMAGE.
30   */
31  package com.qulice.checkstyle;
32  
33  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34  import com.puppycrawl.tools.checkstyle.api.DetailAST;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import java.util.Comparator;
37  import java.util.regex.Pattern;
38  import java.util.stream.StreamSupport;
39  
40  /**
41   * Check for empty lines inside methods and constructors.
42   *
43   * <p>We believe that comments and empty lines are evil. If you need to use
44   * an empty line in order to add a vertical separator of concepts - refactor
45   * your code and make it more cohesive and readable. The bottom line is
46   * that every method should look solid and do just <b>one thing</b>.
47   *
48   * This class is thread safe. It relies on building a list of line ranges by
49   * visiting each method definition and each anonymous inner type. It stores
50   * these references in a non-static thread local.
51   *
52   * @since 0.3
53   */
54  public final class EmptyLinesCheck extends AbstractCheck {
55  
56      /**
57       * Pattern for empty line check.
58       */
59      private static final Pattern PATTERN = Pattern.compile("^\\s*$");
60  
61      /**
62       * Line ranges of all anonymous inner types.
63       */
64      private final LineRanges anons = new LineRanges();
65  
66      /**
67       * Line ranges of all method and constructor bodies.
68       */
69      private final LineRanges methods = new LineRanges();
70  
71      @Override
72      public int[] getDefaultTokens() {
73          return new int[] {
74              TokenTypes.METHOD_DEF,
75              TokenTypes.CTOR_DEF,
76              TokenTypes.OBJBLOCK,
77          };
78      }
79  
80      @Override
81      public int[] getAcceptableTokens() {
82          return this.getDefaultTokens();
83      }
84  
85      @Override
86      public int[] getRequiredTokens() {
87          return this.getDefaultTokens();
88      }
89  
90      @Override
91      public void visitToken(final DetailAST ast) {
92          this.getLine(ast.getLastChild().getLineNo() - 1);
93          if (ast.getType() == TokenTypes.OBJBLOCK
94              && ast.getParent() != null
95              && ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
96              final DetailAST left = ast.getFirstChild();
97              final DetailAST right = ast.getLastChild();
98              if (left != null && right != null) {
99                  this.anons.add(
100                     new LineRange(left.getLineNo(), right.getLineNo())
101                 );
102             }
103         } else if (ast.getType() == TokenTypes.METHOD_DEF
104             || ast.getType() == TokenTypes.CTOR_DEF) {
105             final DetailAST opening = ast.findFirstToken(TokenTypes.SLIST);
106             if (opening != null) {
107                 final DetailAST right =
108                     opening.findFirstToken(TokenTypes.RCURLY);
109                 this.methods.add(
110                     new LineRange(opening.getLineNo(), right.getLineNo())
111                 );
112             }
113         }
114     }
115 
116     @Override
117     public void finishTree(final DetailAST root) {
118         final String[] lines = this.getLines();
119         for (int line = 0; line < lines.length; ++line) {
120             if (this.methods.inRange(line + 1)
121                 && EmptyLinesCheck.PATTERN.matcher(lines[line]).find()
122                 && this.insideMethod(line + 1)) {
123                 this.log(line + 1, "Empty line inside method");
124             }
125         }
126         this.methods.clear();
127         this.anons.clear();
128         super.finishTree(root);
129     }
130 
131     /**
132      * If this is within a valid anonymous class, make sure that is still
133      * directly inside of a method of that anonymous inner class.
134      * Note: This implementation only checks one level deep, as nesting
135      * anonymous inner classes should never been done.
136      * @param line The line to check if it is within a method or not.
137      * @return True if the line is directly inside of a method.
138      */
139     private boolean insideMethod(final int line) {
140         final int method = EmptyLinesCheck.linesBetweenBraces(
141             line, this.methods::iterator, Integer.MIN_VALUE
142         );
143         final int clazz = EmptyLinesCheck.linesBetweenBraces(
144             line, this.anons::iterator, Integer.MAX_VALUE
145         );
146         return method < clazz;
147     }
148 
149     /**
150      * Find number of lines between braces that contain a given line.
151      * @param line Line to check
152      * @param iterator Iterable of line ranges
153      * @param def Default value if line is not within ranges
154      * @return Number of lines between braces
155      */
156     private static int linesBetweenBraces(final int line,
157         final Iterable<LineRange> iterator, final int def) {
158         return StreamSupport.stream(iterator.spliterator(), false)
159             .filter(r -> r.within(line))
160             .min(Comparator.comparingInt(r -> r.last() - r.first()))
161             .map(r -> r.last() - r.first())
162             .orElse(def);
163     }
164 }