001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files 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 * Example:
106 * </p>
107 * <pre>
108 * // OK
109 * if (&#47;&#42; OK &#42;&#47; x &#62; 5) {}
110 * int a = 5; // violation
111 * doSomething(
112 *   param1
113 * ); // OK, by default such trailing of method/code-block ending is allowed
114 * </pre>
115 * <p>
116 * To configure the check to enforce only comment on a line:
117 * </p>
118 * <pre>
119 * &lt;module name=&quot;TrailingComment&quot;&gt;
120 *   &lt;property name=&quot;format&quot; value=&quot;^\s*$&quot;/&gt;
121 * &lt;/module&gt;
122 * </pre>
123 * <p>
124 * Example:
125 * </p>
126 * <pre>
127 * // OK
128 * if (&#47;&#42; OK, this comment does not end the line &#42;&#47; x &#62; 5) {}
129 * int a = 5; // violation, line content before comment should match pattern "^\s*$"
130 * doSomething(
131 *   param1
132 * ); // violation, line content before comment should match pattern "^\s*$"
133 * </pre>
134 * <p>
135 * To configure check so that trailing comment with exact comments like "SUPPRESS CHECKSTYLE",
136 * "NOPMD", "NOSONAR" are suppressed:
137 * </p>
138 * <pre>
139 * &lt;module name="TrailingComment"/&gt;
140 * &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[@text=' NOSONAR\n' or @text=' NOPMD\n'
144 *       or @text=' SUPPRESS CHECKSTYLE\n']]"/&gt;
145 * &lt;/module&gt;
146 * </pre>
147 * <p>
148 * Example for trailing comments check to suppress specific trailing comment:
149 * </p>
150 * <pre>
151 * public class Test {
152 *   int a; // SUPPRESS CHECKSTYLE
153 *   int b; // NOPMD
154 *   int c; // NOSONAR
155 *   int d; // violation, not suppressed
156 * }
157 * </pre>
158 * <p>
159 * To configure check so that trailing comment starting with "SUPPRESS CHECKSTYLE", "NOPMD",
160 * "NOSONAR" are suppressed:
161 * </p>
162 * <pre>
163 * &lt;module name="TrailingComment"/&gt; &lt;module name="SuppressionXpathSingleFilter"&gt;
164 * &lt;property name="checks" value="TrailingCommentCheck"/&gt;
165 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
166 *       [./COMMENT_CONTENT[starts-with(@text, ' NOPMD')]]"/&gt;
167 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
168 *       [./COMMENT_CONTENT[starts-with(@text, ' SUPPRESS CHECKSTYLE')]]"/&gt;
169 *   &lt;property name="query" value="//SINGLE_LINE_COMMENT
170 *       [./COMMENT_CONTENT[starts-with(@text, ' NOSONAR')]]"/&gt;
171 * &lt;/module&gt;
172 * </pre>
173 * <p>
174 * Example:
175 * </p>
176 * <pre>
177 * public class Test {
178 *   int a; // SUPPRESS CHECKSTYLE - OK, comment starts with " SUPPRESS CHECKSTYLE"
179 *   int b; // NOPMD - OK, comment starts with " NOPMD"
180 *   int c; // NOSONAR - OK, comment starts with " NOSONAR"
181 *   int d; // violation, not suppressed
182 * }
183 * </pre>
184 * <p>
185 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
186 * </p>
187 * <p>
188 * Violation Message Keys:
189 * </p>
190 * <ul>
191 * <li>
192 * {@code trailing.comments}
193 * </li>
194 * </ul>
195 *
196 * @noinspection HtmlTagCanBeJavadocTag
197 * @noinspectionreason HtmlTagCanBeJavadocTag - encoded symbols were not decoded
198 *      when replaced with Javadoc tag
199 * @since 3.4
200 */
201@StatelessCheck
202public class TrailingCommentCheck extends AbstractCheck {
203
204    /**
205     * A key is pointing to the warning message text in "messages.properties"
206     * file.
207     */
208    public static final String MSG_KEY = "trailing.comments";
209
210    /** Specify pattern for strings to be formatted without comment specifiers. */
211    private static final Pattern FORMAT_LINE = Pattern.compile("/");
212
213    /**
214     * Define pattern for text allowed in trailing comments.
215     * This pattern will not be applied to multiline comments.
216     */
217    private Pattern legalComment;
218
219    /** Specify pattern for strings allowed before the comment. */
220    private Pattern format = Pattern.compile("^[\\s});]*$");
221
222    /**
223     * Setter to define pattern for text allowed in trailing comments.
224     * This pattern will not be applied to multiline comments.
225     *
226     * @param legalComment pattern to set.
227     */
228    public void setLegalComment(final Pattern legalComment) {
229        this.legalComment = legalComment;
230    }
231
232    /**
233     * Setter to specify pattern for strings allowed before the comment.
234     *
235     * @param pattern a pattern
236     */
237    public final void setFormat(Pattern pattern) {
238        format = pattern;
239    }
240
241    @Override
242    public boolean isCommentNodesRequired() {
243        return true;
244    }
245
246    @Override
247    public int[] getDefaultTokens() {
248        return getRequiredTokens();
249    }
250
251    @Override
252    public int[] getAcceptableTokens() {
253        return getRequiredTokens();
254    }
255
256    @Override
257    public int[] getRequiredTokens() {
258        return new int[] {
259            TokenTypes.SINGLE_LINE_COMMENT,
260            TokenTypes.BLOCK_COMMENT_BEGIN,
261        };
262    }
263
264    @Override
265    public void visitToken(DetailAST ast) {
266        if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
267            checkSingleLineComment(ast);
268        }
269        else {
270            checkBlockComment(ast);
271        }
272    }
273
274    /**
275     * Checks if single-line comment is legal.
276     *
277     * @param ast Detail ast element to be checked.
278     */
279    private void checkSingleLineComment(DetailAST ast) {
280        final int lineNo = ast.getLineNo();
281        final String comment = ast.getFirstChild().getText();
282        final int[] lineBeforeCodePoints =
283                Arrays.copyOfRange(getLineCodePoints(lineNo - 1), 0, ast.getColumnNo());
284        final String lineBefore = new String(lineBeforeCodePoints, 0, lineBeforeCodePoints.length);
285
286        if (!format.matcher(lineBefore).find() && !isLegalCommentContent(comment)) {
287            log(ast, MSG_KEY);
288        }
289    }
290
291    /**
292     * Method to check if block comment is in correct format.
293     *
294     * @param ast Detail ast element to be checked.
295     */
296    private void checkBlockComment(DetailAST ast) {
297        final int lineNo = ast.getLineNo();
298        final DetailAST firstChild = ast.getFirstChild();
299        final DetailAST lastChild = ast.getLastChild();
300        final String comment = firstChild.getText();
301        int[] lineCodePoints = getLineCodePoints(lineNo - 1);
302
303        if (lineCodePoints.length > lastChild.getColumnNo() + 1) {
304            lineCodePoints = Arrays.copyOfRange(lineCodePoints,
305                    lastChild.getColumnNo() + 2, lineCodePoints.length);
306        }
307
308        String line = new String(lineCodePoints, 0, lineCodePoints.length);
309        line = FORMAT_LINE.matcher(line).replaceAll("");
310
311        final int[] lineBeforeCodePoints =
312                Arrays.copyOfRange(getLineCodePoints(lineNo - 1), 0, ast.getColumnNo());
313        final String lineBefore = new String(lineBeforeCodePoints, 0, lineBeforeCodePoints.length);
314        final boolean isCommentAtEndOfLine = ast.getLineNo() != lastChild.getLineNo()
315                || CommonUtil.isBlank(line);
316        final boolean isLegalBlockComment = isLegalCommentContent(comment)
317                && TokenUtil.areOnSameLine(firstChild, lastChild)
318                || format.matcher(lineBefore).find();
319
320        if (isCommentAtEndOfLine && !isLegalBlockComment) {
321            log(ast, MSG_KEY);
322        }
323    }
324
325    /**
326     * Checks if given comment content is legal.
327     *
328     * @param commentContent comment content to check.
329     * @return true if the content is legal.
330     */
331    private boolean isLegalCommentContent(String commentContent) {
332        return legalComment != null && legalComment.matcher(commentContent).find();
333    }
334}