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