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