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