001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 lineWrappingIndentation parameter.
035 *
036 */
037public class LineWrappingHandler {
038
039    /**
040     * Enum to be used for test if first line's indentation should be checked or not.
041     */
042    public enum LineWrappingOptions {
043
044        /**
045         * First line's indentation should NOT be checked.
046         */
047        IGNORE_FIRST_LINE,
048        /**
049         * First line's indentation should be checked.
050         */
051        NONE
052
053    }
054
055    /**
056     * The list of ignored token types for being checked by lineWrapping indentation
057     * inside {@code checkIndentation()} as these tokens are checked for lineWrapping
058     * inside their dedicated handlers.
059     *
060     * @see NewHandler#getIndentImpl()
061     * @see BlockParentHandler#curlyIndent()
062     * @see ArrayInitHandler#getIndentImpl()
063     * @see CaseHandler#getIndentImpl()
064     */
065    private static final int[] IGNORED_LIST = {
066        TokenTypes.LCURLY,
067        TokenTypes.RCURLY,
068        TokenTypes.LITERAL_NEW,
069        TokenTypes.LITERAL_YIELD,
070        TokenTypes.ARRAY_INIT,
071        TokenTypes.LITERAL_DEFAULT,
072        TokenTypes.LITERAL_CASE,
073    };
074
075    /**
076     * The indentation context. Set lazily via {@link #setContext} because the
077     * context and this handler are created together at {@code beginTree} time.
078     */
079    private IndentationContext context;
080
081    /** Default constructor. Call {@link #setContext} before use. */
082    public LineWrappingHandler() {
083        // context is injected via setContext to break construction cycle with
084        // IndentationContext, which needs a LineWrappingHandler reference.
085    }
086
087    /**
088     * Injects the indentation context. Must be called before any check method.
089     *
090     * @param indentationContext the indentation context to use
091     */
092    /* package */ void setContext(IndentationContext indentationContext) {
093        context = indentationContext;
094    }
095
096    /**
097     * Checks line wrapping into expressions and definitions using property
098     * 'lineWrappingIndentation'.
099     *
100     * @param firstNode First node to start examining.
101     * @param lastNode Last node to examine inclusively.
102     */
103    public void checkIndentation(DetailAST firstNode, DetailAST lastNode) {
104        checkIndentation(firstNode, lastNode, context.getLineWrappingIndentation());
105    }
106
107    /**
108     * Checks line wrapping into expressions and definitions.
109     *
110     * @param firstNode First node to start examining.
111     * @param lastNode Last node to examine inclusively.
112     * @param indentLevel Indentation all wrapped lines should use.
113     */
114    private void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel) {
115        checkIndentation(firstNode, lastNode, indentLevel,
116                -1, LineWrappingOptions.IGNORE_FIRST_LINE);
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     * @param startIndent Indentation first line before wrapped lines used.
126     * @param ignoreFirstLine Test if first line's indentation should be checked or not.
127     */
128    public void checkIndentation(DetailAST firstNode, DetailAST lastNode, int indentLevel,
129            int startIndent, LineWrappingOptions ignoreFirstLine) {
130        final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes(firstNode,
131                lastNode);
132
133        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
134        if (firstLineNode.getType() == TokenTypes.AT) {
135            checkForAnnotationIndentation(firstNodesOnLines, indentLevel);
136        }
137
138        if (ignoreFirstLine == LineWrappingOptions.IGNORE_FIRST_LINE) {
139            // First node should be removed because it was already checked before.
140            firstNodesOnLines.remove(firstNodesOnLines.firstKey());
141        }
142
143        final int firstNodeIndent;
144        if (startIndent == -1) {
145            firstNodeIndent = getLineStart(firstLineNode);
146        }
147        else {
148            firstNodeIndent = startIndent;
149        }
150        final int currentIndent = firstNodeIndent + indentLevel;
151
152        for (DetailAST node : firstNodesOnLines.values()) {
153            checkNodeIndentation(node, firstNodeIndent, currentIndent);
154        }
155    }
156
157    /**
158     * Checks whether the indentation of the node on the wrapped line is correct.
159     *
160     * @param node the node for which to check.
161     * @param firstNodeIndent indentation first line before wrapped lines used.
162     * @param currentIndent indentation all wrapped lines should use.
163     */
164    private void checkNodeIndentation(DetailAST node, int firstNodeIndent, int currentIndent) {
165        if (!checkForNullParameterChild(node) && !checkForMethodLparenNewLine(node)
166                && shouldProcessTextBlockLiteral(node)) {
167            final int currentType = node.getType();
168            if (currentType == TokenTypes.RPAREN) {
169                if (node.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF) {
170                    logWarningMessage(node, currentIndent);
171                }
172                else {
173                    logWarningMessage(node, firstNodeIndent);
174                }
175            }
176            else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) {
177                logWarningMessage(node, currentIndent);
178            }
179        }
180    }
181
182    /**
183     * Checks for annotation indentation.
184     *
185     * @param firstNodesOnLines the nodes which are present in the beginning of each line.
186     * @param indentLevel line wrapping indentation.
187     */
188    public void checkForAnnotationIndentation(
189            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
190        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
191        DetailAST node = firstLineNode.getParent();
192        while (node != null) {
193            if (node.getType() == TokenTypes.ANNOTATION) {
194                final DetailAST atNode = node.getFirstChild();
195                final NavigableMap<Integer, DetailAST> annotationLines =
196                        firstNodesOnLines.subMap(
197                                node.getLineNo(),
198                                true,
199                                getNextNodeLine(firstNodesOnLines, node),
200                                true
201                        );
202                checkAnnotationIndentation(atNode, annotationLines, indentLevel);
203            }
204            node = node.getNextSibling();
205        }
206    }
207
208    /**
209     * Checks whether parameter node has any child or not.
210     *
211     * @param node the node for which to check.
212     * @return true if  parameter has no child.
213     */
214    public static boolean checkForNullParameterChild(DetailAST node) {
215        return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS;
216    }
217
218    /**
219     * Checks whether the method lparen starts from a new line or not.
220     *
221     * @param node the node for which to check.
222     * @return true if method lparen starts from a new line.
223     */
224    public static boolean checkForMethodLparenNewLine(DetailAST node) {
225        final int parentType = node.getParent().getType();
226        return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN;
227    }
228
229    /**
230     * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
231     * which case, it returns the last line.
232     *
233     * @param firstNodesOnLines NavigableMap of lines and their first nodes.
234     * @param node the node for which to find the next node line
235     * @return the line number of the next line in the map
236     */
237    private static Integer getNextNodeLine(
238            NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
239        Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
240        if (nextNodeLine == null) {
241            nextNodeLine = firstNodesOnLines.lastKey();
242        }
243        return nextNodeLine;
244    }
245
246    /**
247     * Finds first nodes on line and puts them into Map.
248     *
249     * @param firstNode First node to start examining.
250     * @param lastNode Last node to examine inclusively.
251     * @return NavigableMap which contains lines numbers as a key and first
252     *         nodes on lines as a values.
253     */
254    private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
255            DetailAST lastNode) {
256        final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
257
258        result.put(firstNode.getLineNo(), firstNode);
259        DetailAST curNode = firstNode.getFirstChild();
260
261        while (curNode != lastNode) {
262            if (curNode.getType() == TokenTypes.OBJBLOCK
263                    || curNode.getType() == TokenTypes.SLIST) {
264                curNode = curNode.getLastChild();
265            }
266
267            final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
268
269            if (firstTokenOnLine == null
270                || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
271                result.put(curNode.getLineNo(), curNode);
272            }
273            curNode = getNextCurNode(curNode);
274        }
275        return result;
276    }
277
278    /**
279     * Checks whether indentation of {@code TEXT_BLOCK_LITERAL_END}
280     *     needs to be checked. Yes if it is first on start of the line.
281     *
282     * @param node the node
283     * @return true if node is line-starting node.
284     */
285    private boolean shouldProcessTextBlockLiteral(DetailAST node) {
286        return node.getType() != TokenTypes.TEXT_BLOCK_LITERAL_END
287                || expandedTabsColumnNo(node) == getLineStart(node);
288    }
289
290    /**
291     * Returns next curNode node.
292     *
293     * @param curNode current node.
294     * @return next curNode node.
295     */
296    private static DetailAST getNextCurNode(DetailAST curNode) {
297        DetailAST nodeToVisit = curNode.getFirstChild();
298        DetailAST currentNode = curNode;
299
300        while (nodeToVisit == null) {
301            nodeToVisit = currentNode.getNextSibling();
302            if (nodeToVisit == null) {
303                currentNode = currentNode.getParent();
304            }
305        }
306        return nodeToVisit;
307    }
308
309    /**
310     * Checks line wrapping into annotations.
311     *
312     * @param atNode block tag node.
313     * @param firstNodesOnLines map which contains 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, TokenTypes.RBRACK -> {
364                    while (checkNode.getNextSibling() == null) {
365                        checkNode = checkNode.getParent();
366                    }
367                    checkNode = checkNode.getNextSibling();
368                }
369                default -> endOfScope = false;
370            }
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            context.getLine(ast.getLineNo() - 1);
405
406        return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
407            context.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 = context.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, context.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 (context.isForceStrictCondition()) {
446            if (expandedTabsColumnNo(currentNode) != currentIndent) {
447                context.indentationLog(currentNode,
448                        IndentationContext.MSG_ERROR, currentNode.getText(),
449                        expandedTabsColumnNo(currentNode), currentIndent);
450            }
451        }
452        else {
453            if (expandedTabsColumnNo(currentNode) < currentIndent) {
454                context.indentationLog(currentNode,
455                        IndentationContext.MSG_ERROR, currentNode.getText(),
456                        expandedTabsColumnNo(currentNode), currentIndent);
457            }
458        }
459    }
460
461}