001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.
093 * Type is {@code java.util.regex.Pattern}.
094 * Default value is {@code null}.
095 * </li>
096 * </ul>
097 * <p>
098 * To configure the check:
099 * </p>
100 * <pre>
101 * &lt;module name=&quot;TrailingComment&quot;/&gt;
102 * </pre>
103 * <p>
104 * To configure the check so it enforces only comment on a line:
105 * </p>
106 * <pre>
107 * &lt;module name=&quot;TrailingComment&quot;&gt;
108 *   &lt;property name=&quot;format&quot; value=&quot;^\\s*$&quot;/&gt;
109 * &lt;/module&gt;
110 * </pre>
111 * <p>
112 * Example for trailing comments check to suppress specific trailing comment:
113 * </p>
114 * <pre>
115 * public class Test {
116 *   int a; // SUPPRESS CHECKSTYLE
117 *   int b; // NOPMD
118 *   int c; // NOSONAR
119 *   int d; // violation, not suppressed
120 * }
121 * </pre>
122 * <p>
123 * To configure check so that trailing comment with exact comments like "SUPPRESS CHECKSTYLE",
124 * "NOPMD", "NOSONAR" are suppressed:
125 * </p>
126 * <pre>
127 * &lt;module name="TrailingComment"/&gt;
128 * &lt;module name="SuppressionXpathSingleFilter"&gt;
129 *   &lt;property name="checks" value="TrailingCommentCheck"/&gt;
130 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
131 *       [./COMMENT_CONTENT[@text=' NOSONAR\n' or @text=' NOPMD\n'
132 *       or @text=' SUPPRESS CHECKSTYLE\n']]"/&gt;
133 * &lt;/module&gt;
134 * </pre>
135 * <p>
136 * To configure check so that trailing comment starting with "SUPPRESS CHECKSTYLE", "NOPMD",
137 * "NOSONAR" are suppressed:
138 * </p>
139 * <pre>
140 * &lt;module name="TrailingComment"/&gt; &lt;module name="SuppressionXpathSingleFilter"&gt;
141 * &lt;property name="checks" value="TrailingCommentCheck"/&gt;
142 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
143 *       [./COMMENT_CONTENT[starts-with(@text, ' NOPMD')]]"/&gt;
144 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
145 *       [./COMMENT_CONTENT[starts-with(@text, ' SUPPRESS CHECKSTYLE')]]"/&gt;
146 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
147 *       [./COMMENT_CONTENT[starts-with(@text, ' NOSONAR')]]"/&gt;
148 * &lt;/module&gt;
149 * </pre>
150 * <p>
151 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
152 * </p>
153 * <p>
154 * Violation Message Keys:
155 * </p>
156 * <ul>
157 * <li>
158 * {@code trailing.comments}
159 * </li>
160 * </ul>
161 *
162 * @noinspection HtmlTagCanBeJavadocTag
163 * @since 3.4
164 */
165@StatelessCheck
166public class TrailingCommentCheck extends AbstractCheck {
167
168    /**
169     * A key is pointing to the warning message text in "messages.properties"
170     * file.
171     */
172    public static final String MSG_KEY = "trailing.comments";
173
174    /** Specify pattern for strings to be formatted without comment specifiers. */
175    private static final Pattern FORMAT_LINE = Pattern.compile("/");
176
177    /**
178     * Define pattern for text allowed in trailing comments.
179     * This pattern will not be applied to multiline comments.
180     */
181    private Pattern legalComment;
182
183    /** Specify pattern for strings allowed before the comment. */
184    private Pattern format = Pattern.compile("^[\\s});]*$");
185
186    /**
187     * Setter to define pattern for text allowed in trailing comments.
188     * This pattern will not be applied to multiline comments.
189     *
190     * @param legalComment pattern to set.
191     */
192    public void setLegalComment(final Pattern legalComment) {
193        this.legalComment = legalComment;
194    }
195
196    /**
197     * Setter to specify pattern for strings allowed before the comment.
198     *
199     * @param pattern a pattern
200     */
201    public final void setFormat(Pattern pattern) {
202        format = pattern;
203    }
204
205    @Override
206    public boolean isCommentNodesRequired() {
207        return true;
208    }
209
210    @Override
211    public int[] getDefaultTokens() {
212        return getRequiredTokens();
213    }
214
215    @Override
216    public int[] getAcceptableTokens() {
217        return getRequiredTokens();
218    }
219
220    @Override
221    public int[] getRequiredTokens() {
222        return new int[] {
223            TokenTypes.SINGLE_LINE_COMMENT,
224            TokenTypes.BLOCK_COMMENT_BEGIN,
225        };
226    }
227
228    @Override
229    public void visitToken(DetailAST ast) {
230        if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
231            checkSingleLineComment(ast);
232        }
233        else {
234            checkBlockComment(ast);
235        }
236    }
237
238    /**
239     * Checks if single line comment is legal.
240     *
241     * @param ast Detail ast element to be checked.
242     */
243    private void checkSingleLineComment(DetailAST ast) {
244        final int lineNo = ast.getLineNo();
245        final String comment = ast.getFirstChild().getText();
246        final String line = getLines()[lineNo - 1];
247        final String lineBefore = line.substring(0, ast.getColumnNo());
248
249        if (!format.matcher(lineBefore).find() && !isLegalCommentContent(comment)) {
250            log(ast, MSG_KEY);
251        }
252    }
253
254    /**
255     * Method to check if block comment is in correct format.
256     *
257     * @param ast Detail ast element to be checked.
258     */
259    private void checkBlockComment(DetailAST ast) {
260        final int lineNo = ast.getLineNo();
261        final DetailAST firstChild = ast.getFirstChild();
262        final DetailAST lastChild = ast.getLastChild();
263        final String comment = firstChild.getText();
264        String line = getLines()[lineNo - 1];
265
266        if (line.length() > lastChild.getColumnNo() + 1) {
267            line = line.substring(lastChild.getColumnNo() + 2);
268        }
269
270        line = FORMAT_LINE.matcher(line).replaceAll("");
271
272        final String lineBefore = getLines()[lineNo - 1].substring(0, ast.getColumnNo());
273        final boolean isCommentAtEndOfLine = ast.getLineNo() != lastChild.getLineNo()
274                || CommonUtil.isBlank(line);
275        final boolean isLegalBlockComment = isLegalCommentContent(comment)
276                && TokenUtil.areOnSameLine(firstChild, lastChild)
277                || format.matcher(lineBefore).find();
278
279        if (isCommentAtEndOfLine && !isLegalBlockComment) {
280            log(ast, MSG_KEY);
281        }
282    }
283
284    /**
285     * Checks if given comment content is legal.
286     *
287     * @param commentContent comment content to check.
288     * @return true if the content is legal.
289     */
290    private boolean isLegalCommentContent(String commentContent) {
291        return legalComment != null && legalComment.matcher(commentContent).find();
292    }
293}