001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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;
029
030/**
031 * This class checks line-wrapping into definitions and expressions. The
032 * line-wrapping indentation should be not less then value of the
033 * lineWrappingIndentation parameter.
034 *
035 * @author maxvetrenko
036 *
037 */
038public class LineWrappingHandler {
039
040    /**
041     * A key is pointing to the warning message text in "messages.properties"
042     * file.
043     */
044    private static final String MSG_INDENTATION_ERROR = "indentation.error";
045
046    /**
047     * The current instance of {@code IndentationCheck} class using this
048     * handler. This field used to get access to private fields of
049     * IndentationCheck instance.
050     */
051    private final IndentationCheck indentCheck;
052
053    /**
054     * Root node for current expression.
055     */
056    private final DetailAST firstNode;
057
058    /**
059     * Last node for current expression.
060     */
061    private final DetailAST lastNode;
062
063    /**
064     * User's value of line wrapping indentation.
065     */
066    private final int indentLevel;
067
068    /**
069     * Force strict condition in line wrapping case.
070     */
071    private final boolean forceStrictCondition;
072
073    /**
074     * Sets values of class field, finds last node and calculates indentation level.
075     *
076     * @param instance
077     *            instance of IndentationCheck.
078     * @param firstNode
079     *            root node for current expression.
080     * @param lastNode
081     *            last node for current expression.
082     */
083    public LineWrappingHandler(IndentationCheck instance, DetailAST firstNode, DetailAST lastNode) {
084        indentCheck = instance;
085        this.firstNode = firstNode;
086        this.lastNode = lastNode;
087        indentLevel = indentCheck.getLineWrappingIndentation();
088        forceStrictCondition = indentCheck.isForceStrictCondition();
089    }
090
091    /**
092     *  Getter for lastNode field.
093     *  @return lastNode field
094     */
095    protected final DetailAST getLastNode() {
096        return lastNode;
097    }
098
099    /**
100     * Checks line wrapping into expressions and definitions.
101     */
102    public void checkIndentation() {
103        final NavigableMap<Integer, DetailAST> firstNodesOnLines = collectFirstNodes();
104
105        final DetailAST firstLineNode = firstNodesOnLines.get(firstNodesOnLines.firstKey());
106        if (firstLineNode.getType() == TokenTypes.AT) {
107            checkAnnotationIndentation(firstLineNode, firstNodesOnLines);
108        }
109
110        // First node should be removed because it was already checked before.
111        firstNodesOnLines.remove(firstNodesOnLines.firstKey());
112        final int firstNodeIndent = getFirstNodeIndent(firstLineNode);
113        final int currentIndent = firstNodeIndent + indentLevel;
114
115        for (DetailAST node : firstNodesOnLines.values()) {
116            final int currentType = node.getType();
117
118            if (currentType == TokenTypes.RCURLY
119                    || currentType == TokenTypes.RPAREN
120                    || currentType == TokenTypes.ARRAY_INIT) {
121                logWarningMessage(node, firstNodeIndent);
122            }
123            else {
124                logWarningMessage(node, currentIndent);
125            }
126        }
127    }
128
129    /**
130     * Calculates indentation of first node.
131     *
132     * @param node
133     *            first node.
134     * @return indentation of first node.
135     */
136    private static int getFirstNodeIndent(DetailAST node) {
137        int indentLevel = node.getColumnNo();
138
139        if (node.getType() == TokenTypes.LITERAL_IF
140                && node.getParent().getType() == TokenTypes.LITERAL_ELSE) {
141            final DetailAST lcurly = node.getParent().getPreviousSibling();
142            final DetailAST rcurly = lcurly.getLastChild();
143
144            if (lcurly.getType() == TokenTypes.SLIST
145                    && rcurly.getLineNo() == node.getLineNo()) {
146                indentLevel = rcurly.getColumnNo();
147            }
148            else {
149                indentLevel = node.getParent().getColumnNo();
150            }
151        }
152        return indentLevel;
153    }
154
155    /**
156     * Finds first nodes on line and puts them into Map.
157     *
158     * @return NavigableMap which contains lines numbers as a key and first
159     *         nodes on lines as a values.
160     */
161    private NavigableMap<Integer, DetailAST> collectFirstNodes() {
162        final NavigableMap<Integer, DetailAST> result = new TreeMap<>();
163
164        result.put(firstNode.getLineNo(), firstNode);
165        DetailAST curNode = firstNode.getFirstChild();
166
167        while (curNode != null && curNode != lastNode) {
168
169            if (curNode.getType() == TokenTypes.OBJBLOCK
170                    || curNode.getType() == TokenTypes.SLIST) {
171                curNode = curNode.getNextSibling();
172            }
173
174            if (curNode != null) {
175                final DetailAST firstTokenOnLine = result.get(curNode.getLineNo());
176
177                if (firstTokenOnLine == null
178                    || firstTokenOnLine.getColumnNo() >= curNode.getColumnNo()) {
179                    result.put(curNode.getLineNo(), curNode);
180                }
181                curNode = getNextCurNode(curNode);
182            }
183        }
184        return result;
185    }
186
187    /**
188     * Returns next curNode node.
189     *
190     * @param curNode current node.
191     * @return next curNode node.
192     */
193    private static DetailAST getNextCurNode(DetailAST curNode) {
194        DetailAST nodeToVisit = curNode.getFirstChild();
195        DetailAST currentNode = curNode;
196
197        while (nodeToVisit == null) {
198            nodeToVisit = currentNode.getNextSibling();
199            if (nodeToVisit == null) {
200                currentNode = currentNode.getParent();
201            }
202        }
203        return nodeToVisit;
204    }
205
206    /**
207     * Checks line wrapping into annotations.
208     *
209     * @param atNode at-clause node.
210     * @param firstNodesOnLines map which contains
211     *     first nodes as values and line numbers as keys.
212     */
213    private void checkAnnotationIndentation(DetailAST atNode,
214            NavigableMap<Integer, DetailAST> firstNodesOnLines) {
215        final int currentIndent = atNode.getColumnNo() + indentLevel;
216        final int firstNodeIndent = atNode.getColumnNo();
217        final Collection<DetailAST> values = firstNodesOnLines.values();
218        final DetailAST lastAnnotationNode = getLastAnnotationNode(atNode);
219        final int lastAnnotationLine = lastAnnotationNode.getLineNo();
220
221        final Iterator<DetailAST> itr = values.iterator();
222        while (firstNodesOnLines.size() > 1) {
223            final DetailAST node = itr.next();
224
225            if (node.getLineNo() < lastAnnotationLine
226                    || node.getLineNo() == lastAnnotationLine) {
227                final DetailAST parentNode = node.getParent();
228                if (node.getType() == TokenTypes.AT
229                        && parentNode.getParent().getType() == TokenTypes.MODIFIERS) {
230                    logWarningMessage(node, firstNodeIndent);
231                }
232                else {
233                    logWarningMessage(node, currentIndent);
234                }
235                itr.remove();
236            }
237            else {
238                break;
239            }
240        }
241    }
242
243    /**
244     * Finds and returns last annotation node.
245     * @param atNode first at-clause node.
246     * @return last annotation node.
247     */
248    private static DetailAST getLastAnnotationNode(DetailAST atNode) {
249        DetailAST lastAnnotation = atNode.getParent();
250        while (lastAnnotation.getNextSibling() != null
251                && lastAnnotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
252            lastAnnotation = lastAnnotation.getNextSibling();
253        }
254        return lastAnnotation.getLastChild();
255    }
256
257    /**
258     * Logs warning message if indentation is incorrect.
259     *
260     * @param currentNode
261     *            current node which probably invoked an error.
262     * @param currentIndent
263     *            correct indentation.
264     */
265    private void logWarningMessage(DetailAST currentNode, int currentIndent) {
266        if (forceStrictCondition) {
267            if (currentNode.getColumnNo() != currentIndent) {
268                indentCheck.indentationLog(currentNode.getLineNo(),
269                        MSG_INDENTATION_ERROR, currentNode.getText(),
270                        currentNode.getColumnNo(), currentIndent);
271            }
272        }
273        else {
274            if (currentNode.getColumnNo() < currentIndent) {
275                indentCheck.indentationLog(currentNode.getLineNo(),
276                        MSG_INDENTATION_ERROR, currentNode.getText(),
277                        currentNode.getColumnNo(), currentIndent);
278            }
279        }
280    }
281}