001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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         * First line's indentation should NOT be checked.
047         */
048        IGNORE_FIRST_LINE,
049        /**
050         * First line's indentation should be checked.
051         */
052        NONE;
053
054        /**
055         * Builds enum value from boolean.
056         * @param val value.
057         * @return enum instance.
058         *
059         * @noinspection BooleanParameter
060         */
061        public static LineWrappingOptions ofBoolean(boolean val) {
062            LineWrappingOptions option = NONE;
063            if (val) {
064                option = IGNORE_FIRST_LINE;
065            }
066            return option;
067        }
068    }
069
070    /**
071     * The current instance of {@code IndentationCheck} class using this
072     * handler. This field used to get access to private fields of
073     * IndentationCheck instance.
074     */
075    private final IndentationCheck indentCheck;
076
077    /**
078     * Sets values of class field, finds last node and calculates indentation level.
079     *
080     * @param instance
081     *            instance of IndentationCheck.
082     */
083    public LineWrappingHandler(IndentationCheck instance) {
084        indentCheck = instance;
085    }
086
087    /**
088     * Checks line wrapping into expressions and definitions using property
089     * 'lineWrappingIndentation'.
090     *
091     * @param firstNode First node to start examining.
092     * @param lastNode Last node to examine inclusively.
093     */
094    public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
095        checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation());
096    }
097
098    /**
099     * 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                logWarningMessage(node, firstNodeIndent);
267            }
268            else {
269                logWarningMessage(node, currentIndent);
270            }
271            itr.remove();
272        }
273    }
274
275    /**
276     * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
277     * the same line.
278     *
279     * @param lastAnnotationNode the last node of the annotation
280     * @param node the node indicating where to begin checking
281     * @return true if all the nodes up to the last annotation node are end of scope nodes
282     *         false otherwise
283     */
284    private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
285        DetailAST checkNode = node;
286        boolean endOfScope = true;
287        while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
288            switch (checkNode.getType()) {
289                case TokenTypes.RCURLY:
290                case TokenTypes.RBRACK:
291                    while (checkNode.getNextSibling() == null) {
292                        checkNode = checkNode.getParent();
293                    }
294                    checkNode = checkNode.getNextSibling();
295                    break;
296                default:
297                    endOfScope = false;
298
299            }
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}