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}