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