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