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 javax.annotation.Nullable; 023 024import com.puppycrawl.tools.checkstyle.api.DetailAST; 025import com.puppycrawl.tools.checkstyle.api.TokenTypes; 026import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 027 028/** 029 * Handler for lambda expressions. 030 * 031 */ 032public class LambdaHandler extends AbstractExpressionHandler { 033 034 /** 035 * Checks whether the lambda is correctly indented, this variable get its value from checking 036 * the lambda handler's indentation, and it is being used in aligning the lambda's children. 037 * A true value depicts lambda is correctly aligned without giving any errors. 038 * This is updated to false where there is any Indentation error log. 039 */ 040 private boolean isLambdaCorrectlyIndented = true; 041 042 /** 043 * Construct an instance of this handler with the given indentation context, 044 * abstract syntax tree, and parent handler. 045 * 046 * @param context the indentation check 047 * @param ast the abstract syntax tree 048 * @param parent the parent handler 049 */ 050 public LambdaHandler(IndentationContext context, 051 DetailAST ast, AbstractExpressionHandler parent) { 052 super(context, "lambda", ast, parent); 053 } 054 055 @Override 056 public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) { 057 IndentLevel childIndent = getIndent(); 058 if (isLambdaCorrectlyIndented) { 059 // If the lambda is correctly indented, include its line start as acceptable to 060 // avoid false positives. When "forceStrictCondition" is off, we allow indents 061 // larger than expected (e.g., 12 instead of 6 or 8). These larger indents are 062 // accepted but not recorded, so child indent suggestions may be inaccurate. 063 // Adding the actual line start ensures the tool recognizes the lambda’s real indent 064 // context. 065 childIndent = IndentLevel.addAcceptable(childIndent, getLineStart(getMainAst())); 066 067 if (isSameLineAsSwitch(child.getMainAst()) || child instanceof SlistHandler) { 068 // Lambda with block body (enclosed in {}) 069 childIndent = IndentLevel.addAcceptable(childIndent, 070 getLineStart(getMainAst().getFirstChild())); 071 } 072 else { 073 // Single-expression lambda (no {} block): 074 // assume line wrapping and add additional indentation 075 // for the statement in the next line. 076 childIndent = new IndentLevel(childIndent, 077 getContext().getLineWrappingIndentation()); 078 } 079 } 080 081 return childIndent; 082 } 083 084 /** 085 * {@inheritDoc}. 086 * 087 * @noinspection MethodWithMultipleReturnPoints 088 * @noinspectionreason MethodWithMultipleReturnPoints - indentation is complex and 089 * tightly coupled, thus making this method difficult to refactor 090 */ 091 @Override 092 protected IndentLevel getIndentImpl() { 093 if (getParent() instanceof MethodCallHandler) { 094 return getParent().getSuggestedChildIndent(this); 095 } 096 097 final IndentLevel result; 098 final DetailAST enumConstDef = findParentEnumConstantDef(); 099 if (enumConstDef != null) { 100 result = getEnumConstantBasedIndent(enumConstDef); 101 } 102 else { 103 DetailAST parent = getMainAst().getParent(); 104 if (getParent() instanceof NewHandler) { 105 parent = parent.getParent(); 106 } 107 108 // Use the start of the parent's line as the reference indentation level. 109 IndentLevel level = new IndentLevel(getLineStart(parent)); 110 111 // If the start of the lambda is the first element on the line; 112 // assume line wrapping with respect to its parent. 113 final DetailAST firstChild = getMainAst().getFirstChild(); 114 if (getLineStart(firstChild) == expandedTabsColumnNo(firstChild)) { 115 level = new IndentLevel(level, getContext().getLineWrappingIndentation()); 116 } 117 result = level; 118 } 119 120 return result; 121 } 122 123 @Override 124 public void checkIndentation() { 125 final DetailAST mainAst = getMainAst(); 126 final DetailAST firstChild = mainAst.getFirstChild(); 127 128 // If the "->" has no children, it is a switch 129 // rule lambda (i.e. 'case ONE -> 1;') 130 final boolean isSwitchRuleLambda = firstChild == null; 131 132 if (!isSwitchRuleLambda 133 && getLineStart(firstChild) == expandedTabsColumnNo(firstChild)) { 134 final int firstChildColumnNo = expandedTabsColumnNo(firstChild); 135 final IndentLevel level = getIndent(); 136 137 if (isNonAcceptableIndent(firstChildColumnNo, level)) { 138 isLambdaCorrectlyIndented = false; 139 logError(firstChild, "arguments", firstChildColumnNo, level); 140 } 141 } 142 143 // If the "->" is the first element on the line, assume line wrapping. 144 final int mainAstColumnNo = expandedTabsColumnNo(mainAst); 145 final boolean isLineWrappedLambda = mainAstColumnNo == getLineStart(mainAst); 146 if (isLineWrappedLambda) { 147 checkLineWrappedLambda(isSwitchRuleLambda, mainAstColumnNo); 148 } 149 150 final DetailAST nextSibling = mainAst.getNextSibling(); 151 152 if (isSwitchRuleLambda 153 && nextSibling.getType() == TokenTypes.EXPR 154 && !TokenUtil.areOnSameLine(mainAst, nextSibling)) { 155 // Likely a single-statement switch rule lambda without curly braces, e.g.: 156 // case ONE -> 157 // 1; 158 checkSingleStatementSwitchRuleIndentation(isLineWrappedLambda); 159 } 160 else if (!isSwitchRuleLambda) { 161 checkSingleExpressionBodyIndentation(); 162 } 163 } 164 165 /** 166 * Checks the indentation of a single-expression lambda body when the body is placed 167 * on a line separate from the {@code ->} operator. This ensures that continuation lines like: 168 * {@snippet lang="text" : 169 * return (String s) -> 170 * s.length(); 171 * } 172 * receive a wrapping-indent violation instead of being silently accepted. 173 */ 174 private void checkSingleExpressionBodyIndentation() { 175 final DetailAST mainAst = getMainAst(); 176 final DetailAST body = mainAst.getLastChild(); 177 final boolean isInReturn = 178 mainAst.getParent().getType() == TokenTypes.LITERAL_RETURN; 179 final boolean bodyIsOnNextLine = body.getType() != TokenTypes.SLIST 180 && !TokenUtil.areOnSameLine(mainAst, body); 181 if (isInReturn && bodyIsOnNextLine) { 182 final DetailAST firstToken = getFirstAstNode(body); 183 final boolean firstTokenHasOwnHandler = getContext() 184 .getHandlerFactory().isHandledType(firstToken.getType()); 185 if (!firstTokenHasOwnHandler) { 186 final int bodyColumnNo = expandedTabsColumnNo(firstToken); 187 final IndentLevel bodyIndent = new IndentLevel(getIndent(), 188 getContext().getLineWrappingIndentation()); 189 if (isNonAcceptableIndent(bodyColumnNo, bodyIndent)) { 190 logError(firstToken, "", bodyColumnNo, bodyIndent); 191 } 192 } 193 } 194 } 195 196 /** 197 * Checks that given indent is acceptable or not. 198 * 199 * @param astColumnNo indent value to check 200 * @param level indent level 201 * @return true if indent is not acceptable 202 */ 203 private boolean isNonAcceptableIndent(int astColumnNo, IndentLevel level) { 204 return astColumnNo < level.getFirstIndentLevel() 205 || getContext().isForceStrictCondition() 206 && !level.isAcceptable(astColumnNo); 207 } 208 209 /** 210 * This method checks a line wrapped lambda, whether it is a lambda 211 * expression or switch rule lambda. 212 * 213 * @param isSwitchRuleLambda if mainAst is a switch rule lambda 214 * @param mainAstColumnNo the column number of the lambda we are checking 215 */ 216 private void checkLineWrappedLambda(final boolean isSwitchRuleLambda, 217 final int mainAstColumnNo) { 218 final IndentLevel level; 219 final DetailAST mainAst = getMainAst(); 220 221 if (isSwitchRuleLambda) { 222 // We check the indentation of the case literal or default literal 223 // on the previous line and use that to determine the correct 224 // indentation for the line wrapped "->" 225 final DetailAST previousSibling = mainAst.getPreviousSibling(); 226 final int previousLineStart = getLineStart(previousSibling); 227 228 level = new IndentLevel(new IndentLevel(previousLineStart), 229 getContext().getLineWrappingIndentation()); 230 } 231 else { 232 level = new IndentLevel(getIndent(), 233 getContext().getLineWrappingIndentation()); 234 } 235 236 if (isNonAcceptableIndent(mainAstColumnNo, level)) { 237 isLambdaCorrectlyIndented = false; 238 logError(mainAst, "", mainAstColumnNo, level); 239 } 240 } 241 242 /** 243 * Checks the indentation of statements inside a single-statement switch rule 244 * when the statement is not on the same line as the lambda operator ({@code ->}). 245 * This applies to single-statement switch rules without curly braces {@code {}}. 246 * Example: 247 * {@snippet lang="text" : 248 * case ONE -> 249 * 1; 250 * } 251 * 252 * @param isLambdaFirstInLine if {@code ->} is the first element on the line 253 */ 254 private void checkSingleStatementSwitchRuleIndentation(boolean isLambdaFirstInLine) { 255 final DetailAST mainAst = getMainAst(); 256 IndentLevel level = getParent().getSuggestedChildIndent(this); 257 258 if (isLambdaFirstInLine) { 259 // If the lambda operator (`->`) is at the start of the line, assume line wrapping 260 // and add additional indentation for the statement in the next line. 261 level = new IndentLevel(level, getContext().getLineWrappingIndentation()); 262 } 263 264 // The first line should not match if the switch rule statement starts on the same line 265 // as "->" but continues onto the next lines as part of a single logical expression. 266 final DetailAST nextSibling = mainAst.getNextSibling(); 267 final boolean firstLineMatches = getFirstLine(nextSibling) != mainAst.getLineNo(); 268 checkExpressionSubtree(nextSibling, level, firstLineMatches, false); 269 } 270 271 /** 272 * Checks if the current LAMBDA node is placed on the same line 273 * as the given SWITCH_LITERAL node. 274 * 275 * @param node the SWITCH_LITERAL node to compare with 276 * @return true if the current LAMBDA node is on the same line as the given SWITCH_LITERAL node 277 */ 278 private boolean isSameLineAsSwitch(DetailAST node) { 279 return node.getType() == TokenTypes.LITERAL_SWITCH 280 && TokenUtil.areOnSameLine(getMainAst(), node); 281 } 282 283 /** 284 * Finds the parent ENUM_CONSTANT_DEF node if this lambda is an argument of an enum constant. 285 * 286 * @return the ENUM_CONSTANT_DEF node if found, null otherwise 287 */ 288 @Nullable 289 private DetailAST findParentEnumConstantDef() { 290 DetailAST result = null; 291 DetailAST parent = getMainAst().getParent(); 292 while (parent != null) { 293 if (parent.getType() == TokenTypes.ENUM_CONSTANT_DEF) { 294 result = parent; 295 break; 296 } 297 parent = parent.getParent(); 298 } 299 return result; 300 } 301 302 /** 303 * Calculates the expected indentation for a lambda inside enum constant arguments. 304 * The expected indent is the enum constant's indent plus line wrapping indentation. 305 * 306 * @param enumConstDef the ENUM_CONSTANT_DEF node 307 * @return the expected indentation level 308 */ 309 private IndentLevel getEnumConstantBasedIndent(DetailAST enumConstDef) { 310 final int enumConstIndent = getLineStart(enumConstDef); 311 final IndentLevel baseLevel = new IndentLevel(enumConstIndent); 312 return new IndentLevel(baseLevel, getContext().getLineWrappingIndentation()); 313 } 314 315}