View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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   * <pre>
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   * </pre>
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   * <pre>
56   * public long toNanos(long d)  { return d;             } &#47;&#47; 2 violations
57   * public long toMicros(long d) { return d / (C1 / C0); }
58   * </pre>
59   * <ul>
60   * <li>
61   * Property {@code validateComments} - Control whether to validate whitespaces
62   * surrounding comments.
63   * Type is {@code boolean}.
64   * Default value is {@code false}.
65   * </li>
66   * </ul>
67   *
68   * <p>
69   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
70   * </p>
71   *
72   * <p>
73   * Violation Message Keys:
74   * </p>
75   * <ul>
76   * <li>
77   * {@code single.space.separator}
78   * </li>
79   * </ul>
80   *
81   * @since 6.19
82   */
83  @StatelessCheck
84  public class SingleSpaceSeparatorCheck extends AbstractCheck {
85  
86      /**
87       * A key is pointing to the warning message text in "messages.properties"
88       * file.
89       */
90      public static final String MSG_KEY = "single.space.separator";
91  
92      /** Control whether to validate whitespaces surrounding comments. */
93      private boolean validateComments;
94  
95      /**
96       * Setter to control whether to validate whitespaces surrounding comments.
97       *
98       * @param validateComments {@code true} to validate surrounding whitespaces at comments.
99       * @since 6.19
100      */
101     public void setValidateComments(boolean validateComments) {
102         this.validateComments = validateComments;
103     }
104 
105     @Override
106     public int[] getDefaultTokens() {
107         return getRequiredTokens();
108     }
109 
110     @Override
111     public int[] getAcceptableTokens() {
112         return getRequiredTokens();
113     }
114 
115     @Override
116     public int[] getRequiredTokens() {
117         return CommonUtil.EMPTY_INT_ARRAY;
118     }
119 
120     @Override
121     public boolean isCommentNodesRequired() {
122         return validateComments;
123     }
124 
125     @Override
126     public void beginTree(DetailAST rootAST) {
127         if (rootAST != null) {
128             visitEachToken(rootAST);
129         }
130     }
131 
132     /**
133      * Examines every sibling and child of {@code node} for violations.
134      *
135      * @param node The node to start examining.
136      */
137     private void visitEachToken(DetailAST node) {
138         DetailAST currentNode = node;
139 
140         do {
141             final int columnNo = currentNode.getColumnNo() - 1;
142 
143             // in such expression: "j  =123", placed at the start of the string index of the second
144             // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
145             // possible index for the second whitespace between non-whitespace characters.
146             final int minSecondWhitespaceColumnNo = 2;
147 
148             if (columnNo >= minSecondWhitespaceColumnNo
149                     && !isTextSeparatedCorrectlyFromPrevious(
150                             getLineCodePoints(currentNode.getLineNo() - 1),
151                             columnNo)) {
152                 log(currentNode, MSG_KEY);
153             }
154             if (currentNode.hasChildren()) {
155                 currentNode = currentNode.getFirstChild();
156             }
157             else {
158                 while (currentNode.getNextSibling() == null && currentNode.getParent() != null) {
159                     currentNode = currentNode.getParent();
160                 }
161                 currentNode = currentNode.getNextSibling();
162             }
163         } while (currentNode != null);
164     }
165 
166     /**
167      * Checks if characters in {@code line} at and around {@code columnNo} has
168      * the correct number of spaces. to return {@code true} the following
169      * conditions must be met:
170      * <ul>
171      * <li> the character at {@code columnNo} is the first in the line. </li>
172      * <li> the character at {@code columnNo} is not separated by whitespaces from
173      * the previous non-whitespace character. </li>
174      * <li> the character at {@code columnNo} is separated by only one whitespace
175      * from the previous non-whitespace character. </li>
176      * <li> {@link #validateComments} is disabled and the previous text is the
177      * end of a block comment. </li>
178      * </ul>
179      *
180      * @param line Unicode code point array of line in the file to examine.
181      * @param columnNo The column position in the {@code line} to examine.
182      * @return {@code true} if the text at {@code columnNo} is separated
183      *         correctly from the previous token.
184      */
185     private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) {
186         return isSingleSpace(line, columnNo)
187                 || !CommonUtil.isCodePointWhitespace(line, columnNo)
188                 || isFirstInLine(line, columnNo)
189                 || !validateComments && isBlockCommentEnd(line, columnNo);
190     }
191 
192     /**
193      * Checks if the {@code line} at {@code columnNo} is a single space, and not
194      * preceded by another space.
195      *
196      * @param line Unicode code point array of line in the file to examine.
197      * @param columnNo The column position in the {@code line} to examine.
198      * @return {@code true} if the character at {@code columnNo} is a space, and
199      *         not preceded by another space.
200      */
201     private static boolean isSingleSpace(int[] line, int columnNo) {
202         return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1);
203     }
204 
205     /**
206      * Checks if the {@code line} at {@code columnNo} is a space.
207      *
208      * @param line Unicode code point array of line in the file to examine.
209      * @param columnNo The column position in the {@code line} to examine.
210      * @return {@code true} if the character at {@code columnNo} is a space.
211      */
212     private static boolean isSpace(int[] line, int columnNo) {
213         return line[columnNo] == ' ';
214     }
215 
216     /**
217      * Checks if the {@code line} up to and including {@code columnNo} is all
218      * non-whitespace text encountered.
219      *
220      * @param line Unicode code point array of line in the file to examine.
221      * @param columnNo The column position in the {@code line} to examine.
222      * @return {@code true} if the column position is the first non-whitespace
223      *         text on the {@code line}.
224      */
225     private static boolean isFirstInLine(int[] line, int columnNo) {
226         return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo));
227     }
228 
229     /**
230      * Checks if the {@code line} at {@code columnNo} is the end of a comment,
231      * '*&#47;'.
232      *
233      * @param line Unicode code point array of line in the file to examine.
234      * @param columnNo The column position in the {@code line} to examine.
235      * @return {@code true} if the previous text is an end comment block.
236      */
237     private static boolean isBlockCommentEnd(int[] line, int columnNo) {
238         final int[] strippedLine = CodePointUtil
239                 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo));
240         return CodePointUtil.endsWith(strippedLine, "*/");
241     }
242 
243 }