View Javadoc
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;  &#47;&#47; Multiple whitespaces before comment tokens will be ignored.
47   * private void foo(int  &#47;* whitespaces before and after block-comments will be
48   * ignored *&#47;  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      * '*&#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 }