View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 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>.</p>
21   *
22   * <p>This class is not thread safe. It builds a list of line ranges by visiting
23   * each method definition and each anonymous inner type, keeps them in
24   * instance fields until finishTree() reports and clears them, so a single
25   * instance must not be shared between threads.</p>
26   *
27   * @since 0.3
28   */
29  public final class EmptyLinesCheck extends AbstractCheck {
30  
31      /**
32       * Pattern for empty line check.
33       */
34      private static final Pattern PATTERN = Pattern.compile("^\\s*$");
35  
36      /**
37       * Line ranges of all anonymous inner types.
38       */
39      private final LineRanges anons;
40  
41      /**
42       * Line ranges of all method and constructor bodies.
43       */
44      private final LineRanges methods;
45  
46      /**
47       * Default constructor.
48       */
49      public EmptyLinesCheck() {
50          this.anons = new LineRanges();
51          this.methods = new LineRanges();
52      }
53  
54      @Override
55      public int[] getDefaultTokens() {
56          return new int[] {
57              TokenTypes.METHOD_DEF,
58              TokenTypes.CTOR_DEF,
59              TokenTypes.OBJBLOCK,
60          };
61      }
62  
63      @Override
64      public int[] getAcceptableTokens() {
65          return this.getDefaultTokens();
66      }
67  
68      @Override
69      public int[] getRequiredTokens() {
70          return this.getDefaultTokens();
71      }
72  
73      @Override
74      public void visitToken(final DetailAST ast) {
75          this.getLine(ast.getLastChild().getLineNo() - 1);
76          if (ast.getType() == TokenTypes.OBJBLOCK
77              && ast.getParent() != null
78              && ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
79              final DetailAST left = ast.getFirstChild();
80              final DetailAST right = ast.getLastChild();
81              if (left != null && right != null) {
82                  this.anons.add(
83                      new LineRange(left.getLineNo(), right.getLineNo())
84                  );
85              }
86          } else if (ast.getType() == TokenTypes.METHOD_DEF
87              || ast.getType() == TokenTypes.CTOR_DEF) {
88              final DetailAST opening = ast.findFirstToken(TokenTypes.SLIST);
89              if (opening != null) {
90                  this.methods.add(
91                      new LineRange(
92                          opening.getLineNo(),
93                          opening.findFirstToken(TokenTypes.RCURLY).getLineNo()
94                      )
95                  );
96              }
97          }
98      }
99  
100     @Override
101     public void finishTree(final DetailAST root) {
102         final String[] lines = this.getLines();
103         for (int line = 0; line < lines.length; ++line) {
104             if (this.methods.inRange(line + 1)
105                 && EmptyLinesCheck.PATTERN.matcher(lines[line]).find()
106                 && this.insideMethod(line + 1)) {
107                 this.log(line + 1, "Empty line inside method");
108             }
109         }
110         this.methods.clear();
111         this.anons.clear();
112         super.finishTree(root);
113     }
114 
115     private boolean insideMethod(final int line) {
116         return EmptyLinesCheck.linesBetweenBraces(
117             line, this.methods::iterator, Integer.MIN_VALUE
118         ) < EmptyLinesCheck.linesBetweenBraces(
119             line, this.anons::iterator, Integer.MAX_VALUE
120         );
121     }
122 
123     private static int linesBetweenBraces(final int line,
124         final Iterable<LineRange> iterator, final int def) {
125         return StreamSupport.stream(iterator.spliterator(), false)
126             .filter(r -> r.within(line))
127             .min(Comparator.comparingInt(r -> r.last() - r.first()))
128             .map(r -> r.last() - r.first())
129             .orElse(def);
130     }
131 }