001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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 list of ignored token types for being checked by lineWrapping indentation
074     * inside {@code checkIndentation()} as these tokens are checked for lineWrapping
075     * inside their dedicated handlers.
076     *
077     * @see NewHandler#getIndentImpl()
078     * @see BlockParentHandler#curlyIndent()
079     * @see ArrayInitHandler#getIndentImpl()
080     */
081    private static final int[] IGNORED_LIST = {
082        TokenTypes.RCURLY,
083        TokenTypes.LITERAL_NEW,
084        TokenTypes.ARRAY_INIT,
085    };
086
087    /**
088     * The current instance of {@code IndentationCheck} class using this
089     * handler. This field used to get access to private fields of
090     * IndentationCheck instance.
091     */
092    private final IndentationCheck indentCheck;
093
094    /**
095     * Sets values of class field, finds last node and calculates indentation level.
096     *
097     * @param instance
098     *            instance of IndentationCheck.
099     */
100    public LineWrappingHandler(IndentationCheck instance) {
101        indentCheck = instance;
102    }
103
104    /**
105     * Checks line wrapping into expressions and definitions using property
106     * 'lineWrappingIndentation'.
107     *
108     * @param firstNode First node to start examining.
109     * @param lastNode Last node to examine inclusively.
110     */
111    public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
112        checkIndentation(firstNode, lastNode, indentCheck.getLineWrappingIndentation());
113    }
114
115    /**
116     * Checks line wrapping into expressions and definitions.
117     *
118     * @param firstNode First node to start examining.
119     * @param lastNode Last node to examine inclusively.
120     * @param indentLevel Indentation all wrapped lines should use.
121     */
122    private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) {
123        checkIndentation(firstNode, lastNode, indentLevel,
124                -1, LineWrappingOptions.IGNORE_FIRST_LINE);
125    }
126
127    /**
128     * Checks line wrapping into expressions and definitions.
129     *
130     * @param firstNode First node to start examining.
131     * @param lastNode Last node to examine inclusively.
132     * @param indentLevel Indentation all wrapped lines should use.
133     * @param startIndent Indentation first line before wrapped lines used.
134     * @param ignoreFirstLine Test if first line's indentation should be checked or not.
135     */
136    public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel,
137            int startIndent, LineWrappingOptions ignoreFirstLine) {
138        final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode,
139                lastNode);
140
141        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
142        if (firstLineNode.getType() == TokenTypes.AT) {
143            checkForAnnotationIndentation(firstNodesOnLines, indentLevel);
144        }
145
146        if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) {
147            // First node should be removed because it was already checked before.
148            firstNodesOnLines.remove(firstNodesOnLines.firstKey());
149        }
150
151        final int firstNodeIndent;
152        if (startIndent == -1) {
153            firstNodeIndent = getLineStart(firstLineNode);
154        }
155        else {
156            firstNodeIndent = startIndent;
157        }
158        final int currentIndent = firstNodeIndent + indentLevel;
159
160        for (DetailAST node : firstNodesOnLines.values()) {
161            final int currentType = node.getType();
162            if (checkForNullParameterChild(node) || checkForMethodLparenNewLine(node)) {
163                continue;
164            }
165            if (currentType == TokenTypes.RPAREN) {
166                logWarningMessage(node, firstNodeIndent);
167            }
168            else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) {
169                logWarningMessage(node, currentIndent);
170            }
171        }
172    }
173
174    /**
175     * Checks for annotation indentation.
176     *
177     * @param firstNodesOnLines the nodes which are present in the beginning of each line.
178     * @param indentLevel line wrapping indentation.
179     */
180    public void checkForAnnotationIndentation(
181            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
182        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
183        DetailAST node = firstLineNode.getParent();
184        while (node != null) {
185            if (node.getType() == TokenTypes.ANNOTATION) {
186                final DetailAST atNode = node.getFirstChild();
187                final NavigableMap<Integer, DetailAST> annotationLines =
188                        firstNodesOnLines.subMap(
189                                node.getLineNo(),
190                                true,
191                                getNextNodeLine(firstNodesOnLines, node),
192                                true
193                        );
194                checkAnnotationIndentation(atNode, annotationLines, indentLevel);
195            }
196            node = node.getNextSibling();
197        }
198    }
199
200    /**
201     * Checks whether parameter node has any child or not.
202     *
203     * @param node the node for which to check.
204     * @return true if  parameter has no child.
205     */
206    public static boolean checkForNullParameterChild(DetailAST node) {
207        return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS;
208    }
209
210    /**
211     * Checks whether the method lparen starts from a new line or not.
212     *
213     * @param node the node for which to check.
214     * @return true if method lparen starts from a new line.
215     */
216    public static boolean checkForMethodLparenNewLine(DetailAST node) {
217        final int parentType = node.getParent().getType();
218        return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN;
219    }
220
221    /**
222     * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
223     * which case, it returns the last line.
224     *
225     * @param firstNodesOnLines NavigableMap of lines and their first nodes.
226     * @param node the node for which to find the next node line
227     * @return the line number of the next line in the map
228     */
229    private static Integer getNextNodeLine(
230            NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
231        Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
232        if (nextNodeLine == null) {
233            nextNodeLine = firstNodesOnLines.lastKey();
234        }
235        return nextNodeLine;
236    }
237
238    /**
239     * Finds first nodes on line and puts them into Map.
240     *
241     * @param firstNode First node to start examining.
242     * @param lastNode Last node to examine inclusively.
243     * @return NavigableMap which contains lines numbers as a key and first
244     *         nodes on lines as a values.
245     */
246    private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
247            DetailAST lastNode) {
248        final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
249
250        result.put(firstNode.getLineNo(), firstNode);
251        DetailAST curNode = firstNode.getFirstChild();
252
253        while (curNode != lastNode) {
254            if (curNode.getType() == TokenTypes.OBJBLOCK
255                    || curNode.getType() == TokenTypes.SLIST) {
256                curNode = curNode.getLastChild();
257            }
258
259            final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
260
261            if (firstTokenOnLine == null
262                || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
263                result.put(curNode.getLineNo(), curNode);
264            }
265            curNode = getNextCurNode(curNode);
266        }
267        return result;
268    }
269
270    /**
271     * Returns next curNode node.
272     *
273     * @param curNode current node.
274     * @return next curNode node.
275     */
276    private static DetailAST getNextCurNode(DetailAST curNode) {
277        DetailAST nodeToVisit = curNode.getFirstChild();
278        DetailAST currentNode = curNode;
279
280        while (nodeToVisit == null) {
281            nodeToVisit = currentNode.getNextSibling();
282            if (nodeToVisit == null) {
283                currentNode = currentNode.getParent();
284            }
285        }
286        return nodeToVisit;
287    }
288
289    /**
290     * Checks line wrapping into annotations.
291     *
292     * @param atNode block tag node.
293     * @param firstNodesOnLines map which contains
294     *     first nodes as values and line numbers as keys.
295     * @param indentLevel line wrapping indentation.
296     */
297    private void checkAnnotationIndentation(DetailAST atNode,
298            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
299        final int firstNodeIndent = getLineStart(atNode);
300        final int currentIndent = firstNodeIndent + indentLevel;
301        final Collection<DetailAST> values = firstNodesOnLines.values();
302        final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
303        final int lastAnnotationLine = lastAnnotationNode.getLineNo();
304
305        final Iterator<DetailAST> itr = values.iterator();
306        while (firstNodesOnLines.size() > 1) {
307            final DetailAST node = itr.next();
308
309            final DetailAST parentNode = node.getParent();
310            final boolean isArrayInitPresentInAncestors =
311                isParentContainsTokenType(node, TokenTypes.ANNOTATION_ARRAY_INIT);
312            final boolean isCurrentNodeCloseAnnotationAloneInLine =
313                node.getLineNo() == lastAnnotationLine
314                    && isEndOfScope(lastAnnotationNode, node);
315            if (!isArrayInitPresentInAncestors
316                    && (isCurrentNodeCloseAnnotationAloneInLine
317                    || node.getType() == TokenTypes.AT
318                    && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
319                        || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
320                    || TokenUtil.areOnSameLine(node, atNode))) {
321                logWarningMessage(node, firstNodeIndent);
322            }
323            else if (!isArrayInitPresentInAncestors) {
324                logWarningMessage(node, currentIndent);
325            }
326            itr.remove();
327        }
328    }
329
330    /**
331     * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
332     * the same line.
333     *
334     * @param lastAnnotationNode the last node of the annotation
335     * @param node the node indicating where to begin checking
336     * @return true if all the nodes up to the last annotation node are end of scope nodes
337     *         false otherwise
338     */
339    private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
340        DetailAST checkNode = node;
341        boolean endOfScope = true;
342        while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
343            switch (checkNode.getType()) {
344                case TokenTypes.RCURLY:
345                case TokenTypes.RBRACK:
346                    while (checkNode.getNextSibling() == null) {
347                        checkNode = checkNode.getParent();
348                    }
349                    checkNode = checkNode.getNextSibling();
350                    break;
351                default:
352                    endOfScope = false;
353            }
354        }
355        return endOfScope;
356    }
357
358    /**
359     * Checks that some parent of given node contains given token type.
360     *
361     * @param node node to check
362     * @param type type to look for
363     * @return true if there is a parent of given type
364     */
365    private static boolean isParentContainsTokenType(final DetailAST node, int type) {
366        boolean returnValue = false;
367        for (DetailAST ast = node.getParent(); ast != null; ast = ast.getParent()) {
368            if (ast.getType() == type) {
369                returnValue = true;
370                break;
371            }
372        }
373        return returnValue;
374    }
375
376    /**
377     * Get the column number for the start of a given expression, expanding
378     * tabs out into spaces in the process.
379     *
380     * @param ast   the expression to find the start of
381     *
382     * @return the column number for the start of the expression
383     */
384    private int expandedTabsColumnNo(DetailAST ast) {
385        final String line =
386            indentCheck.getLine(ast.getLineNo() - 1);
387
388        return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
389            indentCheck.getIndentationTabWidth());
390    }
391
392    /**
393     * Get the start of the line for the given expression.
394     *
395     * @param ast   the expression to find the start of the line for
396     *
397     * @return the start of the line for the given expression
398     */
399    private int getLineStart(DetailAST ast) {
400        final String line = indentCheck.getLine(ast.getLineNo() - 1);
401        return getLineStart(line);
402    }
403
404    /**
405     * Get the start of the specified line.
406     *
407     * @param line the specified line number
408     * @return the start of the specified line
409     */
410    private int getLineStart(String line) {
411        int index = 0;
412        while (Character.isWhitespace(line.charAt(index))) {
413            index++;
414        }
415        return CommonUtil.lengthExpandedTabs(line, index, indentCheck.getIndentationTabWidth());
416    }
417
418    /**
419     * Logs warning message if indentation is incorrect.
420     *
421     * @param currentNode
422     *            current node which probably invoked a violation.
423     * @param currentIndent
424     *            correct indentation.
425     */
426    private void logWarningMessage(DetailAST currentNode, int currentIndent) {
427        if (indentCheck.isForceStrictCondition()) {
428            if (expandedTabsColumnNo(currentNode) != currentIndent) {
429                indentCheck.indentationLog(currentNode,
430                        IndentationCheck.MSG_ERROR, currentNode.getText(),
431                        expandedTabsColumnNo(currentNode), currentIndent);
432            }
433        }
434        else {
435            if (expandedTabsColumnNo(currentNode) < currentIndent) {
436                indentCheck.indentationLog(currentNode,
437                        IndentationCheck.MSG_ERROR, currentNode.getText(),
438                        expandedTabsColumnNo(currentNode), currentIndent);
439            }
440        }
441    }
442
443}