1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.whitespace;
21
22 import java.util.Arrays;
23 import java.util.HashSet;
24 import java.util.Set;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31
32 /**
33 * <div>
34 * Checks that non-whitespace characters are separated by no more than one
35 * whitespace. Separating characters by tabs or multiple spaces will be
36 * reported. Currently, the check doesn't permit horizontal alignment. To inspect
37 * whitespaces before and after comments, set the property
38 * {@code validateComments} to true.
39 * </div>
40 *
41 * <p>
42 * Setting {@code validateComments} to false will ignore cases like:
43 * </p>
44 *
45 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
46 * int i; // Multiple whitespaces before comment tokens will be ignored.
47 * private void foo(int /* whitespaces before and after block-comments will be
48 * ignored */ i) {
49 * </code></pre></div>
50 *
51 * <p>
52 * Sometimes, users like to space similar items on different lines to the same
53 * column position for easier reading. This feature isn't supported by this
54 * check, so both braces in the following case will be reported as violations.
55 * </p>
56 *
57 * {@snippet lang="text" :
58 * public long toNanos(long d) { return d; } // 2 violations
59 * public long toMicros(long d) { return d / (C1 / C0); }
60 * }
61 *
62 * @since 6.19
63 */
64 @StatelessCheck
65 public class SingleSpaceSeparatorCheck extends AbstractCheck {
66
67 /**
68 * A key is pointing to the warning message text in "messages.properties"
69 * file.
70 */
71 public static final String MSG_KEY = "single.space.separator";
72
73 /** Control whether to validate whitespaces surrounding comments. */
74 private boolean validateComments;
75
76 /**
77 * Creates a new {@code SingleSpaceSeparatorCheck} instance.
78 */
79 public SingleSpaceSeparatorCheck() {
80 // no code by default
81 }
82
83 /**
84 * Setter to control whether to validate whitespaces surrounding comments.
85 *
86 * @param validateComments {@code true} to validate surrounding whitespaces at comments.
87 * @since 6.19
88 */
89 public void setValidateComments(boolean validateComments) {
90 this.validateComments = validateComments;
91 }
92
93 @Override
94 public int[] getDefaultTokens() {
95 return getRequiredTokens();
96 }
97
98 @Override
99 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 * '*/'.
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 }