001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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;
021
022import java.util.regex.Pattern;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
030
031/**
032 * <p>
033 * The check to ensure that lines with code do not end with comment.
034 * For the case of {@code //} comments that means that the only thing that should precede
035 * it is whitespace. It doesn't check comments if they do not end a line; for example,
036 * it accepts the following: <code>Thread.sleep( 10 /*some comment here&#42;/ );</code>
037 * Format property is intended to deal with the <code>} // while</code> example.
038 * </p>
039 * <p>
040 * Rationale: Steve McConnell in <cite>Code Complete</cite> suggests that endline
041 * comments are a bad practice. An end line comment would be one that is on
042 * the same line as actual code. For example:
043 * </p>
044 * <pre>
045 * a = b + c;      // Some insightful comment
046 * d = e / f;        // Another comment for this line
047 * </pre>
048 * <p>
049 * Quoting <cite>Code Complete</cite> for the justification:
050 * </p>
051 * <ul>
052 * <li>
053 * "The comments have to be aligned so that they do not interfere with the visual
054 * structure of the code. If you don't align them neatly, they'll make your listing
055 * look like it's been through a washing machine."
056 * </li>
057 * <li>
058 * "Endline comments tend to be hard to format...It takes time to align them.
059 * Such time is not spent learning more about the code; it's dedicated solely
060 * to the tedious task of pressing the spacebar or tab key."
061 * </li>
062 * <li>
063 * "Endline comments are also hard to maintain. If the code on any line containing
064 * an endline comment grows, it bumps the comment farther out, and all the other
065 * endline comments will have to bumped out to match. Styles that are hard to
066 * maintain aren't maintained...."
067 * </li>
068 * <li>
069 * "Endline comments also tend to be cryptic. The right side of the line doesn't
070 * offer much room and the desire to keep the comment on one line means the comment
071 * must be short. Work then goes into making the line as short as possible instead
072 * of as clear as possible. The comment usually ends up as cryptic as possible...."
073 * </li>
074 * <li>
075 * "A systemic problem with endline comments is that it's hard to write a meaningful
076 * comment for one line of code. Most endline comments just repeat the line of code,
077 * which hurts more than it helps."
078 * </li>
079 * </ul>
080 * <p>
081 * McConnell's comments on being hard to maintain when the size of the line changes
082 * are even more important in the age of automated refactorings.
083 * </p>
084 * <ul>
085 * <li>
086 * Property {@code format} - Specify pattern for strings allowed before the comment.
087 * Type is {@code java.util.regex.Pattern}.
088 * Default value is <code>"^[\s});]*$"</code>.
089 * </li>
090 * <li>
091 * Property {@code legalComment} - Define pattern for text allowed in trailing comments.
092 * (This pattern will not be applied to multiline comments and the text of
093 * the comment will be trimmed before matching.)
094 * Type is {@code java.util.regex.Pattern}.
095 * Default value is {@code null}.
096 * </li>
097 * </ul>
098 * <p>
099 * To configure the check:
100 * </p>
101 * <pre>
102 * &lt;module name=&quot;TrailingComment&quot;/&gt;
103 * </pre>
104 * <p>
105 * To configure the check so it enforces only comment on a line:
106 * </p>
107 * <pre>
108 * &lt;module name=&quot;TrailingComment&quot;&gt;
109 *   &lt;property name=&quot;format&quot; value=&quot;^\\s*$&quot;/&gt;
110 * &lt;/module&gt;
111 * </pre>
112 *
113 * <p>
114 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
115 * </p>
116 * <p>
117 * Violation Message Keys:
118 * </p>
119 * <ul>
120 * <li>
121 * {@code trailing.comments}
122 * </li>
123 * </ul>
124 *
125 * @noinspection HtmlTagCanBeJavadocTag
126 * @since 3.4
127 */
128@StatelessCheck
129public class TrailingCommentCheck extends AbstractCheck {
130
131    /**
132     * A key is pointing to the warning message text in "messages.properties"
133     * file.
134     */
135    public static final String MSG_KEY = "trailing.comments";
136
137    /** Specify pattern for strings to be formatted without comment specifiers. */
138    private static final Pattern FORMAT_LINE = Pattern.compile("/");
139
140    /**
141     * Define pattern for text allowed in trailing comments.
142     * (This pattern will not be applied to multiline comments and the text
143     * of the comment will be trimmed before matching.)
144     */
145    private Pattern legalComment;
146
147    /** Specify pattern for strings allowed before the comment. */
148    private Pattern format = Pattern.compile("^[\\s});]*$");
149
150    /**
151     * Setter to define pattern for text allowed in trailing comments.
152     * (This pattern will not be applied to multiline comments and the text
153     * of the comment will be trimmed before matching.)
154     *
155     * @param legalComment pattern to set.
156     */
157    public void setLegalComment(final Pattern legalComment) {
158        this.legalComment = legalComment;
159    }
160
161    /**
162     * Setter to specify pattern for strings allowed before the comment.
163     *
164     * @param pattern a pattern
165     */
166    public final void setFormat(Pattern pattern) {
167        format = pattern;
168    }
169
170    @Override
171    public boolean isCommentNodesRequired() {
172        return true;
173    }
174
175    @Override
176    public int[] getDefaultTokens() {
177        return getRequiredTokens();
178    }
179
180    @Override
181    public int[] getAcceptableTokens() {
182        return getRequiredTokens();
183    }
184
185    @Override
186    public int[] getRequiredTokens() {
187        return new int[] {
188            TokenTypes.SINGLE_LINE_COMMENT,
189            TokenTypes.BLOCK_COMMENT_BEGIN,
190        };
191    }
192
193    @Override
194    public void visitToken(DetailAST ast) {
195        if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
196            checkSingleLineComment(ast);
197        }
198        else {
199            checkBlockComment(ast);
200        }
201    }
202
203    /**
204     * Checks if single line comment is legal.
205     *
206     * @param ast Detail ast element to be checked.
207     */
208    private void checkSingleLineComment(DetailAST ast) {
209        final int lineNo = ast.getLineNo();
210        final String comment = ast.getFirstChild().getText();
211        final String line = getLines()[lineNo - 1];
212        final String lineBefore = line.substring(0, ast.getColumnNo());
213
214        if (!format.matcher(lineBefore).find()
215                && !isLegalSingleLineComment(comment)) {
216            log(ast, MSG_KEY);
217        }
218    }
219
220    /**
221     * Method to check if block comment is in correct format.
222     *
223     * @param ast Detail ast element to be checked.
224     */
225    private void checkBlockComment(DetailAST ast) {
226        final int lineNo = ast.getLineNo();
227        final String comment = ast.getFirstChild().getText();
228        String line = getLines()[lineNo - 1];
229
230        if (line.length() > ast.getLastChild().getColumnNo() + 1) {
231            line = line.substring(ast.getLastChild().getColumnNo() + 2);
232        }
233
234        line = FORMAT_LINE.matcher(line).replaceAll("");
235
236        final String lineBefore = getLines()[lineNo - 1].substring(0, ast.getColumnNo());
237
238        // do not check comment which doesn't end line
239        if ((ast.getLineNo() != ast.getLastChild().getLineNo() || CommonUtil.isBlank(line))
240                && !format.matcher(lineBefore).find()
241                && !isLegalBlockComment(ast, comment)) {
242            log(ast, MSG_KEY);
243        }
244    }
245
246    /**
247     * Checks if block comment is legal and matches to the pattern.
248     *
249     * @param ast Detail ast element to be checked.
250     * @param comment comment to check.
251     * @return true if the comment if legal.
252     */
253    private boolean isLegalBlockComment(DetailAST ast, String comment) {
254        final boolean legal;
255
256        // multi-line comment can not be legal
257        if (legalComment == null
258                || !TokenUtil.areOnSameLine(ast.getFirstChild(), ast.getLastChild())) {
259            legal = false;
260        }
261        else {
262            final String commentText = comment.trim();
263            legal = legalComment.matcher(commentText).find();
264        }
265        return legal;
266    }
267
268    /**
269     * Checks if given single line comment is legal (single-line and matches to the
270     * pattern).
271     *
272     * @param comment comment to check.
273     * @return true if the comment if legal.
274     */
275    private boolean isLegalSingleLineComment(String comment) {
276        final boolean legal;
277        if (legalComment == null) {
278            legal = false;
279        }
280        else {
281            // remove chars which start comment
282            final String commentText = comment.substring(1).trim();
283            legal = legalComment.matcher(commentText).find();
284        }
285        return legal;
286    }
287}