1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 lambda expressions.
28 *
29 */
30 public class LambdaHandler extends AbstractExpressionHandler {
31 /**
32 * Checks whether the lambda is correctly indented, this variable get its value from checking
33 * the lambda handler's indentation, and it is being used in aligning the lambda's children.
34 * A true value depicts lambda is correctly aligned without giving any errors.
35 * This is updated to false where there is any Indentation error log.
36 */
37 private boolean isLambdaCorrectlyIndented = true;
38
39 /**
40 * Construct an instance of this handler with the given indentation check,
41 * abstract syntax tree, and parent handler.
42 *
43 * @param indentCheck the indentation check
44 * @param ast the abstract syntax tree
45 * @param parent the parent handler
46 */
47 public LambdaHandler(IndentationCheck indentCheck,
48 DetailAST ast, AbstractExpressionHandler parent) {
49 super(indentCheck, "lambda", ast, parent);
50 }
51
52 @Override
53 public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
54 IndentLevel childIndent = getIndent();
55 if (isLambdaCorrectlyIndented) {
56 // If the lambda is correctly indented, include its line start as acceptable to
57 // avoid false positives. When "forceStrictCondition" is off, we allow indents
58 // larger than expected (e.g., 12 instead of 6 or 8). These larger indents are
59 // accepted but not recorded, so child indent suggestions may be inaccurate.
60 // Adding the actual line start ensures the tool recognizes the lambda’s real indent
61 // context.
62 childIndent = IndentLevel.addAcceptable(childIndent, getLineStart(getMainAst()));
63
64 if (isSameLineAsSwitch(child.getMainAst()) || child instanceof SlistHandler) {
65 // Lambda with block body (enclosed in {})
66 childIndent = IndentLevel.addAcceptable(childIndent,
67 getLineStart(getMainAst().getFirstChild()));
68 }
69 else {
70 // Single-expression lambda (no {} block):
71 // assume line wrapping and add additional indentation
72 // for the statement in the next line.
73 childIndent = new IndentLevel(childIndent,
74 getIndentCheck().getLineWrappingIndentation());
75 }
76 }
77
78 return childIndent;
79 }
80
81 /**
82 * {@inheritDoc}.
83 *
84 * @noinspection MethodWithMultipleReturnPoints
85 * @noinspectionreason MethodWithMultipleReturnPoints - indentation is complex and
86 * tightly coupled, thus making this method difficult to refactor
87 */
88 @Override
89 protected IndentLevel getIndentImpl() {
90 if (getParent() instanceof MethodCallHandler) {
91 return getParent().getSuggestedChildIndent(this);
92 }
93
94 DetailAST parent = getMainAst().getParent();
95 if (getParent() instanceof NewHandler) {
96 parent = parent.getParent();
97 }
98
99 // Use the start of the parent's line as the reference indentation level.
100 IndentLevel level = new IndentLevel(getLineStart(parent));
101
102 // If the start of the lambda is the first element on the line;
103 // assume line wrapping with respect to its parent.
104 final DetailAST firstChild = getMainAst().getFirstChild();
105 if (getLineStart(firstChild) == expandedTabsColumnNo(firstChild)) {
106 level = new IndentLevel(level, getIndentCheck().getLineWrappingIndentation());
107 }
108
109 return level;
110 }
111
112 @Override
113 public void checkIndentation() {
114 final DetailAST mainAst = getMainAst();
115 final DetailAST firstChild = mainAst.getFirstChild();
116
117 // If the "->" has no children, it is a switch
118 // rule lambda (i.e. 'case ONE -> 1;')
119 final boolean isSwitchRuleLambda = firstChild == null;
120
121 if (!isSwitchRuleLambda
122 && getLineStart(firstChild) == expandedTabsColumnNo(firstChild)) {
123 final int firstChildColumnNo = expandedTabsColumnNo(firstChild);
124 final IndentLevel level = getIndent();
125
126 if (isNonAcceptableIndent(firstChildColumnNo, level)) {
127 isLambdaCorrectlyIndented = false;
128 logError(firstChild, "arguments", firstChildColumnNo, level);
129 }
130 }
131
132 // If the "->" is the first element on the line, assume line wrapping.
133 final int mainAstColumnNo = expandedTabsColumnNo(mainAst);
134 final boolean isLineWrappedLambda = mainAstColumnNo == getLineStart(mainAst);
135 if (isLineWrappedLambda) {
136 checkLineWrappedLambda(isSwitchRuleLambda, mainAstColumnNo);
137 }
138
139 final DetailAST nextSibling = mainAst.getNextSibling();
140
141 if (isSwitchRuleLambda
142 && nextSibling.getType() == TokenTypes.EXPR
143 && !TokenUtil.areOnSameLine(mainAst, nextSibling)) {
144 // Likely a single-statement switch rule lambda without curly braces, e.g.:
145 // case ONE ->
146 // 1;
147 checkSingleStatementSwitchRuleIndentation(isLineWrappedLambda);
148 }
149 }
150
151 /**
152 * Checks that given indent is acceptable or not.
153 *
154 * @param astColumnNo indent value to check
155 * @param level indent level
156 * @return true if indent is not acceptable
157 */
158 private boolean isNonAcceptableIndent(int astColumnNo, IndentLevel level) {
159 return astColumnNo < level.getFirstIndentLevel()
160 || getIndentCheck().isForceStrictCondition()
161 && !level.isAcceptable(astColumnNo);
162 }
163
164 /**
165 * This method checks a line wrapped lambda, whether it is a lambda
166 * expression or switch rule lambda.
167 *
168 * @param isSwitchRuleLambda if mainAst is a switch rule lambda
169 * @param mainAstColumnNo the column number of the lambda we are checking
170 */
171 private void checkLineWrappedLambda(final boolean isSwitchRuleLambda,
172 final int mainAstColumnNo) {
173 final IndentLevel level;
174 final DetailAST mainAst = getMainAst();
175
176 if (isSwitchRuleLambda) {
177 // We check the indentation of the case literal or default literal
178 // on the previous line and use that to determine the correct
179 // indentation for the line wrapped "->"
180 final DetailAST previousSibling = mainAst.getPreviousSibling();
181 final int previousLineStart = getLineStart(previousSibling);
182
183 level = new IndentLevel(new IndentLevel(previousLineStart),
184 getIndentCheck().getLineWrappingIndentation());
185 }
186 else {
187 level = new IndentLevel(getIndent(),
188 getIndentCheck().getLineWrappingIndentation());
189 }
190
191 if (isNonAcceptableIndent(mainAstColumnNo, level)) {
192 isLambdaCorrectlyIndented = false;
193 logError(mainAst, "", mainAstColumnNo, level);
194 }
195 }
196
197 /**
198 * Checks the indentation of statements inside a single-statement switch rule
199 * when the statement is not on the same line as the lambda operator ({@code ->}).
200 * This applies to single-statement switch rules without curly braces {@code {}}.
201 * Example:
202 * <pre>
203 * case ONE {@code ->}
204 * 1;
205 * </pre>
206 *
207 * @param isLambdaFirstInLine if {@code ->} is the first element on the line
208 */
209 private void checkSingleStatementSwitchRuleIndentation(boolean isLambdaFirstInLine) {
210 final DetailAST mainAst = getMainAst();
211 IndentLevel level = getParent().getSuggestedChildIndent(this);
212
213 if (isLambdaFirstInLine) {
214 // If the lambda operator (`->`) is at the start of the line, assume line wrapping
215 // and add additional indentation for the statement in the next line.
216 level = new IndentLevel(level, getIndentCheck().getLineWrappingIndentation());
217 }
218
219 // The first line should not match if the switch rule statement starts on the same line
220 // as "->" but continues onto the next lines as part of a single logical expression.
221 final DetailAST nextSibling = mainAst.getNextSibling();
222 final boolean firstLineMatches = getFirstLine(nextSibling) != mainAst.getLineNo();
223 checkExpressionSubtree(nextSibling, level, firstLineMatches, false);
224 }
225
226 /**
227 * Checks if the current LAMBDA node is placed on the same line
228 * as the given SWITCH_LITERAL node.
229 *
230 * @param node the SWITCH_LITERAL node to compare with
231 * @return true if the current LAMBDA node is on the same line
232 * as the given SWITCH_LITERAL node
233 */
234 private boolean isSameLineAsSwitch(DetailAST node) {
235 return node.getType() == TokenTypes.LITERAL_SWITCH
236 && TokenUtil.areOnSameLine(getMainAst(), node);
237 }
238 }