001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 * <div>
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 * </div>
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 *
068 * <p>
069 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
070 * </p>
071 *
072 * <p>
073 * Violation Message Keys:
074 * </p>
075 * <ul>
076 * <li>
077 * {@code single.space.separator}
078 * </li>
079 * </ul>
080 *
081 * @since 6.19
082 */
083@StatelessCheck
084public class SingleSpaceSeparatorCheck extends AbstractCheck {
085
086    /**
087     * A key is pointing to the warning message text in "messages.properties"
088     * file.
089     */
090    public static final String MSG_KEY = "single.space.separator";
091
092    /** Control whether to validate whitespaces surrounding comments. */
093    private boolean validateComments;
094
095    /**
096     * Setter to control whether to validate whitespaces surrounding comments.
097     *
098     * @param validateComments {@code true} to validate surrounding whitespaces at comments.
099     * @since 6.19
100     */
101    public void setValidateComments(boolean validateComments) {
102        this.validateComments = validateComments;
103    }
104
105    @Override
106    public int[] getDefaultTokens() {
107        return getRequiredTokens();
108    }
109
110    @Override
111    public int[] getAcceptableTokens() {
112        return getRequiredTokens();
113    }
114
115    @Override
116    public int[] getRequiredTokens() {
117        return CommonUtil.EMPTY_INT_ARRAY;
118    }
119
120    @Override
121    public boolean isCommentNodesRequired() {
122        return validateComments;
123    }
124
125    @Override
126    public void beginTree(DetailAST rootAST) {
127        if (rootAST != null) {
128            visitEachToken(rootAST);
129        }
130    }
131
132    /**
133     * Examines every sibling and child of {@code node} for violations.
134     *
135     * @param node The node to start examining.
136     */
137    private void visitEachToken(DetailAST node) {
138        DetailAST currentNode = node;
139
140        do {
141            final int columnNo = currentNode.getColumnNo() - 1;
142
143            // in such expression: "j  =123", placed at the start of the string index of the second
144            // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
145            // possible index for the second whitespace between non-whitespace characters.
146            final int minSecondWhitespaceColumnNo = 2;
147
148            if (columnNo >= minSecondWhitespaceColumnNo
149                    && !isTextSeparatedCorrectlyFromPrevious(
150                            getLineCodePoints(currentNode.getLineNo() - 1),
151                            columnNo)) {
152                log(currentNode, MSG_KEY);
153            }
154            if (currentNode.hasChildren()) {
155                currentNode = currentNode.getFirstChild();
156            }
157            else {
158                while (currentNode.getNextSibling() == null && currentNode.getParent() != null) {
159                    currentNode = currentNode.getParent();
160                }
161                currentNode = currentNode.getNextSibling();
162            }
163        } while (currentNode != null);
164    }
165
166    /**
167     * Checks if characters in {@code line} at and around {@code columnNo} has
168     * the correct number of spaces. to return {@code true} the following
169     * conditions must be met:
170     * <ul>
171     * <li> the character at {@code columnNo} is the first in the line. </li>
172     * <li> the character at {@code columnNo} is not separated by whitespaces from
173     * the previous non-whitespace character. </li>
174     * <li> the character at {@code columnNo} is separated by only one whitespace
175     * from the previous non-whitespace character. </li>
176     * <li> {@link #validateComments} is disabled and the previous text is the
177     * end of a block comment. </li>
178     * </ul>
179     *
180     * @param line Unicode code point array of line in the file to examine.
181     * @param columnNo The column position in the {@code line} to examine.
182     * @return {@code true} if the text at {@code columnNo} is separated
183     *         correctly from the previous token.
184     */
185    private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) {
186        return isSingleSpace(line, columnNo)
187                || !CommonUtil.isCodePointWhitespace(line, columnNo)
188                || isFirstInLine(line, columnNo)
189                || !validateComments && isBlockCommentEnd(line, columnNo);
190    }
191
192    /**
193     * Checks if the {@code line} at {@code columnNo} is a single space, and not
194     * preceded by another space.
195     *
196     * @param line Unicode code point array of line in the file to examine.
197     * @param columnNo The column position in the {@code line} to examine.
198     * @return {@code true} if the character at {@code columnNo} is a space, and
199     *         not preceded by another space.
200     */
201    private static boolean isSingleSpace(int[] line, int columnNo) {
202        return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1);
203    }
204
205    /**
206     * Checks if the {@code line} at {@code columnNo} is a space.
207     *
208     * @param line Unicode code point array of line in the file to examine.
209     * @param columnNo The column position in the {@code line} to examine.
210     * @return {@code true} if the character at {@code columnNo} is a space.
211     */
212    private static boolean isSpace(int[] line, int columnNo) {
213        return line[columnNo] == ' ';
214    }
215
216    /**
217     * Checks if the {@code line} up to and including {@code columnNo} is all
218     * non-whitespace text encountered.
219     *
220     * @param line Unicode code point array of line in the file to examine.
221     * @param columnNo The column position in the {@code line} to examine.
222     * @return {@code true} if the column position is the first non-whitespace
223     *         text on the {@code line}.
224     */
225    private static boolean isFirstInLine(int[] line, int columnNo) {
226        return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo));
227    }
228
229    /**
230     * Checks if the {@code line} at {@code columnNo} is the end of a comment,
231     * '*&#47;'.
232     *
233     * @param line Unicode code point array of line in the file to examine.
234     * @param columnNo The column position in the {@code line} to examine.
235     * @return {@code true} if the previous text is an end comment block.
236     */
237    private static boolean isBlockCommentEnd(int[] line, int columnNo) {
238        final int[] strippedLine = CodePointUtil
239                .stripTrailing(Arrays.copyOfRange(line, 0, columnNo));
240        return CodePointUtil.endsWith(strippedLine, "*/");
241    }
242
243}