001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.indentation;
021
022import java.util.Collection;
023import java.util.Iterator;
024import java.util.NavigableMap;
025import java.util.TreeMap;
026
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
030
031/**
032 * This class checks line-wrapping into definitions and expressions. The
033 * line-wrapping indentation should be not less then value of the
034 * lineWrappingIndentation parameter.
035 *
036 * @author maxvetrenko
037 * @author <a href="mailto:piotr.listkiewicz@gmail.com">liscju</a>
038 */
039public class LineWrappingHandler {
040
041    /**
042     * Enum to be used for test if first line's indentation should be checked or not.
043     */
044    public enum LineWrappingOptions {
045
046        /**
047         * First line's indentation should NOT be checked.
048         */
049        IGNORE_FIRST_LINE,
050        /**
051         * First line's indentation should be checked.
052         */
053        NONE;
054
055        /**
056         * Builds enum value from boolean.
057         * @param val value.
058         * @return enum instance.
059         *
060         * @noinspection BooleanParameter
061         */
062        public static LineWrappingOptions ofBoolean(boolean val) {
063            LineWrappingOptions option = NONE;
064            if (val) {
065                option = IGNORE_FIRST_LINE;
066            }
067            return option;
068        }
069
070    }
071
072    /**
073     * The current instance of {@code IndentationCheck} class using this
074     * handler. This field used to get access to private fields of
075     * IndentationCheck instance.
076     */
077    private final IndentationCheck indentCheck;
078
079    /**
080     * Sets values of class field, finds last node and calculates indentation level.
081     *
082     * @param instance
083     *            instance of IndentationCheck.
084     */
085    public LineWrappingHandler(IndentationCheck instance) {
086        indentCheck = instance;
087    }
088
089    /**
090     * Checks line wrapping into expressions and definitions using property
091     * 'lineWrappingIndentation'.
092     *
093     * @param firstNode First node to start examining.
094     * @param lastNode Last node to examine inclusively.
095     */
096    public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
097        checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation());
098    }
099
100    /**
101     * Checks line wrapping into expressions and definitions.
102     *
103     * @param firstNode First node to start examining.
104     * @param lastNode Last node to examine inclusively.
105     * @param indentLevel Indentation all wrapped lines should use.
106     */
107    private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) {
108        checkIndentation(firstNode, lastNode, indentLevel,
109                -1, LineWrappingOptions.IGNORE_FIRST_LINE);
110    }
111
112    /**
113     * Checks line wrapping into expressions and definitions.
114     *
115     * @param firstNode First node to start examining.
116     * @param lastNode Last node to examine inclusively.
117     * @param indentLevel Indentation all wrapped lines should use.
118     * @param startIndent Indentation first line before wrapped lines used.
119     * @param ignoreFirstLine Test if first line's indentation should be checked or not.
120     */
121    public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel,
122            int startIndent, LineWrappingOptions ignoreFirstLine) {
123        final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode,
124                lastNode);
125
126        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
127        if (firstLineNode.getType() == TokenTypes.AT) {
128            DetailAST node = firstLineNode.getParent();
129            while (node != null) {
130                if (node.getType() == TokenTypes.ANNOTATION) {
131                    final DetailAST atNode = node.getFirstChild();
132                    final NavigableMap<Integer, DetailAST> annotationLines =
133                        firstNodesOnLines.subMap(
134                            node.getLineNo(),
135                            true,
136                            getNextNodeLine(firstNodesOnLines, node),
137                            true
138                        );
139                    checkAnnotationIndentation(atNode, annotationLines, indentLevel);
140                }
141                node = node.getNextSibling();
142            }
143        }
144
145        if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) {
146            // First node should be removed because it was already checked before.
147            firstNodesOnLines.remove(firstNodesOnLines.firstKey());
148        }
149
150        final int firstNodeIndent;
151        if (startIndent == -1) {
152            firstNodeIndent = getLineStart(firstLineNode);
153        }
154        else {
155            firstNodeIndent = startIndent;
156        }
157        final int currentIndent = firstNodeIndent + indentLevel;
158
159        for (DetailAST node : firstNodesOnLines.values()) {
160            final int currentType = node.getType();
161
162            if (currentType == TokenTypes.RPAREN) {
163                logWarningMessage(node, firstNodeIndent);
164            }
165            else if (currentType != TokenTypes.RCURLY && currentType != TokenTypes.ARRAY_INIT) {
166                logWarningMessage(node, currentIndent);
167            }
168        }
169    }
170
171    /**
172     * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
173     * which case, it returns the last line.
174     *
175     * @param firstNodesOnLines NavigableMap of lines and their first nodes.
176     * @param node the node for which to find the next node line
177     * @return the line number of the next line in the map
178     */
179    private static Integer getNextNodeLine(
180            NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
181        Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
182        if (nextNodeLine == null) {
183            nextNodeLine = firstNodesOnLines.lastKey();
184        }
185        return nextNodeLine;
186    }
187
188    /**
189     * Finds first nodes on line and puts them into Map.
190     *
191     * @param firstNode First node to start examining.
192     * @param lastNode Last node to examine inclusively.
193     * @return NavigableMap which contains lines numbers as a key and first
194     *         nodes on lines as a values.
195     */
196    private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
197            DetailAST lastNode) {
198        final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
199
200        result.put(firstNode.getLineNo(), firstNode);
201        DetailAST curNode = firstNode.getFirstChild();
202
203        while (curNode != lastNode) {
204            if (curNode.getType() == TokenTypes.OBJBLOCK
205                    || curNode.getType() == TokenTypes.SLIST) {
206                curNode = curNode.getLastChild();
207            }
208
209            final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
210
211            if (firstTokenOnLine == null
212                || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
213                result.put(curNode.getLineNo(), curNode);
214            }
215            curNode = getNextCurNode(curNode);
216        }
217        return result;
218    }
219
220    /**
221     * Returns next curNode node.
222     *
223     * @param curNode current node.
224     * @return next curNode node.
225     */
226    private static DetailAST getNextCurNode(DetailAST curNode) {
227        DetailAST nodeToVisit = curNode.getFirstChild();
228        DetailAST currentNode = curNode;
229
230        while (nodeToVisit == null) {
231            nodeToVisit = currentNode.getNextSibling();
232            if (nodeToVisit == null) {
233                currentNode = currentNode.getParent();
234            }
235        }
236        return nodeToVisit;
237    }
238
239    /**
240     * Checks line wrapping into annotations.
241     *
242     * @param atNode at-clause node.
243     * @param firstNodesOnLines map which contains
244     *     first nodes as values and line numbers as keys.
245     * @param indentLevel line wrapping indentation.
246     */
247    private void checkAnnotationIndentation(DetailAST atNode,
248            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
249        final int firstNodeIndent = getLineStart(atNode);
250        final int currentIndent = firstNodeIndent + indentLevel;
251        final Collection<DetailAST> values = firstNodesOnLines.values();
252        final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
253        final int lastAnnotationLine = lastAnnotationNode.getLineNo();
254
255        final Iterator<DetailAST> itr = values.iterator();
256        while (firstNodesOnLines.size() > 1) {
257            final DetailAST node = itr.next();
258
259            final DetailAST parentNode = node.getParent();
260            final boolean isCurrentNodeCloseAnnotationAloneInLine =
261                node.getLineNo() == lastAnnotationLine
262                    && isEndOfScope(lastAnnotationNode, node);
263            if (isCurrentNodeCloseAnnotationAloneInLine
264                    || node.getType() == TokenTypes.AT
265                    && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
266                        || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
267                    || node.getLineNo() == atNode.getLineNo()) {
268                logWarningMessage(node, firstNodeIndent);
269            }
270            else {
271                logWarningMessage(node, currentIndent);
272            }
273            itr.remove();
274        }
275    }
276
277    /**
278     * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
279     * the same line.
280     *
281     * @param lastAnnotationNode the last node of the annotation
282     * @param node the node indicating where to begin checking
283     * @return true if all the nodes up to the last annotation node are end of scope nodes
284     *         false otherwise
285     */
286    private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
287        DetailAST checkNode = node;
288        boolean endOfScope = true;
289        while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
290            switch (checkNode.getType()) {
291                case TokenTypes.RCURLY:
292                case TokenTypes.RBRACK:
293                    while (checkNode.getNextSibling() == null) {
294                        checkNode = checkNode.getParent();
295                    }
296                    checkNode = checkNode.getNextSibling();
297                    break;
298                default:
299                    endOfScope = false;
300            }
301        }
302        return endOfScope;
303    }
304
305    /**
306     * Get the column number for the start of a given expression, expanding
307     * tabs out into spaces in the process.
308     *
309     * @param ast   the expression to find the start of
310     *
311     * @return the column number for the start of the expression
312     */
313    private int expandedTabsColumnNo(DetailAST ast) {
314        final String line =
315            indentCheck.getLine(ast.getLineNo() - 1);
316
317        return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(),
318            indentCheck.getIndentationTabWidth());
319    }
320
321    /**
322     * Get the start of the line for the given expression.
323     *
324     * @param ast   the expression to find the start of the line for
325     *
326     * @return the start of the line for the given expression
327     */
328    private int getLineStart(DetailAST ast) {
329        final String line = indentCheck.getLine(ast.getLineNo() - 1);
330        return getLineStart(line);
331    }
332
333    /**
334     * Get the start of the specified line.
335     *
336     * @param line the specified line number
337     * @return the start of the specified line
338     */
339    private int getLineStart(String line) {
340        int index = 0;
341        while (Character.isWhitespace(line.charAt(index))) {
342            index++;
343        }
344        return CommonUtils.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
345    }
346
347    /**
348     * Logs warning message if indentation is incorrect.
349     *
350     * @param currentNode
351     *            current node which probably invoked an error.
352     * @param currentIndent
353     *            correct indentation.
354     */
355    private void logWarningMessage(DetailAST currentNode, int currentIndent) {
356        if (indentCheck.isForceStrictCondition()) {
357            if (expandedTabsColumnNo(currentNode) != currentIndent) {
358                indentCheck.indentationLog(currentNode.getLineNo(),
359                        IndentationCheck.MSG_ERROR, currentNode.getText(),
360                        expandedTabsColumnNo(currentNode), currentIndent);
361            }
362        }
363        else {
364            if (expandedTabsColumnNo(currentNode) < currentIndent) {
365                indentCheck.indentationLog(currentNode.getLineNo(),
366                        IndentationCheck.MSG_ERROR, currentNode.getText(),
367                        expandedTabsColumnNo(currentNode), currentIndent);
368            }
369        }
370    }
371
372}