View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.Comparator;
11  import java.util.regex.Pattern;
12  import java.util.stream.StreamSupport;
13  
14  /**
15   * Check for empty lines inside methods and constructors.
16   *
17   * <p>We believe that comments and empty lines are evil. If you need to use
18   * an empty line in order to add a vertical separator of concepts - refactor
19   * your code and make it more cohesive and readable. The bottom line is
20   * that every method should look solid and do just <b>one thing</b>.
21   *
22   * This class is thread safe. It relies on building a list of line ranges by
23   * visiting each method definition and each anonymous inner type. It stores
24   * these references in a non-static thread local.
25   *
26   * @since 0.3
27   */
28  public final class EmptyLinesCheck extends AbstractCheck {
29  
30      /**
31       * Pattern for empty line check.
32       */
33      private static final Pattern PATTERN = Pattern.compile("^\\s*$");
34  
35      /**
36       * Line ranges of all anonymous inner types.
37       */
38      private final LineRanges anons = new LineRanges();
39  
40      /**
41       * Line ranges of all method and constructor bodies.
42       */
43      private final LineRanges methods = new LineRanges();
44  
45      @Override
46      public int[] getDefaultTokens() {
47          return new int[] {
48              TokenTypes.METHOD_DEF,
49              TokenTypes.CTOR_DEF,
50              TokenTypes.OBJBLOCK,
51          };
52      }
53  
54      @Override
55      public int[] getAcceptableTokens() {
56          return this.getDefaultTokens();
57      }
58  
59      @Override
60      public int[] getRequiredTokens() {
61          return this.getDefaultTokens();
62      }
63  
64      @Override
65      public void visitToken(final DetailAST ast) {
66          this.getLine(ast.getLastChild().getLineNo() - 1);
67          if (ast.getType() == TokenTypes.OBJBLOCK
68              && ast.getParent() != null
69              && ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
70              final DetailAST left = ast.getFirstChild();
71              final DetailAST right = ast.getLastChild();
72              if (left != null && right != null) {
73                  this.anons.add(
74                      new LineRange(left.getLineNo(), right.getLineNo())
75                  );
76              }
77          } else if (ast.getType() == TokenTypes.METHOD_DEF
78              || ast.getType() == TokenTypes.CTOR_DEF) {
79              final DetailAST opening = ast.findFirstToken(TokenTypes.SLIST);
80              if (opening != null) {
81                  final DetailAST right =
82                      opening.findFirstToken(TokenTypes.RCURLY);
83                  this.methods.add(
84                      new LineRange(opening.getLineNo(), right.getLineNo())
85                  );
86              }
87          }
88      }
89  
90      @Override
91      public void finishTree(final DetailAST root) {
92          final String[] lines = this.getLines();
93          for (int line = 0; line < lines.length; ++line) {
94              if (this.methods.inRange(line + 1)
95                  && EmptyLinesCheck.PATTERN.matcher(lines[line]).find()
96                  && this.insideMethod(line + 1)) {
97                  this.log(line + 1, "Empty line inside method");
98              }
99          }
100         this.methods.clear();
101         this.anons.clear();
102         super.finishTree(root);
103     }
104 
105     /**
106      * If this is within a valid anonymous class, make sure that is still
107      * directly inside of a method of that anonymous inner class.
108      * Note: This implementation only checks one level deep, as nesting
109      * anonymous inner classes should never been done.
110      * @param line The line to check if it is within a method or not.
111      * @return True if the line is directly inside of a method.
112      */
113     private boolean insideMethod(final int line) {
114         final int method = EmptyLinesCheck.linesBetweenBraces(
115             line, this.methods::iterator, Integer.MIN_VALUE
116         );
117         final int clazz = EmptyLinesCheck.linesBetweenBraces(
118             line, this.anons::iterator, Integer.MAX_VALUE
119         );
120         return method < clazz;
121     }
122 
123     /**
124      * Find number of lines between braces that contain a given line.
125      * @param line Line to check
126      * @param iterator Iterable of line ranges
127      * @param def Default value if line is not within ranges
128      * @return Number of lines between braces
129      */
130     private static int linesBetweenBraces(final int line,
131         final Iterable<LineRange> iterator, final int def) {
132         return StreamSupport.stream(iterator.spliterator(), false)
133             .filter(r -> r.within(line))
134             .min(Comparator.comparingInt(r -> r.last() - r.first()))
135             .map(r -> r.last() - r.first())
136             .orElse(def);
137     }
138 }