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