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