001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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                continue;
169            }
170            if (currentType == TokenTypes.RPAREN) {
171                logWarningMessage(node, firstNodeIndent);
172            }
173            else if (!TokenUtil.isOfType(currentType, IGNORED_LIST)) {
174                logWarningMessage(node, currentIndent);
175            }
176        }
177    }
178
179    /**
180     * Checks for annotation indentation.
181     *
182     * @param firstNodesOnLines the nodes which are present in the beginning of each line.
183     * @param indentLevel line wrapping indentation.
184     */
185    public void checkForAnnotationIndentation(
186            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
187        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
188        DetailAST node = firstLineNode.getParent();
189        while (node != null) {
190            if (node.getType() == TokenTypes.ANNOTATION) {
191                final DetailAST atNode = node.getFirstChild();
192                final NavigableMap<Integer, DetailAST> annotationLines =
193                        firstNodesOnLines.subMap(
194                                node.getLineNo(),
195                                true,
196                                getNextNodeLine(firstNodesOnLines, node),
197                                true
198                        );
199                checkAnnotationIndentation(atNode, annotationLines, indentLevel);
200            }
201            node = node.getNextSibling();
202        }
203    }
204
205    /**
206     * Checks whether parameter node has any child or not.
207     *
208     * @param node the node for which to check.
209     * @return true if  parameter has no child.
210     */
211    public static boolean checkForNullParameterChild(DetailAST node) {
212        return node.getFirstChild() == null && node.getType() == TokenTypes.PARAMETERS;
213    }
214
215    /**
216     * Checks whether the method lparen starts from a new line or not.
217     *
218     * @param node the node for which to check.
219     * @return true if method lparen starts from a new line.
220     */
221    public static boolean checkForMethodLparenNewLine(DetailAST node) {
222        final int parentType = node.getParent().getType();
223        return parentType == TokenTypes.METHOD_DEF && node.getType() == TokenTypes.LPAREN;
224    }
225
226    /**
227     * Gets the next node line from the firstNodesOnLines map unless there is no next line, in
228     * which case, it returns the last line.
229     *
230     * @param firstNodesOnLines NavigableMap of lines and their first nodes.
231     * @param node the node for which to find the next node line
232     * @return the line number of the next line in the map
233     */
234    private static Integer getNextNodeLine(
235            NavigableMap<Integer, DetailAST> firstNodesOnLines, DetailAST node) {
236        Integer nextNodeLine = firstNodesOnLines.higherKey(node.getLastChild().getLineNo());
237        if (nextNodeLine == null) {
238            nextNodeLine = firstNodesOnLines.lastKey();
239        }
240        return nextNodeLine;
241    }
242
243    /**
244     * Finds first nodes on line and puts them into Map.
245     *
246     * @param firstNode First node to start examining.
247     * @param lastNode Last node to examine inclusively.
248     * @return NavigableMap which contains lines numbers as a key and first
249     *         nodes on lines as a values.
250     */
251    private NavigableMap<Integer, DetailAST> collectFirstNodes(DetailAST firstNode,
252            DetailAST lastNode) {
253        final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
254
255        result.put(firstNode.getLineNo(), firstNode);
256        DetailAST curNode = firstNode.getFirstChild();
257
258        while (curNode != lastNode) {
259            if (curNode.getType() == TokenTypes.OBJBLOCK
260                    || curNode.getType() == TokenTypes.SLIST) {
261                curNode = curNode.getLastChild();
262            }
263
264            final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
265
266            if (firstTokenOnLine == null
267                || expandedTabsColumnNo(firstTokenOnLine) >= expandedTabsColumnNo(curNode)) {
268                result.put(curNode.getLineNo(), curNode);
269            }
270            curNode = getNextCurNode(curNode);
271        }
272        return result;
273    }
274
275    /**
276     * Returns next curNode node.
277     *
278     * @param curNode current node.
279     * @return next curNode node.
280     */
281    private static DetailAST getNextCurNode(DetailAST curNode) {
282        DetailAST nodeToVisit = curNode.getFirstChild();
283        DetailAST currentNode = curNode;
284
285        while (nodeToVisit == null) {
286            nodeToVisit = currentNode.getNextSibling();
287            if (nodeToVisit == null) {
288                currentNode = currentNode.getParent();
289            }
290        }
291        return nodeToVisit;
292    }
293
294    /**
295     * Checks line wrapping into annotations.
296     *
297     * @param atNode block tag node.
298     * @param firstNodesOnLines map which contains
299     *     first nodes as values and line numbers as keys.
300     * @param indentLevel line wrapping indentation.
301     */
302    private void checkAnnotationIndentation(DetailAST atNode,
303            NavigableMap<Integer, DetailAST> firstNodesOnLines, int indentLevel) {
304        final int firstNodeIndent = getLineStart(atNode);
305        final int currentIndent = firstNodeIndent + indentLevel;
306        final Collection<DetailAST> values = firstNodesOnLines.values();
307        final DetailAST lastAnnotationNode = atNode.getParent().getLastChild();
308        final int lastAnnotationLine = lastAnnotationNode.getLineNo();
309
310        final Iterator<DetailAST> itr = values.iterator();
311        while (firstNodesOnLines.size() > 1) {
312            final DetailAST node = itr.next();
313
314            final DetailAST parentNode = node.getParent();
315            final boolean isArrayInitPresentInAncestors =
316                isParentContainsTokenType(node, TokenTypes.ANNOTATION_ARRAY_INIT);
317            final boolean isCurrentNodeCloseAnnotationAloneInLine =
318                node.getLineNo() == lastAnnotationLine
319                    && isEndOfScope(lastAnnotationNode, node);
320            if (!isArrayInitPresentInAncestors
321                    && (isCurrentNodeCloseAnnotationAloneInLine
322                    || node.getType() == TokenTypes.AT
323                    && (parentNode.getParent().getType() == TokenTypes.MODIFIERS
324                        || parentNode.getParent().getType() == TokenTypes.ANNOTATIONS)
325                    || TokenUtil.areOnSameLine(node, atNode))) {
326                logWarningMessage(node, firstNodeIndent);
327            }
328            else if (!isArrayInitPresentInAncestors) {
329                logWarningMessage(node, currentIndent);
330            }
331            itr.remove();
332        }
333    }
334
335    /**
336     * Checks line for end of scope.  Handles occurrences of close braces and close parenthesis on
337     * the same line.
338     *
339     * @param lastAnnotationNode the last node of the annotation
340     * @param node the node indicating where to begin checking
341     * @return true if all the nodes up to the last annotation node are end of scope nodes
342     *         false otherwise
343     */
344    private static boolean isEndOfScope(final DetailAST lastAnnotationNode, final DetailAST node) {
345        DetailAST checkNode = node;
346        boolean endOfScope = true;
347        while (endOfScope && !checkNode.equals(lastAnnotationNode)) {
348            switch (checkNode.getType()) {
349                case TokenTypes.RCURLY:
350                case TokenTypes.RBRACK:
351                    while (checkNode.getNextSibling() == null) {
352                        checkNode = checkNode.getParent();
353                    }
354                    checkNode = checkNode.getNextSibling();
355                    break;
356                default:
357                    endOfScope = false;
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            indentCheck.getLine(ast.getLineNo() - 1);
392
393        return CommonUtil.lengthExpandedTabs(line, ast.getColumnNo(),
394            indentCheck.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 = indentCheck.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, indentCheck.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 (indentCheck.isForceStrictCondition()) {
433            if (expandedTabsColumnNo(currentNode) != currentIndent) {
434                indentCheck.indentationLog(currentNode,
435                        IndentationCheck.MSG_ERROR, currentNode.getText(),
436                        expandedTabsColumnNo(currentNode), currentIndent);
437            }
438        }
439        else {
440            if (expandedTabsColumnNo(currentNode) < currentIndent) {
441                indentCheck.indentationLog(currentNode,
442                        IndentationCheck.MSG_ERROR, currentNode.getText(),
443                        expandedTabsColumnNo(currentNode), currentIndent);
444            }
445        }
446    }
447
448}