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
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}