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