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