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