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 final 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 the call's arguments. 199 * 200 * @param child the child handler to classify 201 * @return true if the child is the invocation target, not an argument 202 */ 203 private boolean isInvocationTarget(AbstractExpressionHandler child) { 204 boolean crossedElist = false; 205 DetailAST node = child.getMainAst().getParent(); 206 while (node != null && node != getMainAst()) { 207 if (node.getType() == TokenTypes.ELIST) { 208 crossedElist = true; 209 break; 210 } 211 node = node.getParent(); 212 } 213 return !crossedElist; 214 } 215 216 @Override 217 public void checkIndentation() { 218 DetailAST lparen = null; 219 if (getMainAst().getType() == TokenTypes.METHOD_CALL) { 220 final DetailAST exprNode = getMainAst().getParent(); 221 if (exprNode.getParent().getType() == TokenTypes.SLIST) { 222 checkExpressionSubtree(getMainAst().getFirstChild(), getIndent(), false, false); 223 lparen = getMainAst(); 224 } 225 } 226 else { 227 // TokenTypes.CTOR_CALL|TokenTypes.SUPER_CTOR_CALL 228 lparen = getMainAst().getFirstChild(); 229 } 230 231 if (lparen != null) { 232 final DetailAST rparen = getMainAst().findFirstToken(TokenTypes.RPAREN); 233 checkLeftParen(lparen); 234 235 if (!TokenUtil.areOnSameLine(rparen, lparen)) { 236 checkExpressionSubtree( 237 getMainAst().findFirstToken(TokenTypes.ELIST), 238 new IndentLevel(getIndent(), getBasicOffset()), 239 false, true); 240 241 checkRparenIndent(lparen, rparen); 242 checkWrappingIndentation(getMainAst(), getCallLastNode(getMainAst())); 243 } 244 } 245 else if (isSimpleReturnMethodCall()) { 246 checkReturnStatementCallArguments(); 247 } 248 } 249 250 /** 251 * Checks the indentation of arguments of a method call that appears as the 252 * expression of a {@code return} statement. This complements the SLIST-parent 253 * flow above and covers cases where args are on continuation lines but were 254 * previously not validated because the enclosing statement is not a direct 255 * SLIST child. Arguments whose leftmost token is a self-checking construct 256 * (currently {@code new}) are skipped so that dedicated handler owns the 257 * diagnostic instead of shadowing it with a generic {@code method call} message. 258 */ 259 private void checkReturnStatementCallArguments() { 260 final DetailAST rparen = getMainAst().findFirstToken(TokenTypes.RPAREN); 261 if (!TokenUtil.areOnSameLine(rparen, getMainAst())) { 262 final DetailAST elist = getMainAst().findFirstToken(TokenTypes.ELIST); 263 final IndentLevel argIndent = new IndentLevel(getIndent(), getBasicOffset()); 264 for (DetailAST arg = elist.getFirstChild(); arg != null; 265 arg = arg.getNextSibling()) { 266 if (arg.getType() == TokenTypes.EXPR) { 267 final DetailAST firstToken = getFirstAstNode(arg); 268 // Let NewHandler own diagnostics for `new` expressions so 269 // the message is attributed to the more specific construct. 270 if (firstToken.getType() != TokenTypes.LITERAL_NEW 271 && isOnStartOfLine(firstToken)) { 272 final int actualColumn = expandedTabsColumnNo(firstToken); 273 if (argIndent.isGreaterThan(actualColumn)) { 274 logChildError(firstToken, actualColumn, argIndent); 275 } 276 } 277 } 278 } 279 } 280 } 281 282 /** 283 * Returns whether this method call is a simple (non-chained) call that is the 284 * expression of a {@code return} statement. 285 * 286 * @return {@code true} if this method call is directly returned. 287 */ 288 private boolean isSimpleReturnMethodCall() { 289 final DetailAST exprNode = getMainAst().getParent(); 290 return getMainAst().getFirstChild().getType() != TokenTypes.DOT 291 && exprNode.getParent().getType() == TokenTypes.LITERAL_RETURN; 292 } 293 294 /** 295 * Checks the indentation of the right parenthesis for method calls. 296 * 297 * @param lparen left parenthesis associated with rparen 298 * @param rparen parenthesis to check 299 */ 300 private void checkRparenIndent(DetailAST lparen, DetailAST rparen) { 301 final int rparenLevel = expandedTabsColumnNo(rparen); 302 final int lparenLevel = expandedTabsColumnNo(lparen); 303 304 final IndentLevel standardIndent = getIndent(); 305 306 // For chained method calls, also allow rparen at the line start position 307 IndentLevel enhancedIndent = standardIndent; 308 if (getParent() instanceof MethodCallHandler) { 309 final int lineStart = getLineStart(getFirstAst(getMainAst())); 310 if (lineStart != standardIndent.getFirstIndentLevel()) { 311 enhancedIndent = IndentLevel.addAcceptable(standardIndent, 312 new IndentLevel(lineStart)); 313 } 314 } 315 316 if (rparenLevel != lparenLevel + 1 317 && !enhancedIndent.isAcceptable(rparenLevel) 318 && isOnStartOfLine(rparen)) { 319 logError(rparen, "rparen", rparenLevel); 320 } 321 } 322 323 @Override 324 protected boolean shouldIncreaseIndent() { 325 return false; 326 } 327 328 /** 329 * Returns method or constructor call right paren. 330 * 331 * @param firstNode 332 * call ast(TokenTypes.METHOD_CALL|TokenTypes.CTOR_CALL|TokenTypes.SUPER_CTOR_CALL) 333 * @return ast node containing right paren for specified method or constructor call. If 334 * method calls are chained returns right paren for last call. 335 */ 336 private static DetailAST getCallLastNode(DetailAST firstNode) { 337 return firstNode.getLastChild(); 338 } 339 340}