View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.indentation;
21  
22  import com.puppycrawl.tools.checkstyle.api.DetailAST;
23  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
24  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
25  
26  /**
27   * Handler for method calls.
28   *
29   */
30  public class MethodCallHandler extends AbstractExpressionHandler {
31  
32      /**
33       * The indentation context used by this class.
34       */
35      private final IndentationContext context;
36  
37      /**
38       * Construct an instance of this handler with the given indentation context,
39       * abstract syntax tree, and parent handler.
40       *
41       * @param context        the indentation check
42       * @param ast           the abstract syntax tree
43       * @param parent        the parent handler
44       */
45      public MethodCallHandler(IndentationContext context,
46          DetailAST ast, AbstractExpressionHandler parent) {
47          super(context, "method call", ast, parent);
48          this.context = context;
49      }
50  
51      @Override
52      protected IndentLevel getIndentImpl() {
53          final IndentLevel indentLevel;
54          // if inside a method call's params, this could be part of
55          // an expression, so get the previous line's start
56          if (getParent() instanceof MethodCallHandler container) {
57              if (TokenUtil.areOnSameLine(container.getMainAst(), getMainAst())
58                      || isChainedMethodCallWrapped()
59                      || areMethodsChained(container.getMainAst(), getMainAst())) {
60                  indentLevel = container.getIndent();
61              }
62              // we should increase indentation only if this is the first
63              // chained method call which was moved to the next line
64              else {
65                  indentLevel = new IndentLevel(container.getIndent(),
66                      getContext().getLineWrappingIndentation());
67              }
68          }
69          else if (getMainAst().getFirstChild().getType() == TokenTypes.LITERAL_NEW) {
70              indentLevel = super.getIndentImpl();
71          }
72          else {
73              // if our expression isn't first on the line, just use the start
74              // of the line
75              final DetailAstSet astSet = new DetailAstSet(context);
76              findSubtreeAst(astSet, getMainAst().getFirstChild(), true);
77              final int firstCol = expandedTabsColumnNo(astSet.firstLine());
78              final int lineStart = getLineStart(getFirstAst(getMainAst()));
79              if (lineStart == firstCol) {
80                  indentLevel = super.getIndentImpl();
81              }
82              else {
83                  indentLevel = new IndentLevel(lineStart);
84              }
85          }
86          return indentLevel;
87      }
88  
89      /**
90       * Checks if ast2 is a chained method call that starts on the same level as ast1 ends.
91       * In other words, if the right paren of ast1 is on the same level as the lparen of ast2:
92       * {@code
93       *     value.methodOne(
94       *         argument1
95       *     ).methodTwo(
96       *         argument2
97       *     );
98       * }
99       *
100      * @param ast1 Ast1
101      * @param ast2 Ast2
102      * @return True if ast2 begins on the same level that ast1 ends
103      */
104     private static boolean areMethodsChained(DetailAST ast1, DetailAST ast2) {
105         final DetailAST rparen = ast1.findFirstToken(TokenTypes.RPAREN);
106         return TokenUtil.areOnSameLine(rparen, ast2);
107     }
108 
109     /**
110      * If this is the first chained method call which was moved to the next line.
111      *
112      * @return true if chained class are wrapped
113      */
114     private boolean isChainedMethodCallWrapped() {
115         final DetailAST main = getMainAst();
116         final DetailAST dot = main.getFirstChild();
117         final DetailAST target = dot.getFirstChild();
118 
119         final DetailAST dot1 = target.getFirstChild();
120         DetailAST target1 = dot1;
121         while (target1.getFirstChild() != null
122                 && target1.getType() != TokenTypes.METHOD_CALL) {
123             target1 = target1.getFirstChild();
124         }
125 
126         return dot1.getType() == TokenTypes.DOT
127                 && target1.getType() == TokenTypes.METHOD_CALL;
128     }
129 
130     /**
131      * Get the first AST of the specified method call.
132      *
133      * @param ast
134      *            the method call
135      *
136      * @return the first AST of the specified method call
137      */
138     private static DetailAST getFirstAst(DetailAST ast) {
139         // walk down the first child part of the dots that make up a method
140         // call name
141 
142         DetailAST astNode = ast.getFirstChild();
143         while (astNode.getType() == TokenTypes.DOT) {
144             astNode = astNode.getFirstChild();
145         }
146         return astNode;
147     }
148 
149     /**
150      * Returns method or constructor name. For {@code foo(arg)} it is `foo`, for
151      *     {@code foo.bar(arg)} it is `bar` for {@code super(arg)} it is 'super'.
152      *
153      * @return TokenTypes.IDENT node for a method call, TokenTypes.SUPER_CTOR_CALL otherwise.
154      */
155     private DetailAST getMethodIdentAst() {
156         DetailAST ast = getMainAst();
157         if (ast.getType() != TokenTypes.SUPER_CTOR_CALL) {
158             ast = ast.getFirstChild();
159             if (ast.getType() == TokenTypes.DOT) {
160                 ast = ast.getLastChild();
161             }
162         }
163         return ast;
164     }
165 
166     @Override
167     public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
168         // for whatever reason a method that crosses lines, like asList
169         // here:
170         //            System.out.println("methods are: " + Arrays.asList(
171         //                new String[] {"method"}).toString());
172         // will not have the right line num, so just get the child name
173 
174         final DetailAST ident = getMethodIdentAst();
175         final DetailAST rparen = getMainAst().findFirstToken(TokenTypes.RPAREN);
176         IndentLevel suggestedLevel = new IndentLevel(getLineStart(ident));
177         if (!TokenUtil.areOnSameLine(child.getMainAst().getFirstChild(), ident)
178                 && !isInvocationTarget(child)) {
179             suggestedLevel = new IndentLevel(suggestedLevel,
180                     getBasicOffset(),
181                     getContext().getLineWrappingIndentation());
182         }
183 
184         // If the right parenthesis is at the start of a line;
185         // include line wrapping in suggested indent level.
186         if (getLineStart(rparen) == rparen.getColumnNo()) {
187             suggestedLevel = IndentLevel.addAcceptable(suggestedLevel, new IndentLevel(
188                     getParent().getSuggestedChildIndent(this),
189                     getContext().getLineWrappingIndentation()
190             ));
191         }
192 
193         return suggestedLevel;
194     }
195 
196     /**
197      * Returns true if the given child handler represents the invocation target
198      * of this method call (the expression before the dot), rather than one of
199      * the call's arguments.
200      *
201      * @param child the child handler to classify
202      * @return true if the child is the invocation target, not an argument
203      */
204     private boolean isInvocationTarget(AbstractExpressionHandler child) {
205         boolean crossedElist = false;
206         DetailAST node = child.getMainAst().getParent();
207         while (node != null && node != getMainAst()) {
208             if (node.getType() == TokenTypes.ELIST) {
209                 crossedElist = true;
210                 break;
211             }
212             node = node.getParent();
213         }
214         return !crossedElist;
215     }
216 
217     @Override
218     public void checkIndentation() {
219         DetailAST lparen = null;
220         if (getMainAst().getType() == TokenTypes.METHOD_CALL) {
221             final DetailAST exprNode = getMainAst().getParent();
222             if (exprNode.getParent().getType() == TokenTypes.SLIST) {
223                 checkExpressionSubtree(getMainAst().getFirstChild(), getIndent(), false, false);
224                 lparen = getMainAst();
225             }
226         }
227         else {
228             // TokenTypes.CTOR_CALL|TokenTypes.SUPER_CTOR_CALL
229             lparen = getMainAst().getFirstChild();
230         }
231 
232         if (lparen != null) {
233             final DetailAST rparen = getMainAst().findFirstToken(TokenTypes.RPAREN);
234             checkLeftParen(lparen);
235 
236             if (!TokenUtil.areOnSameLine(rparen, lparen)) {
237                 checkExpressionSubtree(
238                     getMainAst().findFirstToken(TokenTypes.ELIST),
239                     new IndentLevel(getIndent(), getBasicOffset()),
240                     false, true);
241 
242                 checkRparenIndent(lparen, rparen);
243                 checkWrappingIndentation(getMainAst(), getCallLastNode(getMainAst()));
244             }
245         }
246         else if (isSimpleReturnMethodCall()) {
247             checkReturnStatementCallArguments();
248         }
249     }
250 
251     /**
252      * Checks the indentation of arguments of a method call that appears as the
253      * expression of a {@code return} statement. This complements the SLIST-parent
254      * flow above and covers cases where args are on continuation lines but were
255      * previously not validated because the enclosing statement is not a direct
256      * SLIST child. Arguments whose leftmost token is a self-checking construct
257      * (currently {@code new}) are skipped so that dedicated handler owns the
258      * diagnostic instead of shadowing it with a generic {@code method call}
259      * message.
260      */
261     private void checkReturnStatementCallArguments() {
262         final DetailAST rparen = getMainAst().findFirstToken(TokenTypes.RPAREN);
263         if (!TokenUtil.areOnSameLine(rparen, getMainAst())) {
264             final DetailAST elist = getMainAst().findFirstToken(TokenTypes.ELIST);
265             final IndentLevel argIndent = new IndentLevel(getIndent(), getBasicOffset());
266             for (DetailAST arg = elist.getFirstChild(); arg != null;
267                     arg = arg.getNextSibling()) {
268                 if (arg.getType() == TokenTypes.EXPR) {
269                     final DetailAST firstToken = getFirstAstNode(arg);
270                     // Let NewHandler own diagnostics for `new` expressions so
271                     // the message is attributed to the more specific construct.
272                     if (firstToken.getType() != TokenTypes.LITERAL_NEW
273                             && isOnStartOfLine(firstToken)) {
274                         final int actualColumn = expandedTabsColumnNo(firstToken);
275                         if (argIndent.isGreaterThan(actualColumn)) {
276                             logChildError(firstToken, actualColumn, argIndent);
277                         }
278                     }
279                 }
280             }
281         }
282     }
283 
284     /**
285      * Returns whether this method call is a simple (non-chained) call that is the
286      * expression of a {@code return} statement.
287      *
288      * @return {@code true} if this method call is directly returned.
289      */
290     private boolean isSimpleReturnMethodCall() {
291         final DetailAST exprNode = getMainAst().getParent();
292         return getMainAst().getFirstChild().getType() != TokenTypes.DOT
293                 && exprNode.getParent().getType() == TokenTypes.LITERAL_RETURN;
294     }
295 
296     /**
297      * Checks the indentation of the right parenthesis for method calls.
298      *
299      * @param lparen left parenthesis associated with rparen
300      * @param rparen parenthesis to check
301      */
302     private void checkRparenIndent(DetailAST lparen, DetailAST rparen) {
303         final int rparenLevel = expandedTabsColumnNo(rparen);
304         final int lparenLevel = expandedTabsColumnNo(lparen);
305 
306         final IndentLevel standardIndent = getIndent();
307 
308         // For chained method calls, also allow rparen at the line start position
309         IndentLevel enhancedIndent = standardIndent;
310         if (getParent() instanceof MethodCallHandler) {
311             final int lineStart = getLineStart(getFirstAst(getMainAst()));
312             if (lineStart != standardIndent.getFirstIndentLevel()) {
313                 enhancedIndent = IndentLevel.addAcceptable(standardIndent,
314                         new IndentLevel(lineStart));
315             }
316         }
317 
318         if (rparenLevel != lparenLevel + 1
319                 && !enhancedIndent.isAcceptable(rparenLevel)
320                 && isOnStartOfLine(rparen)) {
321             logError(rparen, "rparen", rparenLevel);
322         }
323     }
324 
325     @Override
326     protected boolean shouldIncreaseIndent() {
327         return false;
328     }
329 
330     /**
331      * Returns method or constructor call right paren.
332      *
333      * @param firstNode
334      *          call ast(TokenTypes.METHOD_CALL|TokenTypes.CTOR_CALL|TokenTypes.SUPER_CTOR_CALL)
335      * @return ast node containing right paren for specified method or constructor call. If
336      *     method calls are chained returns right paren for last call.
337      */
338     private static DetailAST getCallLastNode(DetailAST firstNode) {
339         return firstNode.getLastChild();
340     }
341 
342 }