View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 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.CommonUtils;
30  
31  /**
32   * This class checks line-wrapping into definitions and expressions. The
33   * line-wrapping indentation should be not less then value of the
34   * lineWrappingIndentation parameter.
35   *
36   * @author maxvetrenko
37   * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a>
38   */
39  public class LineWrappingHandler {
40  
41      /**
42       * Enum to be used for test if first line's indentation should be checked or not.
43       */
44      public enum LineWrappingOptions {
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           * @param val value.
57           * @return enum instance.
58           *
59           * @noinspection BooleanParameter
60           */
61          public static LineWrappingOptions ofBoolean(boolean val) {
62              LineWrappingOptions option = NONE;
63              if (val) {
64                  option = IGNORE_FIRST_LINE;
65              }
66              return option;
67          }
68      }
69  
70      /**
71       * The current instance of {@code IndentationCheck} class using this
72       * handler. This field used to get access to private fields of
73       * IndentationCheck instance.
74       */
75      private final IndentationCheck indentCheck;
76  
77      /**
78       * Sets values of class field, finds last node and calculates indentation level.
79       *
80       * @param instance
81       *            instance of IndentationCheck.
82       */
83      public LineWrappingHandler(IndentationCheck instance) {
84          indentCheck = instance;
85      }
86  
87      /**
88       * Checks line wrapping into expressions and definitions using property
89       * 'lineWrappingIndentation'.
90       *
91       * @param firstNode First node to start examining.
92       * @param lastNode Last node to examine inclusively.
93       */
94      public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
95          checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation());
96      }
97  
98      /**
99       * Checks line wrapping into expressions and definitions.
100      *
101      * @param firstNode First node to start examining.
102      * @param lastNode Last node to examine inclusively.
103      * @param indentLevel Indentation all wrapped lines should use.
104      */
105     private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) {
106         checkIndentation(firstNode, lastNode, indentLevel,
107                 -1, LineWrappingOptions.IGNORE_FIRST_LINE);
108     }
109 
110     /**
111      * Checks line wrapping into expressions and definitions.
112      *
113      * @param firstNode First node to start examining.
114      * @param lastNode Last node to examine inclusively.
115      * @param indentLevel Indentation all wrapped lines should use.
116      * @param startIndent Indentation first line before wrapped lines used.
117      * @param ignoreFirstLine Test if first line's indentation should be checked or not.
118      */
119     public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel,
120             int startIndent, LineWrappingOptions ignoreFirstLine) {
121         final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode,
122                 lastNode);
123 
124         final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
125         if (firstLineNode.getType() == TokenTypes.AT) {
126             DetailAST node = firstLineNode.getParent();
127             while (node != null) {
128                 if (node.getType() == TokenTypes.ANNOTATION) {
129                     final DetailAST atNode = node.getFirstChild();
130                     final NavigableMap<Integer, DetailAST> annotationLines =
131                         firstNodesOnLines.subMap(
132                             node.getLineNo(),
133                             true,
134                             getNextNodeLine(firstNodesOnLines, node),
135                             true
136                         );
137                     checkAnnotationIndentation(atNode, annotationLines, indentLevel);
138                 }
139                 node = node.getNextSibling();
140             }
141         }
142 
143         if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) {
144             // First node should be removed because it was already checked before.
145             firstNodesOnLines.remove(firstNodesOnLines.firstKey());
146         }
147 
148         final int firstNodeIndent;
149         if (startIndent == -1) {
150             firstNodeIndent = getLineStart(firstLineNode);
151         }
152         else {
153             firstNodeIndent = startIndent;
154         }
155         final int currentIndent = firstNodeIndent + indentLevel;
156 
157         for (DetailAST node : firstNodesOnLines.values()) {
158             final int currentType = node.getType();
159 
160             if (currentType == TokenTypes.RPAREN) {
161                 logWarningMessage(node, firstNodeIndent);
162             }
163             else if (currentType != TokenTypes.RCURLY && currentType != TokenTypes.ARRAY_INIT) {
164                 logWarningMessage(node, currentIndent);
165             }
166         }
167     }
168 
169     /**
170      * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
171      * which case, it returns the last line.
172      *
173      * @param firstNodesOnLines NavigableMap of lines and their first nodes.
174      * @param node the node for which to find the next node line
175      * @return the line number of the next line in the map
176      */
177     private static Integer getNextNodeLine(
178             NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
179         Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
180         if (nextNodeLine == null) {
181             nextNodeLine = firstNodesOnLines.lastKey();
182         }
183         return nextNodeLine;
184     }
185 
186     /**
187      * Finds first nodes on line and puts them into Map.
188      *
189      * @param firstNode First node to start examining.
190      * @param lastNode Last node to examine inclusively.
191      * @return NavigableMap which contains lines numbers as a key and first
192      *         nodes on lines as a values.
193      */
194     private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
195             DetailAST lastNode) {
196         final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
197 
198         result.put(firstNode.getLineNo(), firstNode);
199         DetailAST curNode = firstNode.getFirstChild();
200 
201         while (curNode != lastNode) {
202 
203             if (curNode.getType() == TokenTypes.OBJBLOCK
204                     || curNode.getType() == TokenTypes.SLIST) {
205                 curNode = curNode.getLastChild();
206             }
207 
208             final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
209 
210             if (firstTokenOnLine == null
211                 || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
212                 result.put(curNode.getLineNo(), curNode);
213             }
214             curNode = getNextCurNode(curNode);
215         }
216         return result;
217     }
218 
219     /**
220      * Returns next curNode node.
221      *
222      * @param curNode current node.
223      * @return next curNode node.
224      */
225     private static DetailAST getNextCurNode(DetailAST curNode) {
226         DetailAST nodeToVisit = curNode.getFirstChild();
227         DetailAST currentNode = curNode;
228 
229         while (nodeToVisit == null) {
230             nodeToVisit = currentNode.getNextSibling();
231             if (nodeToVisit == null) {
232                 currentNode = currentNode.getParent();
233             }
234         }
235         return nodeToVisit;
236     }
237 
238     /**
239      * Checks line wrapping into annotations.
240      *
241      * @param atNode at-clause node.
242      * @param firstNodesOnLines map which contains
243      *     first nodes as values and line numbers as keys.
244      * @param indentLevel line wrapping indentation.
245      */
246     private void checkAnnotationIndentation(DetailAST atNode,
247             NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
248         final int firstNodeIndent = getLineStart(atNode);
249         final int currentIndent = firstNodeIndent + indentLevel;
250         final Collection<DetailAST> values = firstNodesOnLines.values();
251         final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
252         final int lastAnnotationLine = lastAnnotationNode.getLineNo();
253 
254         final Iterator<DetailAST> itr = values.iterator();
255         while (firstNodesOnLines.size() > 1) {
256             final DetailAST node = itr.next();
257 
258             final DetailAST parentNode = node.getParent();
259             final boolean isCurrentNodeCloseAnnotationAloneInLine =
260                 node.getLineNo() == lastAnnotationLine
261                     && isEndOfScope(lastAnnotationNode, node);
262             if (isCurrentNodeCloseAnnotationAloneInLine
263                     || node.getType() == TokenTypes.AT
264                     && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
265                         || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
266                     || node.getLineNo() == atNode.getLineNo()) {
267                 logWarningMessage(node, firstNodeIndent);
268             }
269             else {
270                 logWarningMessage(node, currentIndent);
271             }
272             itr.remove();
273         }
274     }
275 
276     /**
277      * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
278      * the same line.
279      *
280      * @param lastAnnotationNode the last node of the annotation
281      * @param node the node indicating where to begin checking
282      * @return true if all the nodes up to the last annotation node are end of scope nodes
283      *         false otherwise
284      */
285     private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
286         DetailAST checkNode = node;
287         boolean endOfScope = true;
288         while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
289             switch (checkNode.getType()) {
290                 case TokenTypes.RCURLY:
291                 case TokenTypes.RBRACK:
292                     while (checkNode.getNextSibling() == null) {
293                         checkNode = checkNode.getParent();
294                     }
295                     checkNode = checkNode.getNextSibling();
296                     break;
297                 default:
298                     endOfScope = false;
299 
300             }
301 
302         }
303         return endOfScope;
304     }
305 
306     /**
307      * Get the column number for the start of a given expression, expanding
308      * tabs out into spaces in the process.
309      *
310      * @param ast   the expression to find the start of
311      *
312      * @return the column number for the start of the expression
313      */
314     private int expandedTabsColumnNo(DetailAST ast) {
315         final String line =
316             indentCheck.getLine(ast.getLineNo() - 1);
317 
318         return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(),
319             indentCheck.getIndentationTabWidth());
320     }
321 
322     /**
323      * Get the start of the line for the given expression.
324      *
325      * @param ast   the expression to find the start of the line for
326      *
327      * @return the start of the line for the given expression
328      */
329     private int getLineStart(DetailAST ast) {
330         final String line = indentCheck.getLine(ast.getLineNo() - 1);
331         return getLineStart(line);
332     }
333 
334     /**
335      * Get the start of the specified line.
336      *
337      * @param line the specified line number
338      * @return the start of the specified line
339      */
340     private int getLineStart(String line) {
341         int index = 0;
342         while (Character.isWhitespace(line.charAt(index))) {
343             index++;
344         }
345         return CommonUtils.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
346     }
347 
348     /**
349      * Logs warning message if indentation is incorrect.
350      *
351      * @param currentNode
352      *            current node which probably invoked an error.
353      * @param currentIndent
354      *            correct indentation.
355      */
356     private void logWarningMessage(DetailAST currentNode, int currentIndent) {
357         if (indentCheck.isForceStrictCondition()) {
358             if (expandedTabsColumnNo(currentNode) != currentIndent) {
359                 indentCheck.indentationLog(currentNode.getLineNo(),
360                         IndentationCheck.MSG_ERROR, currentNode.getText(),
361                         expandedTabsColumnNo(currentNode), currentIndent);
362             }
363         }
364         else {
365             if (expandedTabsColumnNo(currentNode) < currentIndent) {
366                 indentCheck.indentationLog(currentNode.getLineNo(),
367                         IndentationCheck.MSG_ERROR, currentNode.getText(),
368                         expandedTabsColumnNo(currentNode), currentIndent);
369             }
370         }
371     }
372 }