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