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 */
037public class LineWrappingHandler {
038
039    /**
040     * Enum to be used for test if first line's indentation should be checked or not.
041     */
042    public enum LineWrappingOptions {
043
044        /**
045         * First line's indentation should NOT be checked.
046         */
047        IGNORE_FIRST_LINE,
048        /**
049         * First line's indentation should be checked.
050         */
051        NONE;
052
053        /**
054         * Builds enum value from boolean.
055         * @param val value.
056         * @return enum instance.
057         *
058         * @noinspection BooleanParameter
059         */
060        public static LineWrappingOptions ofBoolean(boolean val) {
061            LineWrappingOptions option = NONE;
062            if (val) {
063                option = IGNORE_FIRST_LINE;
064            }
065            return option;
066        }
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            if (curNode.getType() == TokenTypes.OBJBLOCK
203                    || curNode.getType() == TokenTypes.SLIST) {
204                curNode = curNode.getLastChild();
205            }
206
207            final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
208
209            if (firstTokenOnLine == null
210                || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
211                result.put(curNode.getLineNo(), curNode);
212            }
213            curNode = getNextCurNode(curNode);
214        }
215        return result;
216    }
217
218    /**
219     * Returns next curNode node.
220     *
221     * @param curNode current node.
222     * @return next curNode node.
223     */
224    private static DetailAST getNextCurNode(DetailAST curNode) {
225        DetailAST nodeToVisit = curNode.getFirstChild();
226        DetailAST currentNode = curNode;
227
228        while (nodeToVisit == null) {
229            nodeToVisit = currentNode.getNextSibling();
230            if (nodeToVisit == null) {
231                currentNode = currentNode.getParent();
232            }
233        }
234        return nodeToVisit;
235    }
236
237    /**
238     * Checks line wrapping into annotations.
239     *
240     * @param atNode at-clause node.
241     * @param firstNodesOnLines map which contains
242     *     first nodes as values and line numbers as keys.
243     * @param indentLevel line wrapping indentation.
244     */
245    private void checkAnnotationIndentation(DetailAST atNode,
246            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
247        final int firstNodeIndent = getLineStart(atNode);
248        final int currentIndent = firstNodeIndent + indentLevel;
249        final Collection<DetailAST> values = firstNodesOnLines.values();
250        final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
251        final int lastAnnotationLine = lastAnnotationNode.getLineNo();
252
253        final Iterator<DetailAST> itr = values.iterator();
254        while (firstNodesOnLines.size() > 1) {
255            final DetailAST node = itr.next();
256
257            final DetailAST parentNode = node.getParent();
258            final boolean isCurrentNodeCloseAnnotationAloneInLine =
259                node.getLineNo() == lastAnnotationLine
260                    && isEndOfScope(lastAnnotationNode, node);
261            if (isCurrentNodeCloseAnnotationAloneInLine
262                    || node.getType() == TokenTypes.AT
263                    && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
264                        || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
265                    || node.getLineNo() == atNode.getLineNo()) {
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        return endOfScope;
301    }
302
303    /**
304     * Get the column number for the start of a given expression, expanding
305     * tabs out into spaces in the process.
306     *
307     * @param ast   the expression to find the start of
308     *
309     * @return the column number for the start of the expression
310     */
311    private int expandedTabsColumnNo(DetailAST ast) {
312        final String line =
313            indentCheck.getLine(ast.getLineNo() - 1);
314
315        return CommonUtils.lengthExpandedTabs(line, ast.getColumnNo(),
316            indentCheck.getIndentationTabWidth());
317    }
318
319    /**
320     * Get the start of the line for the given expression.
321     *
322     * @param ast   the expression to find the start of the line for
323     *
324     * @return the start of the line for the given expression
325     */
326    private int getLineStart(DetailAST ast) {
327        final String line = indentCheck.getLine(ast.getLineNo() - 1);
328        return getLineStart(line);
329    }
330
331    /**
332     * Get the start of the specified line.
333     *
334     * @param line the specified line number
335     * @return the start of the specified line
336     */
337    private int getLineStart(String line) {
338        int index = 0;
339        while (Character.isWhitespace(line.charAt(index))) {
340            index++;
341        }
342        return CommonUtils.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
343    }
344
345    /**
346     * Logs warning message if indentation is incorrect.
347     *
348     * @param currentNode
349     *            current node which probably invoked an error.
350     * @param currentIndent
351     *            correct indentation.
352     */
353    private void logWarningMessage(DetailAST currentNode, int currentIndent) {
354        if (indentCheck.isForceStrictCondition()) {
355            if (expandedTabsColumnNo(currentNode) != currentIndent) {
356                indentCheck.indentationLog(currentNode.getLineNo(),
357                        IndentationCheck.MSG_ERROR, currentNode.getText(),
358                        expandedTabsColumnNo(currentNode), currentIndent);
359            }
360        }
361        else {
362            if (expandedTabsColumnNo(currentNode) < currentIndent) {
363                indentCheck.indentationLog(currentNode.getLineNo(),
364                        IndentationCheck.MSG_ERROR, currentNode.getText(),
365                        expandedTabsColumnNo(currentNode), currentIndent);
366            }
367        }
368    }
369
370}