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:
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:
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>.
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 = 80;
46  
47      /**
48       * Configure the maximum allowed length.
49       * @param value New value
50       */
51      public void setMax(final int value) {
52          this.max = value;
53      }
54  
55      @Override
56      public int[] getDefaultTokens() {
57          return this.getRequiredTokens();
58      }
59  
60      @Override
61      public int[] getAcceptableTokens() {
62          return this.getRequiredTokens();
63      }
64  
65      @Override
66      public int[] getRequiredTokens() {
67          return new int[] {TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF};
68      }
69  
70      @Override
71      public void visitToken(final DetailAST ast) {
72          final DetailAST start = MethodDeclarationLengthCheck.head(ast);
73          final DetailAST end = MethodDeclarationLengthCheck.tail(ast);
74          if (start != null && end != null
75              && start.getLineNo() < end.getLineNo()) {
76              this.verify(start, end);
77          }
78      }
79  
80      /**
81       * Checks whether the joined declaration fits on one line.
82       * @param start First non-annotation token of the declaration
83       * @param end Token that terminates the declaration (SLIST or SEMI)
84       */
85      private void verify(final DetailAST start, final DetailAST end) {
86          final String[] lines = this.getLines();
87          final int first = start.getLineNo();
88          final int last = end.getLineNo();
89          final int col = start.getColumnNo();
90          final StringBuilder joined = new StringBuilder(
91              lines[first - 1].substring(col).trim()
92          );
93          for (int idx = first; idx < last; idx += 1) {
94              final String trimmed = lines[idx].trim();
95              if (!trimmed.isEmpty()) {
96                  joined.append(' ').append(trimmed);
97              }
98          }
99          if (col + joined.length() <= this.max) {
100             this.log(
101                 first,
102                 "Method declaration can be placed on a single line"
103             );
104         }
105     }
106 
107     /**
108      * Returns the first child of the method/constructor that is not an
109      * annotation (i.e., the first keyword or type of the signature).
110      * @param def METHOD_DEF or CTOR_DEF node
111      * @return First non-annotation child token, or null if absent
112      */
113     private static DetailAST head(final DetailAST def) {
114         final DetailAST modifiers = def.findFirstToken(TokenTypes.MODIFIERS);
115         DetailAST child = modifiers.getFirstChild();
116         while (child != null && child.getType() == TokenTypes.ANNOTATION) {
117             child = child.getNextSibling();
118         }
119         final DetailAST result;
120         if (child == null) {
121             DetailAST fallback = def.findFirstToken(TokenTypes.TYPE);
122             if (fallback == null) {
123                 fallback = def.findFirstToken(TokenTypes.IDENT);
124             }
125             result = fallback;
126         } else {
127             result = child;
128         }
129         return result;
130     }
131 
132     /**
133      * Returns the token that closes the declaration: the opening brace
134      * of the body, or the terminating semicolon for abstract methods.
135      * @param def METHOD_DEF or CTOR_DEF node
136      * @return SLIST or SEMI child, or null if neither is present
137      */
138     private static DetailAST tail(final DetailAST def) {
139         DetailAST end = def.findFirstToken(TokenTypes.SLIST);
140         if (end == null) {
141             end = def.findFirstToken(TokenTypes.SEMI);
142         }
143         return end;
144     }
145 }