001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.indentation; 021 022import com.puppycrawl.tools.checkstyle.api.DetailAST; 023import com.puppycrawl.tools.checkstyle.api.TokenTypes; 024import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 025 026/** 027 * Handler for method calls. 028 * 029 */ 030public class MethodCallHandler extends AbstractExpressionHandler { 031 032 /** 033 * The indentation context used by this class. 034 */ 035 private final IndentationContext context; 036 037 /** 038 * Construct an instance of this handler with the given indentation context, 039 * abstract syntax tree, and parent handler. 040 * 041 * @param context the indentation check 042 * @param ast the abstract syntax tree 043 * @param parent the parent handler 044 */ 045 public MethodCallHandler(IndentationContext context, 046 DetailAST ast, AbstractExpressionHandler parent) { 047 super(context, "method call", ast, parent); 048 this.context = context; 049 } 050 051 @Override 052 protected IndentLevel getIndentImpl() { 053 final IndentLevel indentLevel; 054 // if inside a method call's params, this could be part of 055 // an expression, so get the previous line's start 056 if (getParent() instanceof MethodCallHandler container) { 057 if (TokenUtil.areOnSameLine(container.getMainAst(), getMainAst()) 058 || isChainedMethodCallWrapped() 059 || areMethodsChained(container.getMainAst(), getMainAst())) { 060 indentLevel = container.getIndent(); 061 } 062 // we should increase indentation only if this is the first 063 // chained method call which was moved to the next line 064 else { 065 indentLevel = new IndentLevel(container.getIndent(), 066 getContext().getLineWrappingIndentation()); 067 } 068 } 069 else if (getMainAst().getFirstChild().getType() == TokenTypes.LITERAL_NEW) { 070 indentLevel = super.getIndentImpl(); 071 } 072 else { 073 // if our expression isn't first on the line, just use the start 074 // of the line 075 final DetailAstSet astSet = new DetailAstSet(context); 076 findSubtreeAst(astSet, getMainAst().getFirstChild(), true); 077 final int firstCol = expandedTabsColumnNo(astSet.firstLine()); 078 final int lineStart = getLineStart(getFirstAst(getMainAst())); 079 if (lineStart == firstCol) { 080 indentLevel = super.getIndentImpl(); 081 } 082 else { 083 indentLevel = new IndentLevel(lineStart); 084 } 085 } 086 return indentLevel; 087 } 088 089 /** 090 * Checks if ast2 is a chained method call that starts on the same level as ast1 ends. 091 * In other words, if the right paren of ast1 is on the same level as the lparen of ast2: 092 * {@code 093 * value.methodOne( 094 * argument1 095 * ).methodTwo( 096 * argument2 097 * ); 098 * } 099 * 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}