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 check,
044     * abstract syntax tree, and parent handler.
045     *
046     * @param indentCheck the indentation check
047     * @param ast the abstract syntax tree
048     * @param parent the parent handler
049     */
050    public LambdaHandler(IndentationCheck indentCheck,
051                         DetailAST ast, AbstractExpressionHandler parent) {
052        super(indentCheck, "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                        getIndentCheck().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, getIndentCheck().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    }
161
162    /**
163     * Checks that given indent is acceptable or not.
164     *
165     * @param astColumnNo indent value to check
166     * @param level indent level
167     * @return true if indent is not acceptable
168     */
169    private boolean isNonAcceptableIndent(int astColumnNo, IndentLevel level) {
170        return astColumnNo < level.getFirstIndentLevel()
171            || getIndentCheck().isForceStrictCondition()
172               && !level.isAcceptable(astColumnNo);
173    }
174
175    /**
176     * This method checks a line wrapped lambda, whether it is a lambda
177     * expression or switch rule lambda.
178     *
179     * @param isSwitchRuleLambda if mainAst is a switch rule lambda
180     * @param mainAstColumnNo the column number of the lambda we are checking
181     */
182    private void checkLineWrappedLambda(final boolean isSwitchRuleLambda,
183                                        final int mainAstColumnNo) {
184        final IndentLevel level;
185        final DetailAST mainAst = getMainAst();
186
187        if (isSwitchRuleLambda) {
188            // We check the indentation of the case literal or default literal
189            // on the previous line and use that to determine the correct
190            // indentation for the line wrapped "->"
191            final DetailAST previousSibling = mainAst.getPreviousSibling();
192            final int previousLineStart = getLineStart(previousSibling);
193
194            level = new IndentLevel(new IndentLevel(previousLineStart),
195                    getIndentCheck().getLineWrappingIndentation());
196        }
197        else {
198            level = new IndentLevel(getIndent(),
199                getIndentCheck().getLineWrappingIndentation());
200        }
201
202        if (isNonAcceptableIndent(mainAstColumnNo, level)) {
203            isLambdaCorrectlyIndented = false;
204            logError(mainAst, "", mainAstColumnNo, level);
205        }
206    }
207
208    /**
209     * Checks the indentation of statements inside a single-statement switch rule
210     * when the statement is not on the same line as the lambda operator ({@code ->}).
211     * This applies to single-statement switch rules without curly braces {@code {}}.
212     * Example:
213     * <pre>
214     * case ONE {@code ->}
215     *     1;
216     * </pre>
217     *
218     * @param isLambdaFirstInLine if {@code ->} is the first element on the line
219     */
220    private void checkSingleStatementSwitchRuleIndentation(boolean isLambdaFirstInLine) {
221        final DetailAST mainAst = getMainAst();
222        IndentLevel level = getParent().getSuggestedChildIndent(this);
223
224        if (isLambdaFirstInLine) {
225            // If the lambda operator (`->`) is at the start of the line, assume line wrapping
226            // and add additional indentation for the statement in the next line.
227            level = new IndentLevel(level, getIndentCheck().getLineWrappingIndentation());
228        }
229
230        // The first line should not match if the switch rule statement starts on the same line
231        // as "->" but continues onto the next lines as part of a single logical expression.
232        final DetailAST nextSibling = mainAst.getNextSibling();
233        final boolean firstLineMatches = getFirstLine(nextSibling) != mainAst.getLineNo();
234        checkExpressionSubtree(nextSibling, level, firstLineMatches, false);
235    }
236
237    /**
238     * Checks if the current LAMBDA node is placed on the same line
239     * as the given SWITCH_LITERAL node.
240     *
241     * @param node the SWITCH_LITERAL node to compare with
242     * @return true if the current LAMBDA node is on the same line
243     *     as the given SWITCH_LITERAL node
244     */
245    private boolean isSameLineAsSwitch(DetailAST node) {
246        return node.getType() == TokenTypes.LITERAL_SWITCH
247            && TokenUtil.areOnSameLine(getMainAst(), node);
248    }
249
250    /**
251     * Finds the parent ENUM_CONSTANT_DEF node if this lambda is an argument of an enum constant.
252     *
253     * @return the ENUM_CONSTANT_DEF node if found, null otherwise
254     */
255    @Nullable
256    private DetailAST findParentEnumConstantDef() {
257        DetailAST result = null;
258        DetailAST parent = getMainAst().getParent();
259        while (parent != null) {
260            if (parent.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
261                result = parent;
262                break;
263            }
264            parent = parent.getParent();
265        }
266        return result;
267    }
268
269    /**
270     * Calculates the expected indentation for a lambda inside enum constant arguments.
271     * The expected indent is the enum constant's indent plus line wrapping indentation.
272     *
273     * @param enumConstDef the ENUM_CONSTANT_DEF node
274     * @return the expected indentation level
275     */
276    private IndentLevel getEnumConstantBasedIndent(DetailAST enumConstDef) {
277        final int enumConstIndent = getLineStart(enumConstDef);
278        final IndentLevel baseLevel = new IndentLevel(enumConstIndent);
279        return new IndentLevel(baseLevel, getIndentCheck().getLineWrappingIndentation());
280    }
281
282}