001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.whitespace;
021
022import com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
026
027/**
028 * <p>
029 * Checks that non-whitespace characters are separated by no more than one
030 * whitespace. Separating characters by tabs or multiple spaces will be
031 * reported. Currently the check doesn't permit horizontal alignment. To inspect
032 * whitespaces before and after comments, set the property
033 * {@code validateComments} to true.
034 * </p>
035 *
036 * <p>
037 * Setting {@code validateComments} to false will ignore cases like:
038 * </p>
039 *
040 * <pre>
041 * int i;  &#47;&#47; Multiple whitespaces before comment tokens will be ignored.
042 * private void foo(int  &#47;* whitespaces before and after block-comments will be
043 * ignored *&#47;  i) {
044 * </pre>
045 *
046 * <p>
047 * Sometimes, users like to space similar items on different lines to the same
048 * column position for easier reading. This feature isn't supported by this
049 * check, so both braces in the following case will be reported as violations.
050 * </p>
051 *
052 * <pre>
053 * public long toNanos(long d)  { return d;             } &#47;&#47; 2 violations
054 * public long toMicros(long d) { return d / (C1 / C0); }
055 * </pre>
056 * <ul>
057 * <li>
058 * Property {@code validateComments} - Control whether to validate whitespaces
059 * surrounding comments.
060 * Type is {@code boolean}.
061 * Default value is {@code false}.
062 * </li>
063 * </ul>
064 * <p>
065 * To configure the check:
066 * </p>
067 *
068 * <pre>
069 * &lt;module name=&quot;SingleSpaceSeparator&quot;/&gt;
070 * </pre>
071 * <p>Example:</p>
072 * <pre>
073 * int foo()   { // violation, 3 whitespaces
074 *   return  1; // violation, 2 whitespaces
075 * }
076 * int fun1() { // OK, 1 whitespace
077 *   return 3; // OK, 1 whitespace
078 * }
079 * void  fun2() {} // violation, 2 whitespaces
080 * </pre>
081 *
082 * <p>
083 * To configure the check so that it validates comments:
084 * </p>
085 *
086 * <pre>
087 * &lt;module name=&quot;SingleSpaceSeparator&quot;&gt;
088 *   &lt;property name=&quot;validateComments&quot; value=&quot;true&quot;/&gt;
089 * &lt;/module&gt;
090 * </pre>
091 * <p>Example:</p>
092 * <pre>
093 * void fun1() {}  // violation, 2 whitespaces before the comment starts
094 * void fun2() { return; }  /* violation here, 2 whitespaces before the comment starts *&#47;
095 *
096 * /* violation, 2 whitespaces after the comment ends *&#47;  int a;
097 *
098 * String s; /* OK, 1 whitespace *&#47;
099 *
100 * /**
101 * * This is a Javadoc comment
102 * *&#47;  int b; // violation, 2 whitespaces after the javadoc comment ends
103 *
104 * float f1; // OK, 1 whitespace
105 *
106 * /**
107 * * OK, 1 white space after the doc comment ends
108 * *&#47; float f2;
109 * </pre>
110 * <p>
111 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
112 * </p>
113 * <p>
114 * Violation Message Keys:
115 * </p>
116 * <ul>
117 * <li>
118 * {@code single.space.separator}
119 * </li>
120 * </ul>
121 *
122 * @since 6.19
123 */
124@StatelessCheck
125public class SingleSpaceSeparatorCheck extends AbstractCheck {
126
127    /**
128     * A key is pointing to the warning message text in "messages.properties"
129     * file.
130     */
131    public static final String MSG_KEY = "single.space.separator";
132
133    /** Control whether to validate whitespaces surrounding comments. */
134    private boolean validateComments;
135
136    /**
137     * Setter to control whether to validate whitespaces surrounding comments.
138     *
139     * @param validateComments {@code true} to validate surrounding whitespaces at comments.
140     */
141    public void setValidateComments(boolean validateComments) {
142        this.validateComments = validateComments;
143    }
144
145    @Override
146    public int[] getDefaultTokens() {
147        return getRequiredTokens();
148    }
149
150    @Override
151    public int[] getAcceptableTokens() {
152        return getRequiredTokens();
153    }
154
155    @Override
156    public int[] getRequiredTokens() {
157        return CommonUtil.EMPTY_INT_ARRAY;
158    }
159
160    @Override
161    public boolean isCommentNodesRequired() {
162        return validateComments;
163    }
164
165    @Override
166    public void beginTree(DetailAST rootAST) {
167        if (rootAST != null) {
168            visitEachToken(rootAST);
169        }
170    }
171
172    /**
173     * Examines every sibling and child of {@code node} for violations.
174     *
175     * @param node The node to start examining.
176     */
177    private void visitEachToken(DetailAST node) {
178        DetailAST sibling = node;
179
180        do {
181            final int columnNo = sibling.getColumnNo() - 1;
182
183            // in such expression: "j  =123", placed at the start of the string index of the second
184            // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
185            // possible index for the second whitespace between non-whitespace characters.
186            final int minSecondWhitespaceColumnNo = 2;
187
188            if (columnNo >= minSecondWhitespaceColumnNo
189                    && !isTextSeparatedCorrectlyFromPrevious(getLine(sibling.getLineNo() - 1),
190                            columnNo)) {
191                log(sibling, MSG_KEY);
192            }
193            if (sibling.getChildCount() >= 1) {
194                visitEachToken(sibling.getFirstChild());
195            }
196
197            sibling = sibling.getNextSibling();
198        } while (sibling != null);
199    }
200
201    /**
202     * Checks if characters in {@code line} at and around {@code columnNo} has
203     * the correct number of spaces. to return {@code true} the following
204     * conditions must be met:<br />
205     * - the character at {@code columnNo} is the first in the line.<br />
206     * - the character at {@code columnNo} is not separated by whitespaces from
207     * the previous non-whitespace character. <br />
208     * - the character at {@code columnNo} is separated by only one whitespace
209     * from the previous non-whitespace character.<br />
210     * - {@link #validateComments} is disabled and the previous text is the
211     * end of a block comment.
212     *
213     * @param line The line in the file to examine.
214     * @param columnNo The column position in the {@code line} to examine.
215     * @return {@code true} if the text at {@code columnNo} is separated
216     *         correctly from the previous token.
217     */
218    private boolean isTextSeparatedCorrectlyFromPrevious(String line, int columnNo) {
219        return isSingleSpace(line, columnNo)
220                || !isWhitespace(line, columnNo)
221                || isFirstInLine(line, columnNo)
222                || !validateComments && isBlockCommentEnd(line, columnNo);
223    }
224
225    /**
226     * Checks if the {@code line} at {@code columnNo} is a single space, and not
227     * preceded by another space.
228     *
229     * @param line The line in the file to examine.
230     * @param columnNo The column position in the {@code line} to examine.
231     * @return {@code true} if the character at {@code columnNo} is a space, and
232     *         not preceded by another space.
233     */
234    private static boolean isSingleSpace(String line, int columnNo) {
235        return isSpace(line, columnNo) && !Character.isWhitespace(line.charAt(columnNo - 1));
236    }
237
238    /**
239     * Checks if the {@code line} at {@code columnNo} is a space.
240     *
241     * @param line The line in the file to examine.
242     * @param columnNo The column position in the {@code line} to examine.
243     * @return {@code true} if the character at {@code columnNo} is a space.
244     */
245    private static boolean isSpace(String line, int columnNo) {
246        return line.charAt(columnNo) == ' ';
247    }
248
249    /**
250     * Checks if the {@code line} at {@code columnNo} is a whitespace character.
251     *
252     * @param line The line in the file to examine.
253     * @param columnNo The column position in the {@code line} to examine.
254     * @return {@code true} if the character at {@code columnNo} is a
255     *         whitespace.
256     */
257    private static boolean isWhitespace(String line, int columnNo) {
258        return Character.isWhitespace(line.charAt(columnNo));
259    }
260
261    /**
262     * Checks if the {@code line} up to and including {@code columnNo} is all
263     * non-whitespace text encountered.
264     *
265     * @param line The line in the file to examine.
266     * @param columnNo The column position in the {@code line} to examine.
267     * @return {@code true} if the column position is the first non-whitespace
268     *         text on the {@code line}.
269     */
270    private static boolean isFirstInLine(String line, int columnNo) {
271        return CommonUtil.isBlank(line.substring(0, columnNo));
272    }
273
274    /**
275     * Checks if the {@code line} at {@code columnNo} is the end of a comment,
276     * '*&#47;'.
277     *
278     * @param line The line in the file to examine.
279     * @param columnNo The column position in the {@code line} to examine.
280     * @return {@code true} if the previous text is a end comment block.
281     */
282    private static boolean isBlockCommentEnd(String line, int columnNo) {
283        return line.substring(0, columnNo).trim().endsWith("*/");
284    }
285
286}