001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.CommonUtil;
030import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
031
032/**
033 * This class checks line-wrapping into definitions and expressions. The
034 * line-wrapping indentation should be not less than value of the
035 * lineWrappingIndentation parameter.
036 *
037 */
038public class LineWrappingHandler {
039
040    /**
041     * Enum to be used for test if first line's indentation should be checked or not.
042     */
043    public enum LineWrappingOptions {
044
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         *
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 block tag 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 isArrayInitPresentInAncestors =
261                isParentContainsTokenType(node, TokenTypes.ANNOTATION_ARRAY_INIT);
262            final boolean isCurrentNodeCloseAnnotationAloneInLine =
263                node.getLineNo() == lastAnnotationLine
264                    && isEndOfScope(lastAnnotationNode, node);
265            if (!isArrayInitPresentInAncestors
266                    && (isCurrentNodeCloseAnnotationAloneInLine
267                    || node.getType() == TokenTypes.AT
268                    && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
269                        || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
270                    || TokenUtil.areOnSameLine(node, atNode))) {
271                logWarningMessage(node, firstNodeIndent);
272            }
273            else if (!isArrayInitPresentInAncestors) {
274                logWarningMessage(node, currentIndent);
275            }
276            itr.remove();
277        }
278    }
279
280    /**
281     * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
282     * the same line.
283     *
284     * @param lastAnnotationNode the last node of the annotation
285     * @param node the node indicating where to begin checking
286     * @return true if all the nodes up to the last annotation node are end of scope nodes
287     *         false otherwise
288     */
289    private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
290        DetailAST checkNode = node;
291        boolean endOfScope = true;
292        while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
293            switch (checkNode.getType()) {
294                case TokenTypes.RCURLY:
295                case TokenTypes.RBRACK:
296                    while (checkNode.getNextSibling() == null) {
297                        checkNode = checkNode.getParent();
298                    }
299                    checkNode = checkNode.getNextSibling();
300                    break;
301                default:
302                    endOfScope = false;
303            }
304        }
305        return endOfScope;
306    }
307
308    private static boolean isParentContainsTokenType(final DetailAST node, int type) {
309        boolean returnValue = false;
310        for (DetailAST ast = node.getParent(); ast != null; ast = ast.getParent()) {
311            if (ast.getType() == type) {
312                returnValue = true;
313                break;
314            }
315        }
316        return returnValue;
317    }
318
319    /**
320     * Get the column number for the start of a given expression, expanding
321     * tabs out into spaces in the process.
322     *
323     * @param ast   the expression to find the start of
324     *
325     * @return the column number for the start of the expression
326     */
327    private int expandedTabsColumnNo(DetailAST ast) {
328        final String line =
329            indentCheck.getLine(ast.getLineNo() - 1);
330
331        return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
332            indentCheck.getIndentationTabWidth());
333    }
334
335    /**
336     * Get the start of the line for the given expression.
337     *
338     * @param ast   the expression to find the start of the line for
339     *
340     * @return the start of the line for the given expression
341     */
342    private int getLineStart(DetailAST ast) {
343        final String line = indentCheck.getLine(ast.getLineNo() - 1);
344        return getLineStart(line);
345    }
346
347    /**
348     * Get the start of the specified line.
349     *
350     * @param line the specified line number
351     * @return the start of the specified line
352     */
353    private int getLineStart(String line) {
354        int index = 0;
355        while (Character.isWhitespace(line.charAt(index))) {
356            index++;
357        }
358        return CommonUtil.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
359    }
360
361    /**
362     * Logs warning message if indentation is incorrect.
363     *
364     * @param currentNode
365     *            current node which probably invoked a violation.
366     * @param currentIndent
367     *            correct indentation.
368     */
369    private void logWarningMessage(DetailAST currentNode, int currentIndent) {
370        if (indentCheck.isForceStrictCondition()) {
371            if (expandedTabsColumnNo(currentNode) != currentIndent) {
372                indentCheck.indentationLog(currentNode,
373                        IndentationCheck.MSG_ERROR, currentNode.getText(),
374                        expandedTabsColumnNo(currentNode), currentIndent);
375            }
376        }
377        else {
378            if (expandedTabsColumnNo(currentNode) < currentIndent) {
379                indentCheck.indentationLog(currentNode,
380                        IndentationCheck.MSG_ERROR, currentNode.getText(),
381                        expandedTabsColumnNo(currentNode), currentIndent);
382            }
383        }
384    }
385
386}