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 javax.annotation.Nullable;
23
24 import com.puppycrawl.tools.checkstyle.api.DetailAST;
25 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
26 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
27
28 /**
29 * Handler for lambda expressions.
30 *
31 */
32 public class LambdaHandler extends AbstractExpressionHandler {
33
34 /**
35 * Checks whether the lambda is correctly indented, this variable get its value from checking
36 * the lambda handler's indentation, and it is being used in aligning the lambda's children.
37 * A true value depicts lambda is correctly aligned without giving any errors.
38 * This is updated to false where there is any Indentation error log.
39 */
40 private boolean isLambdaCorrectlyIndented = true;
41
42 /**
43 * Construct an instance of this handler with the given indentation context,
44 * abstract syntax tree, and parent handler.
45 *
46 * @param context the indentation check
47 * @param ast the abstract syntax tree
48 * @param parent the parent handler
49 */
50 public LambdaHandler(IndentationContext context,
51 DetailAST ast, AbstractExpressionHandler parent) {
52 super(context, "lambda", ast, parent);
53 }
54
55 @Override
56 public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
57 IndentLevel childIndent = getIndent();
58 if (isLambdaCorrectlyIndented) {
59 // If the lambda is correctly indented, include its line start as acceptable to
60 // avoid false positives. When "forceStrictCondition" is off, we allow indents
61 // larger than expected (e.g., 12 instead of 6 or 8). These larger indents are
62 // accepted but not recorded, so child indent suggestions may be inaccurate.
63 // Adding the actual line start ensures the tool recognizes the lambda’s real indent
64 // context.
65 childIndent = IndentLevel.addAcceptable(childIndent, getLineStart(getMainAst()));
66
67 if (isSameLineAsSwitch(child.getMainAst()) || child instanceof SlistHandler) {
68 // Lambda with block body (enclosed in {})
69 childIndent = IndentLevel.addAcceptable(childIndent,
70 getLineStart(getMainAst().getFirstChild()));
71 }
72 else {
73 // Single-expression lambda (no {} block):
74 // assume line wrapping and add additional indentation
75 // for the statement in the next line.
76 childIndent = new IndentLevel(childIndent,
77 getContext().getLineWrappingIndentation());
78 }
79 }
80
81 return childIndent;
82 }
83
84 /**
85 * {@inheritDoc}.
86 *
87 * @noinspection MethodWithMultipleReturnPoints
88 * @noinspectionreason MethodWithMultipleReturnPoints - indentation is complex and
89 * tightly coupled, thus making this method difficult to refactor
90 */
91 @Override
92 protected IndentLevel getIndentImpl() {
93 if (getParent() instanceof MethodCallHandler) {
94 return getParent().getSuggestedChildIndent(this);
95 }
96
97 final IndentLevel result;
98 final DetailAST enumConstDef = findParentEnumConstantDef();
99 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
168 * lines like:
169 * {@snippet lang="text" :
170 * return (String s) ->
171 * s.length();
172 * }
173 * receive a wrapping-indent violation instead of being silently accepted.
174 */
175 private void checkSingleExpressionBodyIndentation() {
176 final DetailAST mainAst = getMainAst();
177 final DetailAST body = mainAst.getLastChild();
178 final boolean isInReturn =
179 mainAst.getParent().getType() == TokenTypes.LITERAL_RETURN;
180 final boolean bodyIsOnNextLine = body.getType() != TokenTypes.SLIST
181 && !TokenUtil.areOnSameLine(mainAst, body);
182 if (isInReturn && bodyIsOnNextLine) {
183 final DetailAST firstToken = getFirstAstNode(body);
184 final boolean firstTokenHasOwnHandler = getContext()
185 .getHandlerFactory().isHandledType(firstToken.getType());
186 if (!firstTokenHasOwnHandler) {
187 final int bodyColumnNo = expandedTabsColumnNo(firstToken);
188 final IndentLevel bodyIndent = new IndentLevel(getIndent(),
189 getContext().getLineWrappingIndentation());
190 if (isNonAcceptableIndent(bodyColumnNo, bodyIndent)) {
191 logError(firstToken, "", bodyColumnNo, bodyIndent);
192 }
193 }
194 }
195 }
196
197 /**
198 * Checks that given indent is acceptable or not.
199 *
200 * @param astColumnNo indent value to check
201 * @param level indent level
202 * @return true if indent is not acceptable
203 */
204 private boolean isNonAcceptableIndent(int astColumnNo, IndentLevel level) {
205 return astColumnNo < level.getFirstIndentLevel()
206 || getContext().isForceStrictCondition()
207 && !level.isAcceptable(astColumnNo);
208 }
209
210 /**
211 * This method checks a line wrapped lambda, whether it is a lambda
212 * expression or switch rule lambda.
213 *
214 * @param isSwitchRuleLambda if mainAst is a switch rule lambda
215 * @param mainAstColumnNo the column number of the lambda we are checking
216 */
217 private void checkLineWrappedLambda(final boolean isSwitchRuleLambda,
218 final int mainAstColumnNo) {
219 final IndentLevel level;
220 final DetailAST mainAst = getMainAst();
221
222 if (isSwitchRuleLambda) {
223 // We check the indentation of the case literal or default literal
224 // on the previous line and use that to determine the correct
225 // indentation for the line wrapped "->"
226 final DetailAST previousSibling = mainAst.getPreviousSibling();
227 final int previousLineStart = getLineStart(previousSibling);
228
229 level = new IndentLevel(new IndentLevel(previousLineStart),
230 getContext().getLineWrappingIndentation());
231 }
232 else {
233 level = new IndentLevel(getIndent(),
234 getContext().getLineWrappingIndentation());
235 }
236
237 if (isNonAcceptableIndent(mainAstColumnNo, level)) {
238 isLambdaCorrectlyIndented = false;
239 logError(mainAst, "", mainAstColumnNo, level);
240 }
241 }
242
243 /**
244 * Checks the indentation of statements inside a single-statement switch rule
245 * when the statement is not on the same line as the lambda operator ({@code ->}).
246 * This applies to single-statement switch rules without curly braces {@code {}}.
247 * Example:
248 * {@snippet lang="text" :
249 * case ONE ->
250 * 1;
251 * }
252 *
253 * @param isLambdaFirstInLine if {@code ->} is the first element on the line
254 */
255 private void checkSingleStatementSwitchRuleIndentation(boolean isLambdaFirstInLine) {
256 final DetailAST mainAst = getMainAst();
257 IndentLevel level = getParent().getSuggestedChildIndent(this);
258
259 if (isLambdaFirstInLine) {
260 // If the lambda operator (`->`) is at the start of the line, assume line wrapping
261 // and add additional indentation for the statement in the next line.
262 level = new IndentLevel(level, getContext().getLineWrappingIndentation());
263 }
264
265 // The first line should not match if the switch rule statement starts on the same line
266 // as "->" but continues onto the next lines as part of a single logical expression.
267 final DetailAST nextSibling = mainAst.getNextSibling();
268 final boolean firstLineMatches = getFirstLine(nextSibling) != mainAst.getLineNo();
269 checkExpressionSubtree(nextSibling, level, firstLineMatches, false);
270 }
271
272 /**
273 * Checks if the current LAMBDA node is placed on the same line
274 * as the given SWITCH_LITERAL node.
275 *
276 * @param node the SWITCH_LITERAL node to compare with
277 * @return true if the current LAMBDA node is on the same line
278 * as the given SWITCH_LITERAL node
279 */
280 private boolean isSameLineAsSwitch(DetailAST node) {
281 return node.getType() == TokenTypes.LITERAL_SWITCH
282 && TokenUtil.areOnSameLine(getMainAst(), node);
283 }
284
285 /**
286 * Finds the parent ENUM_CONSTANT_DEF node if this lambda is an argument of an enum constant.
287 *
288 * @return the ENUM_CONSTANT_DEF node if found, null otherwise
289 */
290 @Nullable
291 private DetailAST findParentEnumConstantDef() {
292 DetailAST result = null;
293 DetailAST parent = getMainAst().getParent();
294 while (parent != null) {
295 if (parent.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
296 result = parent;
297 break;
298 }
299 parent = parent.getParent();
300 }
301 return result;
302 }
303
304 /**
305 * Calculates the expected indentation for a lambda inside enum constant arguments.
306 * The expected indent is the enum constant's indent plus line wrapping indentation.
307 *
308 * @param enumConstDef the ENUM_CONSTANT_DEF node
309 * @return the expected indentation level
310 */
311 private IndentLevel getEnumConstantBasedIndent(DetailAST enumConstDef) {
312 final int enumConstIndent = getLineStart(enumConstDef);
313 final IndentLevel baseLevel = new IndentLevel(enumConstIndent);
314 return new IndentLevel(baseLevel, getContext().getLineWrappingIndentation());
315 }
316
317 }