1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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
24 import com.puppycrawl.tools.checkstyle.StatelessCheck;
25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26 import com.puppycrawl.tools.checkstyle.api.DetailAST;
27 import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
28 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
29
30 /**
31 * <div>
32 * Checks that non-whitespace characters are separated by no more than one
33 * whitespace. Separating characters by tabs or multiple spaces will be
34 * reported. Currently, the check doesn't permit horizontal alignment. To inspect
35 * whitespaces before and after comments, set the property
36 * {@code validateComments} to true.
37 * </div>
38 *
39 * <p>
40 * Setting {@code validateComments} to false will ignore cases like:
41 * </p>
42 *
43 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
44 * int i; // Multiple whitespaces before comment tokens will be ignored.
45 * private void foo(int /* whitespaces before and after block-comments will be
46 * ignored */ i) {
47 * </code></pre></div>
48 *
49 * <p>
50 * Sometimes, users like to space similar items on different lines to the same
51 * column position for easier reading. This feature isn't supported by this
52 * check, so both braces in the following case will be reported as violations.
53 * </p>
54 *
55 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
56 * public long toNanos(long d) { return d; } // 2 violations
57 * public long toMicros(long d) { return d / (C1 / C0); }
58 * </code></pre></div>
59 *
60 * @since 6.19
61 */
62 @StatelessCheck
63 public class SingleSpaceSeparatorCheck extends AbstractCheck {
64
65 /**
66 * A key is pointing to the warning message text in "messages.properties"
67 * file.
68 */
69 public static final String MSG_KEY = "single.space.separator";
70
71 /** Control whether to validate whitespaces surrounding comments. */
72 private boolean validateComments;
73
74 /**
75 * Setter to control whether to validate whitespaces surrounding comments.
76 *
77 * @param validateComments {@code true} to validate surrounding whitespaces at comments.
78 * @since 6.19
79 */
80 public void setValidateComments(boolean validateComments) {
81 this.validateComments = validateComments;
82 }
83
84 @Override
85 public int[] getDefaultTokens() {
86 return getRequiredTokens();
87 }
88
89 @Override
90 public int[] getAcceptableTokens() {
91 return getRequiredTokens();
92 }
93
94 @Override
95 public int[] getRequiredTokens() {
96 return CommonUtil.EMPTY_INT_ARRAY;
97 }
98
99 @Override
100 public boolean isCommentNodesRequired() {
101 return validateComments;
102 }
103
104 @Override
105 public void beginTree(DetailAST rootAST) {
106 if (rootAST != null) {
107 visitEachToken(rootAST);
108 }
109 }
110
111 /**
112 * Examines every sibling and child of {@code node} for violations.
113 *
114 * @param node The node to start examining.
115 */
116 private void visitEachToken(DetailAST node) {
117 DetailAST currentNode = node;
118
119 do {
120 final int columnNo = currentNode.getColumnNo() - 1;
121
122 // in such expression: "j =123", placed at the start of the string index of the second
123 // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
124 // possible index for the second whitespace between non-whitespace characters.
125 final int minSecondWhitespaceColumnNo = 2;
126
127 if (columnNo >= minSecondWhitespaceColumnNo
128 && !isTextSeparatedCorrectlyFromPrevious(
129 getLineCodePoints(currentNode.getLineNo() - 1),
130 columnNo)) {
131 log(currentNode, MSG_KEY);
132 }
133 if (currentNode.hasChildren()) {
134 currentNode = currentNode.getFirstChild();
135 }
136 else {
137 while (currentNode.getNextSibling() == null && currentNode.getParent() != null) {
138 currentNode = currentNode.getParent();
139 }
140 currentNode = currentNode.getNextSibling();
141 }
142 } while (currentNode != null);
143 }
144
145 /**
146 * Checks if characters in {@code line} at and around {@code columnNo} has
147 * the correct number of spaces. to return {@code true} the following
148 * conditions must be met:
149 * <ul>
150 * <li> the character at {@code columnNo} is the first in the line. </li>
151 * <li> the character at {@code columnNo} is not separated by whitespaces from
152 * the previous non-whitespace character. </li>
153 * <li> the character at {@code columnNo} is separated by only one whitespace
154 * from the previous non-whitespace character. </li>
155 * <li> {@link #validateComments} is disabled and the previous text is the
156 * end of a block comment. </li>
157 * </ul>
158 *
159 * @param line Unicode code point array of line in the file to examine.
160 * @param columnNo The column position in the {@code line} to examine.
161 * @return {@code true} if the text at {@code columnNo} is separated
162 * correctly from the previous token.
163 */
164 private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) {
165 return isSingleSpace(line, columnNo)
166 || !CommonUtil.isCodePointWhitespace(line, columnNo)
167 || isFirstInLine(line, columnNo)
168 || !validateComments && isBlockCommentEnd(line, columnNo);
169 }
170
171 /**
172 * Checks if the {@code line} at {@code columnNo} is a single space, and not
173 * preceded by another space.
174 *
175 * @param line Unicode code point array of line in the file to examine.
176 * @param columnNo The column position in the {@code line} to examine.
177 * @return {@code true} if the character at {@code columnNo} is a space, and
178 * not preceded by another space.
179 */
180 private static boolean isSingleSpace(int[] line, int columnNo) {
181 return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1);
182 }
183
184 /**
185 * Checks if the {@code line} at {@code columnNo} is a space.
186 *
187 * @param line Unicode code point array of line in the file to examine.
188 * @param columnNo The column position in the {@code line} to examine.
189 * @return {@code true} if the character at {@code columnNo} is a space.
190 */
191 private static boolean isSpace(int[] line, int columnNo) {
192 return line[columnNo] == ' ';
193 }
194
195 /**
196 * Checks if the {@code line} up to and including {@code columnNo} is all
197 * non-whitespace text encountered.
198 *
199 * @param line Unicode code point array of line in the file to examine.
200 * @param columnNo The column position in the {@code line} to examine.
201 * @return {@code true} if the column position is the first non-whitespace
202 * text on the {@code line}.
203 */
204 private static boolean isFirstInLine(int[] line, int columnNo) {
205 return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo));
206 }
207
208 /**
209 * Checks if the {@code line} at {@code columnNo} is the end of a comment,
210 * '*/'.
211 *
212 * @param line Unicode code point array of line in the file to examine.
213 * @param columnNo The column position in the {@code line} to examine.
214 * @return {@code true} if the previous text is an end comment block.
215 */
216 private static boolean isBlockCommentEnd(int[] line, int columnNo) {
217 final int[] strippedLine = CodePointUtil
218 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo));
219 return CodePointUtil.endsWith(strippedLine, "*/");
220 }
221
222 }