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  
11  /**
12   * Checks that method and constructor declarations do not span multiple
13   * lines when the entire signature can fit on one line within the limit.
14   *
15   * <p>This is how a correct declaration looks like:</p>
16   *
17   * <pre>
18   * public Foo(final int max) throws IOException {
19   *     ...
20   * }
21   * </pre>
22   *
23   * <p>And this is what will be reported:</p>
24   *
25   * <pre>
26   * public Foo(final int max)
27   *     throws IOException {
28   *     ...
29   * }
30   * </pre>
31   *
32   * <p>The reason is explained in <a
33   * href="https://www.yegor256.com/2014/04/27/typical-mistakes-in-java-code.html#indentation">this
34   * article</a>: one should put as much as possible on one line within the
35   * configured limit (80 by default). See <a
36   * href="https://github.com/yegor256/qulice/issues/647">#647</a>.</p>
37   *
38   * @since 0.24
39   */
40  public final class MethodDeclarationLengthCheck extends AbstractCheck {
41  
42      /**
43       * Maximum allowed length of the joined declaration, in characters.
44       */
45      private int max;
46  
47      /**
48       * Default constructor.
49       */
50      public MethodDeclarationLengthCheck() {
51          this.max = 80;
52      }
53  
54      /**
55       * Configure the maximum allowed length.
56       *
57       * @param value New value
58       */
59      public void setMax(final int value) {
60          this.max = value;
61      }
62  
63      @Override
64      public int[] getDefaultTokens() {
65          return this.getRequiredTokens();
66      }
67  
68      @Override
69      public int[] getAcceptableTokens() {
70          return this.getRequiredTokens();
71      }
72  
73      @Override
74      public int[] getRequiredTokens() {
75          return new int[] {TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF};
76      }
77  
78      @Override
79      public void visitToken(final DetailAST ast) {
80          final DetailAST start = MethodDeclarationLengthCheck.head(ast);
81          final DetailAST end = MethodDeclarationLengthCheck.tail(ast);
82          if (start != null && end != null
83              && start.getLineNo() < end.getLineNo()) {
84              this.verify(start, end);
85          }
86      }
87  
88      private void verify(final DetailAST start, final DetailAST end) {
89          final String[] lines = this.getLines();
90          final int first = start.getLineNo();
91          final int last = end.getLineNo();
92          final int col = start.getColumnNo();
93          final StringBuilder joined = new StringBuilder(
94              lines[first - 1].substring(col).trim()
95          );
96          for (int idx = first; idx < last; idx += 1) {
97              final String trimmed = lines[idx].trim();
98              if (!trimmed.isEmpty()) {
99                  joined.append(' ').append(trimmed);
100             }
101         }
102         if (col + joined.length() <= this.max) {
103             this.log(
104                 first,
105                 "Method declaration can be placed on a single line"
106             );
107         }
108     }
109 
110     private static DetailAST head(final DetailAST def) {
111         final DetailAST modifiers = def.findFirstToken(TokenTypes.MODIFIERS);
112         DetailAST child = modifiers.getFirstChild();
113         while (child != null && child.getType() == TokenTypes.ANNOTATION) {
114             child = child.getNextSibling();
115         }
116         final DetailAST result;
117         if (child == null) {
118             DetailAST fallback = def.findFirstToken(TokenTypes.TYPE);
119             if (fallback == null) {
120                 fallback = def.findFirstToken(TokenTypes.IDENT);
121             }
122             result = fallback;
123         } else {
124             result = child;
125         }
126         return result;
127     }
128 
129     private static DetailAST tail(final DetailAST def) {
130         DetailAST end = def.findFirstToken(TokenTypes.SLIST);
131         if (end == null) {
132             end = def.findFirstToken(TokenTypes.SEMI);
133         }
134         return end;
135     }
136 }