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 org.apache.commons.lang3.ArrayUtils;
023
024import com.puppycrawl.tools.checkstyle.api.Check;
025import com.puppycrawl.tools.checkstyle.api.DetailAST;
026import com.puppycrawl.tools.checkstyle.api.TokenTypes;
027import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
028
029/**
030 * This Check controls the indentation between comments and surrounding code.
031 * Comments are indented at the same level as the surrounding code.
032 * Detailed info about such convention can be found
033 * <a href=
034 * "http://checkstyle.sourceforge.net/reports/google-java-style.html#s4.8.6.1-block-comment-style">
035 * here</a>
036 * <p>
037 * Examples:
038 * </p>
039 * <p>
040 * To configure the Check:
041 * </p>
042 *
043 * <pre>
044 * {@code
045 * &lt;module name=&quot;CommentsIndentation&quot;/module&gt;
046 * }
047 * {@code
048 * /*
049 *  * comment
050 *  * some comment
051 *  *&#47;
052 * boolean bool = true; - such comment indentation is ok
053 *    /*
054 *    * comment
055 *    * some comment
056 *     *&#47;
057 * double d = 3.14; - Block Comment has incorrect indentation level 7, expected 4.
058 * // some comment - comment is ok
059 * String str = "";
060 *     // some comment Comment has incorrect indentation level 8, expected 4.
061 * String str1 = "";
062 * }
063 * </pre>
064 *
065 *
066 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
067 * @author <a href="mailto:andreyselkin@gmail.com">Andrei Selkin</a>
068 *
069 */
070public class CommentsIndentationCheck extends Check {
071
072    /**
073     * A key is pointing to the warning message text in "messages.properties"
074     * file.
075     */
076    public static final String MSG_KEY_SINGLE = "comments.indentation.single";
077
078    /**
079     * A key is pointing to the warning message text in "messages.properties"
080     * file.
081     */
082    public static final String MSG_KEY_BLOCK = "comments.indentation.block";
083
084    @Override
085    public int[] getDefaultTokens() {
086        return new int[] {
087            TokenTypes.SINGLE_LINE_COMMENT,
088            TokenTypes.BLOCK_COMMENT_BEGIN,
089        };
090    }
091
092    @Override
093    public int[] getAcceptableTokens() {
094        return new int[] {
095            TokenTypes.SINGLE_LINE_COMMENT,
096            TokenTypes.BLOCK_COMMENT_BEGIN,
097        };
098    }
099
100    @Override
101    public int[] getRequiredTokens() {
102        return ArrayUtils.EMPTY_INT_ARRAY;
103    }
104
105    @Override
106    public boolean isCommentNodesRequired() {
107        return true;
108    }
109
110    @Override
111    public void visitToken(DetailAST commentAst) {
112        switch (commentAst.getType()) {
113            case TokenTypes.SINGLE_LINE_COMMENT:
114                visitSingleLineComment(commentAst);
115                break;
116            case TokenTypes.BLOCK_COMMENT_BEGIN:
117                visitBlockComment(commentAst);
118                break;
119            default:
120                final String exceptionMsg = "Unexpected token type: " + commentAst.getText();
121                throw new IllegalArgumentException(exceptionMsg);
122        }
123    }
124
125    /**
126     * Checks single line comment indentations over surrounding code, e.g.:
127     * <p>
128     * {@code
129     * // some comment - this is ok
130     * double d = 3.14;
131     *     // some comment - this is <b>not</b> ok.
132     * double d1 = 5.0;
133     * }
134     * </p>
135     * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}.
136     */
137    private void visitSingleLineComment(DetailAST singleLineComment) {
138        final DetailAST nextStatement = singleLineComment.getNextSibling();
139        final DetailAST prevStatement = getPrevStatementFromSwitchBlock(singleLineComment);
140
141        if (nextStatement != null
142            && nextStatement.getType() != TokenTypes.RCURLY
143            && !isTrailingSingleLineComment(singleLineComment)
144            && !areSameLevelIndented(singleLineComment, prevStatement, nextStatement)) {
145
146            log(singleLineComment.getLineNo(), MSG_KEY_SINGLE, nextStatement.getLineNo(),
147                singleLineComment.getColumnNo(), nextStatement.getColumnNo());
148        }
149    }
150
151    /**
152     * Gets comment's previous statement from switch block.
153     * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
154     * @return comment's previous statement or null if previous statement is absent.
155     */
156    private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) {
157        DetailAST prevStmt = null;
158        final DetailAST parentStatement = comment.getParent();
159        if (parentStatement != null) {
160            if (parentStatement.getType() == TokenTypes.CASE_GROUP) {
161                prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement);
162            }
163            else {
164                prevStmt = getPrevCaseToken(parentStatement);
165            }
166        }
167        return prevStmt;
168    }
169
170    /**
171     * Gets previous statement for comment which is placed immediately under case.
172     * @param parentStatement comment's parent statement.
173     * @return comment's previous statement or null if previous statement is absent.
174     */
175    private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) {
176        DetailAST prevStmt = null;
177        final DetailAST prevBlock = parentStatement.getPreviousSibling();
178        if (prevBlock.getLastChild() != null) {
179            DetailAST blockBody = prevBlock.getLastChild().getLastChild();
180            if (blockBody.getPreviousSibling() != null) {
181                blockBody = blockBody.getPreviousSibling();
182            }
183            if (blockBody.getType() == TokenTypes.EXPR) {
184                prevStmt = blockBody.getFirstChild().getFirstChild();
185            }
186            else {
187                prevStmt = blockBody;
188            }
189        }
190        return prevStmt;
191    }
192
193    /**
194     * Gets previous case-token for comment.
195     * @param parentStatement comment's parent statement.
196     * @return previous case-token or null if previous case-token is absent.
197     */
198    private static DetailAST getPrevCaseToken(DetailAST parentStatement) {
199        final DetailAST prevCaseToken;
200        final DetailAST parentBlock = parentStatement.getParent();
201        if (parentBlock != null && parentBlock.getParent() != null
202            && parentBlock.getParent().getPreviousSibling() != null
203            && parentBlock.getParent().getPreviousSibling()
204                .getType() == TokenTypes.LITERAL_CASE) {
205
206            prevCaseToken = parentBlock.getParent().getPreviousSibling();
207        }
208        else {
209            prevCaseToken = null;
210        }
211        return prevCaseToken;
212    }
213
214    /**
215     * Checks if comment and next code statement
216     * (or previous code stmt like <b>case</b> in switch block) are indented at the same level,
217     * e.g.:
218     * <p>
219     * <pre>
220     * {@code
221     * // some comment - same indentation level
222     * int x = 10;
223     *     // some comment - different indentation level
224     * int x1 = 5;
225     * /*
226     *  *
227     *  *&#47;
228     *  boolean bool = true; - same indentation level
229     * }
230     * </pre>
231     * </p>
232     * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}.
233     * @param prevStmt previous code statement.
234     * @param nextStmt next code statement.
235     * @return true if comment and next code statement are indented at the same level.
236     */
237    private static boolean areSameLevelIndented(DetailAST singleLineComment,
238                                                DetailAST prevStmt, DetailAST nextStmt) {
239        boolean result;
240        if (prevStmt == null) {
241            result = singleLineComment.getColumnNo() == nextStmt.getColumnNo();
242        }
243        else {
244            result = singleLineComment.getColumnNo() == nextStmt.getColumnNo()
245                || singleLineComment.getColumnNo() == prevStmt.getColumnNo();
246        }
247        return result;
248    }
249
250    /**
251     * Checks if current single line comment is trailing comment, e.g.:
252     * <p>
253     * {@code
254     * double d = 3.14; // some comment
255     * }
256     * </p>
257     * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}.
258     * @return true if current single line comment is trailing comment.
259     */
260    private boolean isTrailingSingleLineComment(DetailAST singleLineComment) {
261        final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1);
262        final int commentColumnNo = singleLineComment.getColumnNo();
263        return !CommonUtils.hasWhitespaceBefore(commentColumnNo, targetSourceLine);
264    }
265
266    /**
267     * Checks comment block indentations over surrounding code, e.g.:
268     * <p>
269     * {@code
270     * /* some comment *&#47; - this is ok
271     * double d = 3.14;
272     *     /* some comment *&#47; - this is <b>not</b> ok.
273     * double d1 = 5.0;
274     * }
275     * </p>
276     * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}.
277     */
278    private void visitBlockComment(DetailAST blockComment) {
279        final DetailAST nextStatement = blockComment.getNextSibling();
280        final DetailAST prevStatement = getPrevStatementFromSwitchBlock(blockComment);
281
282        if (nextStatement != null
283            && nextStatement.getType() != TokenTypes.RCURLY
284            && !isTrailingBlockComment(blockComment)
285            && !areSameLevelIndented(blockComment, prevStatement, nextStatement)) {
286
287            log(blockComment.getLineNo(), MSG_KEY_BLOCK, nextStatement.getLineNo(),
288                blockComment.getColumnNo(), nextStatement.getColumnNo());
289        }
290    }
291
292    /**
293     * Checks if current comment block is trailing comment, e.g.:
294     * <p>
295     * {@code
296     * double d = 3.14; /* some comment *&#47;
297     * /* some comment *&#47; double d = 18.5;
298     * }
299     * </p>
300     * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}.
301     * @return true if current comment block is trailing comment.
302     */
303    private boolean isTrailingBlockComment(DetailAST blockComment) {
304        final String commentLine = getLine(blockComment.getLineNo() - 1);
305        final int commentColumnNo = blockComment.getColumnNo();
306        return !CommonUtils.hasWhitespaceBefore(commentColumnNo, commentLine)
307            || blockComment.getNextSibling().getLineNo() == blockComment.getLineNo();
308    }
309}