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 java.util.Collection;
23  import java.util.Iterator;
24  import java.util.NavigableMap;
25  import java.util.TreeMap;
26  
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31  
32  /**
33   * This class checks line-wrapping into definitions and expressions. The
34   * line-wrapping indentation should be not less than value of the
35   * lineWrappingIndentation parameter.
36   *
37   */
38  public class LineWrappingHandler {
39  
40      /**
41       * Enum to be used for test if first line's indentation should be checked or not.
42       */
43      public enum LineWrappingOptions {
44  
45          /**
46           * First line's indentation should NOT be checked.
47           */
48          IGNORE_FIRST_LINE,
49          /**
50           * First line's indentation should be checked.
51           */
52          NONE
53  
54      }
55  
56      /**
57       * The list of ignored token types for being checked by lineWrapping indentation
58       * inside {@code checkIndentation()} as these tokens are checked for lineWrapping
59       * inside their dedicated handlers.
60       *
61       * @see NewHandler#getIndentImpl()
62       * @see BlockParentHandler#curlyIndent()
63       * @see ArrayInitHandler#getIndentImpl()
64       * @see CaseHandler#getIndentImpl()
65       */
66      private static final int[] IGNORED_LIST = {
67          TokenTypes.LCURLY,
68          TokenTypes.RCURLY,
69          TokenTypes.LITERAL_NEW,
70          TokenTypes.LITERAL_YIELD,
71          TokenTypes.ARRAY_INIT,
72          TokenTypes.LITERAL_DEFAULT,
73          TokenTypes.LITERAL_CASE,
74      };
75  
76      /**
77       * The current instance of {@code IndentationCheck} class using this
78       * handler. This field used to get access to private fields of
79       * IndentationCheck instance.
80       */
81      private final IndentationCheck indentCheck;
82  
83      /**
84       * Sets values of class field, finds last node and calculates indentation level.
85       *
86       * @param instance
87       *            instance of IndentationCheck.
88       */
89      public LineWrappingHandler(IndentationCheck instance) {
90          indentCheck = instance;
91      }
92  
93      /**
94       * Checks line wrapping into expressions and definitions using property
95       * 'lineWrappingIndentation'.
96       *
97       * @param firstNode First node to start examining.
98       * @param lastNode Last node to examine inclusively.
99       */
100     public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
101         checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation());
102     }
103 
104     /**
105      * Checks line wrapping into expressions and definitions.
106      *
107      * @param firstNode First node to start examining.
108      * @param lastNode Last node to examine inclusively.
109      * @param indentLevel Indentation all wrapped lines should use.
110      */
111     private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) {
112         checkIndentation(firstNode, lastNode, indentLevel,
113                 -1, LineWrappingOptions.IGNORE_FIRST_LINE);
114     }
115 
116     /**
117      * Checks line wrapping into expressions and definitions.
118      *
119      * @param firstNode First node to start examining.
120      * @param lastNode Last node to examine inclusively.
121      * @param indentLevel Indentation all wrapped lines should use.
122      * @param startIndent Indentation first line before wrapped lines used.
123      * @param ignoreFirstLine Test if first line's indentation should be checked or not.
124      */
125     public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel,
126             int startIndent, LineWrappingOptions ignoreFirstLine) {
127         final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode,
128                 lastNode);
129 
130         final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
131         if (firstLineNode.getType() == TokenTypes.AT) {
132             checkForAnnotationIndentation(firstNodesOnLines, indentLevel);
133         }
134 
135         if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) {
136             // First node should be removed because it was already checked before.
137             firstNodesOnLines.remove(firstNodesOnLines.firstKey());
138         }
139 
140         final int firstNodeIndent;
141         if (startIndent == -1) {
142             firstNodeIndent = getLineStart(firstLineNode);
143         }
144         else {
145             firstNodeIndent = startIndent;
146         }
147         final int currentIndent = firstNodeIndent + indentLevel;
148 
149         for (DetailAST node : firstNodesOnLines.values()) {
150             final int currentType = node.getType();
151             if (checkForNullParameterChild(node) || checkForMethodLparenNewLine(node)
152                     || !shouldProcessTextBlockLiteral(node)) {
153                 continue;
154             }
155             if (currentType == TokenTypes.RPAREN) {
156                 logWarningMessage(node, firstNodeIndent);
157             }
158             else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) {
159                 logWarningMessage(node, currentIndent);
160             }
161         }
162     }
163 
164     /**
165      * Checks for annotation indentation.
166      *
167      * @param firstNodesOnLines the nodes which are present in the beginning of each line.
168      * @param indentLevel line wrapping indentation.
169      */
170     public void checkForAnnotationIndentation(
171             NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
172         final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
173         DetailAST node = firstLineNode.getParent();
174         while (node != null) {
175             if (node.getType() == TokenTypes.ANNOTATION) {
176                 final DetailAST atNode = node.getFirstChild();
177                 final NavigableMap<Integer, DetailAST> annotationLines =
178                         firstNodesOnLines.subMap(
179                                 node.getLineNo(),
180                                 true,
181                                 getNextNodeLine(firstNodesOnLines, node),
182                                 true
183                         );
184                 checkAnnotationIndentation(atNode, annotationLines, indentLevel);
185             }
186             node = node.getNextSibling();
187         }
188     }
189 
190     /**
191      * Checks whether parameter node has any child or not.
192      *
193      * @param node the node for which to check.
194      * @return true if  parameter has no child.
195      */
196     public static boolean checkForNullParameterChild(DetailAST node) {
197         return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS;
198     }
199 
200     /**
201      * Checks whether the method lparen starts from a new line or not.
202      *
203      * @param node the node for which to check.
204      * @return true if method lparen starts from a new line.
205      */
206     public static boolean checkForMethodLparenNewLine(DetailAST node) {
207         final int parentType = node.getParent().getType();
208         return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN;
209     }
210 
211     /**
212      * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
213      * which case, it returns the last line.
214      *
215      * @param firstNodesOnLines NavigableMap of lines and their first nodes.
216      * @param node the node for which to find the next node line
217      * @return the line number of the next line in the map
218      */
219     private static Integer getNextNodeLine(
220             NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
221         Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
222         if (nextNodeLine == null) {
223             nextNodeLine = firstNodesOnLines.lastKey();
224         }
225         return nextNodeLine;
226     }
227 
228     /**
229      * Finds first nodes on line and puts them into Map.
230      *
231      * @param firstNode First node to start examining.
232      * @param lastNode Last node to examine inclusively.
233      * @return NavigableMap which contains lines numbers as a key and first
234      *         nodes on lines as a values.
235      */
236     private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
237             DetailAST lastNode) {
238         final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
239 
240         result.put(firstNode.getLineNo(), firstNode);
241         DetailAST curNode = firstNode.getFirstChild();
242 
243         while (curNode != lastNode) {
244             if (curNode.getType() == TokenTypes.OBJBLOCK
245                     || curNode.getType() == TokenTypes.SLIST) {
246                 curNode = curNode.getLastChild();
247             }
248 
249             final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
250 
251             if (firstTokenOnLine == null
252                 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
253                 result.put(curNode.getLineNo(), curNode);
254             }
255             curNode = getNextCurNode(curNode);
256         }
257         return result;
258     }
259 
260     /**
261      * Checks whether indentation of {@code TEXT_BLOCK_LITERAL_END}
262      *     needs to be checked. Yes if it is first on start of the line.
263      *
264      * @param node the node
265      * @return true if node is line-starting node.
266      */
267     private boolean shouldProcessTextBlockLiteral(DetailAST node) {
268         return node.getType() != TokenTypes.TEXT_BLOCK_LITERAL_END
269                 || expandedTabsColumnNo(node) == getLineStart(node);
270     }
271 
272     /**
273      * Returns next curNode node.
274      *
275      * @param curNode current node.
276      * @return next curNode node.
277      */
278     private static DetailAST getNextCurNode(DetailAST curNode) {
279         DetailAST nodeToVisit = curNode.getFirstChild();
280         DetailAST currentNode = curNode;
281 
282         while (nodeToVisit == null) {
283             nodeToVisit = currentNode.getNextSibling();
284             if (nodeToVisit == null) {
285                 currentNode = currentNode.getParent();
286             }
287         }
288         return nodeToVisit;
289     }
290 
291     /**
292      * Checks line wrapping into annotations.
293      *
294      * @param atNode block tag node.
295      * @param firstNodesOnLines map which contains
296      *     first nodes as values and line numbers as keys.
297      * @param indentLevel line wrapping indentation.
298      */
299     private void checkAnnotationIndentation(DetailAST atNode,
300             NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
301         final int firstNodeIndent = getLineStart(atNode);
302         final int currentIndent = firstNodeIndent + indentLevel;
303         final Collection<DetailAST> values = firstNodesOnLines.values();
304         final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
305         final int lastAnnotationLine = lastAnnotationNode.getLineNo();
306 
307         final Iterator<DetailAST> itr = values.iterator();
308         while (firstNodesOnLines.size() > 1) {
309             final DetailAST node = itr.next();
310 
311             final DetailAST parentNode = node.getParent();
312             final boolean isArrayInitPresentInAncestors =
313                 isParentContainsTokenType(node, TokenTypes.ANNOTATION_ARRAY_INIT);
314             final boolean isCurrentNodeCloseAnnotationAloneInLine =
315                 node.getLineNo() == lastAnnotationLine
316                     && isEndOfScope(lastAnnotationNode, node);
317             if (!isArrayInitPresentInAncestors
318                     && (isCurrentNodeCloseAnnotationAloneInLine
319                     || node.getType() == TokenTypes.AT
320                     && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
321                         || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
322                     || TokenUtil.areOnSameLine(node, atNode))) {
323                 logWarningMessage(node, firstNodeIndent);
324             }
325             else if (!isArrayInitPresentInAncestors) {
326                 logWarningMessage(node, currentIndent);
327             }
328             itr.remove();
329         }
330     }
331 
332     /**
333      * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
334      * the same line.
335      *
336      * @param lastAnnotationNode the last node of the annotation
337      * @param node the node indicating where to begin checking
338      * @return true if all the nodes up to the last annotation node are end of scope nodes
339      *         false otherwise
340      */
341     private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
342         DetailAST checkNode = node;
343         boolean endOfScope = true;
344         while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
345             switch (checkNode.getType()) {
346                 case TokenTypes.RCURLY, TokenTypes.RBRACK -> {
347                     while (checkNode.getNextSibling() == null) {
348                         checkNode = checkNode.getParent();
349                     }
350                     checkNode = checkNode.getNextSibling();
351                 }
352                 default -> endOfScope = false;
353             }
354         }
355 
356         return endOfScope;
357     }
358 
359     /**
360      * Checks that some parent of given node contains given token type.
361      *
362      * @param node node to check
363      * @param type type to look for
364      * @return true if there is a parent of given type
365      */
366     private static boolean isParentContainsTokenType(final DetailAST node, int type) {
367         boolean returnValue = false;
368         for (DetailAST ast = node.getParent(); ast != null; ast = ast.getParent()) {
369             if (ast.getType() == type) {
370                 returnValue = true;
371                 break;
372             }
373         }
374         return returnValue;
375     }
376 
377     /**
378      * Get the column number for the start of a given expression, expanding
379      * tabs out into spaces in the process.
380      *
381      * @param ast   the expression to find the start of
382      *
383      * @return the column number for the start of the expression
384      */
385     private int expandedTabsColumnNo(DetailAST ast) {
386         final String line =
387             indentCheck.getLine(ast.getLineNo() - 1);
388 
389         return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
390             indentCheck.getIndentationTabWidth());
391     }
392 
393     /**
394      * Get the start of the line for the given expression.
395      *
396      * @param ast   the expression to find the start of the line for
397      *
398      * @return the start of the line for the given expression
399      */
400     private int getLineStart(DetailAST ast) {
401         final String line = indentCheck.getLine(ast.getLineNo() - 1);
402         return getLineStart(line);
403     }
404 
405     /**
406      * Get the start of the specified line.
407      *
408      * @param line the specified line number
409      * @return the start of the specified line
410      */
411     private int getLineStart(String line) {
412         int index = 0;
413         while (Character.isWhitespace(line.charAt(index))) {
414             index++;
415         }
416         return CommonUtil.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
417     }
418 
419     /**
420      * Logs warning message if indentation is incorrect.
421      *
422      * @param currentNode
423      *            current node which probably invoked a violation.
424      * @param currentIndent
425      *            correct indentation.
426      */
427     private void logWarningMessage(DetailAST currentNode, int currentIndent) {
428         if (indentCheck.isForceStrictCondition()) {
429             if (expandedTabsColumnNo(currentNode) != currentIndent) {
430                 indentCheck.indentationLog(currentNode,
431                         IndentationCheck.MSG_ERROR, currentNode.getText(),
432                         expandedTabsColumnNo(currentNode), currentIndent);
433             }
434         }
435         else {
436             if (expandedTabsColumnNo(currentNode) < currentIndent) {
437                 indentCheck.indentationLog(currentNode,
438                         IndentationCheck.MSG_ERROR, currentNode.getText(),
439                         expandedTabsColumnNo(currentNode), currentIndent);
440             }
441         }
442     }
443 
444 }