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