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