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