View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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 javax.annotation.Nullable;
23  
24  import com.puppycrawl.tools.checkstyle.api.DetailAST;
25  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
26  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
27  
28  /**
29   * Handler for lambda expressions.
30   *
31   */
32  public class LambdaHandler extends AbstractExpressionHandler {
33  
34      /**
35       * Checks whether the lambda is correctly indented, this variable get its value from checking
36       * the lambda handler's indentation, and it is being used in aligning the lambda's children.
37       * A true value depicts lambda is correctly aligned without giving any errors.
38       * This is updated to false where there is any Indentation error log.
39       */
40      private boolean isLambdaCorrectlyIndented = true;
41  
42      /**
43       * Construct an instance of this handler with the given indentation context,
44       * abstract syntax tree, and parent handler.
45       *
46       * @param context      the indentation check
47       * @param ast the abstract syntax tree
48       * @param parent the parent handler
49       */
50      public LambdaHandler(IndentationContext context,
51                           DetailAST ast, AbstractExpressionHandler parent) {
52          super(context, "lambda", ast, parent);
53      }
54  
55      @Override
56      public IndentLevel getSuggestedChildIndent(AbstractExpressionHandler child) {
57          IndentLevel childIndent = getIndent();
58          if (isLambdaCorrectlyIndented) {
59              // If the lambda is correctly indented, include its line start as acceptable to
60              // avoid false positives. When "forceStrictCondition" is off, we allow indents
61              // larger than expected (e.g., 12 instead of 6 or 8). These larger indents are
62              // accepted but not recorded, so child indent suggestions may be inaccurate.
63              // Adding the actual line start ensures the tool recognizes the lambda’s real indent
64              // context.
65              childIndent = IndentLevel.addAcceptable(childIndent, getLineStart(getMainAst()));
66  
67              if (isSameLineAsSwitch(child.getMainAst()) || child instanceof SlistHandler) {
68                  // Lambda with block body (enclosed in {})
69                  childIndent = IndentLevel.addAcceptable(childIndent,
70                      getLineStart(getMainAst().getFirstChild()));
71              }
72              else {
73                  // Single-expression lambda (no {} block):
74                  // assume line wrapping and add additional indentation
75                  // for the statement in the next line.
76                  childIndent = new IndentLevel(childIndent,
77                          getContext().getLineWrappingIndentation());
78              }
79          }
80  
81          return childIndent;
82      }
83  
84      /**
85       * {@inheritDoc}.
86       *
87       * @noinspection MethodWithMultipleReturnPoints
88       * @noinspectionreason MethodWithMultipleReturnPoints - indentation is complex and
89       *      tightly coupled, thus making this method difficult to refactor
90       */
91      @Override
92      protected IndentLevel getIndentImpl() {
93          if (getParent() instanceof MethodCallHandler) {
94              return getParent().getSuggestedChildIndent(this);
95          }
96  
97          final IndentLevel result;
98          final DetailAST enumConstDef = findParentEnumConstantDef();
99          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 }