001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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;
023import java.util.HashSet;
024import java.util.Set;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031
032/**
033 * <div>
034 * Checks that non-whitespace characters are separated by no more than one
035 * whitespace. Separating characters by tabs or multiple spaces will be
036 * reported. Currently, the check doesn't permit horizontal alignment. To inspect
037 * whitespaces before and after comments, set the property
038 * {@code validateComments} to true.
039 * </div>
040 *
041 * <p>
042 * Setting {@code validateComments} to false will ignore cases like:
043 * </p>
044 *
045 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
046 * int i;  &#47;&#47; Multiple whitespaces before comment tokens will be ignored.
047 * private void foo(int  &#47;* whitespaces before and after block-comments will be
048 * ignored *&#47;  i) {
049 * </code></pre></div>
050 *
051 * <p>
052 * Sometimes, users like to space similar items on different lines to the same
053 * column position for easier reading. This feature isn't supported by this
054 * check, so both braces in the following case will be reported as violations.
055 * </p>
056 *
057 * {@snippet lang="text" :
058 * public long toNanos(long d)  { return d;             } // 2 violations
059 * public long toMicros(long d) { return d / (C1 / C0); }
060 * }
061 *
062 * @since 6.19
063 */
064@StatelessCheck
065public class SingleSpaceSeparatorCheck extends AbstractCheck {
066
067    /**
068     * A key is pointing to the warning message text in "messages.properties"
069     * file.
070     */
071    public static final String MSG_KEY = "single.space.separator";
072
073    /** Control whether to validate whitespaces surrounding comments. */
074    private boolean validateComments;
075
076    /**
077     * Creates a new {@code SingleSpaceSeparatorCheck} instance.
078     */
079    public SingleSpaceSeparatorCheck() {
080        // no code by default
081    }
082
083    /**
084     * Setter to control whether to validate whitespaces surrounding comments.
085     *
086     * @param validateComments {@code true} to validate surrounding whitespaces at comments.
087     * @since 6.19
088     */
089    public void setValidateComments(boolean validateComments) {
090        this.validateComments = validateComments;
091    }
092
093    @Override
094    public int[] getDefaultTokens() {
095        return getRequiredTokens();
096    }
097
098    @Override
099    public int[] getAcceptableTokens() {
100        return getRequiredTokens();
101    }
102
103    @Override
104    public int[] getRequiredTokens() {
105        return CommonUtil.EMPTY_INT_ARRAY;
106    }
107
108    @Override
109    public boolean isCommentNodesRequired() {
110        return validateComments;
111    }
112
113    @Override
114    public void beginTree(DetailAST rootAST) {
115        if (rootAST != null) {
116            visitEachToken(rootAST);
117        }
118    }
119
120    /**
121     * Examines every sibling and child of {@code node} for violations.
122     *
123     * @param node The node to start examining.
124     */
125    private void visitEachToken(DetailAST node) {
126        DetailAST currentNode = node;
127        final Set<Long> reportedPositions = new HashSet<>();
128
129        do {
130            final int columnNo = currentNode.getColumnNo() - 1;
131
132            // in such expression: "j  =123", placed at the start of the string index of the second
133            // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
134            // possible index for the second whitespace between non-whitespace characters.
135            final int minSecondWhitespaceColumnNo = 2;
136
137            final boolean isSeparatedIncorrectly =
138                    columnNo >= minSecondWhitespaceColumnNo
139                        && !isTextSeparatedCorrectlyFromPrevious(
140                                getLineCodePoints(currentNode.getLineNo() - 1),
141                                columnNo);
142
143            // several nodes start at the same position, so without this the same
144            // whitespace is reported once per node
145            if (isSeparatedIncorrectly && reportedPositions.add(getPositionKey(currentNode))) {
146                log(currentNode, MSG_KEY);
147            }
148            if (currentNode.hasChildren()) {
149                currentNode = currentNode.getFirstChild();
150            }
151            else {
152                while (currentNode.getNextSibling() == null && currentNode.getParent() != null) {
153                    currentNode = currentNode.getParent();
154                }
155                currentNode = currentNode.getNextSibling();
156            }
157        } while (currentNode != null);
158    }
159
160    /**
161     * Combines the line and the column of a node into a single value, to keep
162     * track of the positions a violation was already reported for.
163     *
164     * @param node The node to build the value for.
165     * @return The combined position of {@code node}.
166     */
167    private static long getPositionKey(DetailAST node) {
168        return (long) node.getLineNo() << Integer.SIZE | node.getColumnNo();
169    }
170
171    /**
172     * Checks if characters in {@code line} at and around {@code columnNo} has
173     * the correct number of spaces. to return {@code true} the following
174     * conditions must be met:
175     * <ul>
176     * <li> the character at {@code columnNo} is the first in the line. </li>
177     * <li> the character at {@code columnNo} is not separated by whitespaces from
178     * the previous non-whitespace character. </li>
179     * <li> the character at {@code columnNo} is separated by only one whitespace
180     * from the previous non-whitespace character. </li>
181     * <li> {@link #validateComments} is disabled and the previous text is the
182     * end of a block comment. </li>
183     * </ul>
184     *
185     * @param line Unicode code point array of line in the file to examine.
186     * @param columnNo The column position in the {@code line} to examine.
187     * @return {@code true} if the text at {@code columnNo} is separated
188     *         correctly from the previous token.
189     */
190    private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) {
191        return isSingleSpace(line, columnNo)
192                || !CommonUtil.isCodePointWhitespace(line, columnNo)
193                || isFirstInLine(line, columnNo)
194                || !validateComments && isBlockCommentEnd(line, columnNo);
195    }
196
197    /**
198     * Checks if the {@code line} at {@code columnNo} is a single space, and not
199     * preceded by another space.
200     *
201     * @param line Unicode code point array of line in the file to examine.
202     * @param columnNo The column position in the {@code line} to examine.
203     * @return {@code true} if the character at {@code columnNo} is a space, and
204     *         not preceded by another space.
205     */
206    private static boolean isSingleSpace(int[] line, int columnNo) {
207        return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1);
208    }
209
210    /**
211     * Checks if the {@code line} at {@code columnNo} is a space.
212     *
213     * @param line Unicode code point array of line in the file to examine.
214     * @param columnNo The column position in the {@code line} to examine.
215     * @return {@code true} if the character at {@code columnNo} is a space.
216     */
217    private static boolean isSpace(int[] line, int columnNo) {
218        return line[columnNo] == ' ';
219    }
220
221    /**
222     * Checks if the {@code line} up to and including {@code columnNo} is all
223     * non-whitespace text encountered.
224     *
225     * @param line Unicode code point array of line in the file to examine.
226     * @param columnNo The column position in the {@code line} to examine.
227     * @return {@code true} if the column position is the first non-whitespace
228     *         text on the {@code line}.
229     */
230    private static boolean isFirstInLine(int[] line, int columnNo) {
231        return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo));
232    }
233
234    /**
235     * Checks if the {@code line} at {@code columnNo} is the end of a comment,
236     * '*&#47;'.
237     *
238     * @param line Unicode code point array of line in the file to examine.
239     * @param columnNo The column position in the {@code line} to examine.
240     * @return {@code true} if the previous text is an end comment block.
241     */
242    private static boolean isBlockCommentEnd(int[] line, int columnNo) {
243        final int[] strippedLine = CodePointUtil
244                .stripTrailing(Arrays.copyOfRange(line, 0, columnNo));
245        return CodePointUtil.endsWith(strippedLine, "*/");
246    }
247
248}