001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.whitespace; 021 022import java.util.Arrays; 023import java.util.HashSet; 024import java.util.Set; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.utils.CodePointUtil; 030import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 031 032/** 033 * <div> 034 * Checks that non-whitespace characters are separated by no more than one 035 * whitespace. Separating characters by tabs or multiple spaces will be 036 * reported. Currently, the check doesn't permit horizontal alignment. To inspect 037 * whitespaces before and after comments, set the property 038 * {@code validateComments} to true. 039 * </div> 040 * 041 * <p> 042 * Setting {@code validateComments} to false will ignore cases like: 043 * </p> 044 * 045 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 046 * int i; // Multiple whitespaces before comment tokens will be ignored. 047 * private void foo(int /* whitespaces before and after block-comments will be 048 * ignored */ i) { 049 * </code></pre></div> 050 * 051 * <p> 052 * Sometimes, users like to space similar items on different lines to the same 053 * column position for easier reading. This feature isn't supported by this 054 * check, so both braces in the following case will be reported as violations. 055 * </p> 056 * 057 * {@snippet lang="text" : 058 * public long toNanos(long d) { return d; } // 2 violations 059 * public long toMicros(long d) { return d / (C1 / C0); } 060 * } 061 * 062 * @since 6.19 063 */ 064@StatelessCheck 065public class SingleSpaceSeparatorCheck extends AbstractCheck { 066 067 /** 068 * A key is pointing to the warning message text in "messages.properties" file. 069 */ 070 public static final String MSG_KEY = "single.space.separator"; 071 072 /** Control whether to validate whitespaces surrounding comments. */ 073 private boolean validateComments; 074 075 /** 076 * Creates a new {@code SingleSpaceSeparatorCheck} instance. 077 */ 078 public SingleSpaceSeparatorCheck() { 079 // no code by default 080 } 081 082 /** 083 * Setter to control whether to validate whitespaces surrounding comments. 084 * 085 * @param validateComments {@code true} to validate surrounding whitespaces at comments. 086 * @since 6.19 087 */ 088 public void setValidateComments(boolean validateComments) { 089 this.validateComments = validateComments; 090 } 091 092 @Override 093 public int[] getDefaultTokens() { 094 return getRequiredTokens(); 095 } 096 097 @Override 098 public int[] getAcceptableTokens() { 099 return getRequiredTokens(); 100 } 101 102 @Override 103 public int[] getRequiredTokens() { 104 return CommonUtil.EMPTY_INT_ARRAY; 105 } 106 107 @Override 108 public boolean isCommentNodesRequired() { 109 return validateComments; 110 } 111 112 @Override 113 public void beginTree(DetailAST rootAST) { 114 if (rootAST != null) { 115 visitEachToken(rootAST); 116 } 117 } 118 119 /** 120 * Examines every sibling and child of {@code node} for violations. 121 * 122 * @param node The node to start examining. 123 */ 124 private void visitEachToken(DetailAST node) { 125 DetailAST currentNode = node; 126 final Set<Long> reportedPositions = new HashSet<>(); 127 128 do { 129 final int columnNo = currentNode.getColumnNo() - 1; 130 131 // in such expression: "j =123", placed at the start of the string index of the second 132 // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal 133 // possible index for the second whitespace between non-whitespace characters. 134 final int minSecondWhitespaceColumnNo = 2; 135 136 final boolean isSeparatedIncorrectly = 137 columnNo >= minSecondWhitespaceColumnNo 138 && !isTextSeparatedCorrectlyFromPrevious( 139 getLineCodePoints(currentNode.getLineNo() - 1), 140 columnNo); 141 142 // several nodes start at the same position, so without this the same 143 // whitespace is reported once per node 144 if (isSeparatedIncorrectly && reportedPositions.add(getPositionKey(currentNode))) { 145 log(currentNode, MSG_KEY); 146 } 147 if (currentNode.hasChildren()) { 148 currentNode = currentNode.getFirstChild(); 149 } 150 else { 151 while (currentNode.getNextSibling() == null && currentNode.getParent() != null) { 152 currentNode = currentNode.getParent(); 153 } 154 currentNode = currentNode.getNextSibling(); 155 } 156 } while (currentNode != null); 157 } 158 159 /** 160 * Combines the line and the column of a node into a single value, to keep 161 * track of the positions a violation was already reported for. 162 * 163 * @param node The node to build the value for. 164 * @return The combined position of {@code node}. 165 */ 166 private static long getPositionKey(DetailAST node) { 167 return (long) node.getLineNo() << Integer.SIZE | node.getColumnNo(); 168 } 169 170 /** 171 * Checks if characters in {@code line} at and around {@code columnNo} has 172 * the correct number of spaces. to return {@code true} the following conditions must be met: 173 * <ul> 174 * <li> the character at {@code columnNo} is the first in the line. </li> 175 * <li> the character at {@code columnNo} is not separated by whitespaces from 176 * the previous non-whitespace character. </li> 177 * <li> the character at {@code columnNo} is separated by only one whitespace 178 * from the previous non-whitespace character. </li> 179 * <li> {@link #validateComments} is disabled and the previous text is the 180 * end of a block comment. </li> 181 * </ul> 182 * 183 * @param line Unicode code point array of line in the file to examine. 184 * @param columnNo The column position in the {@code line} to examine. 185 * @return {@code true} if the text at {@code columnNo} is separated 186 * correctly from the previous token. 187 */ 188 private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) { 189 return isSingleSpace(line, columnNo) 190 || !CommonUtil.isCodePointWhitespace(line, columnNo) 191 || isFirstInLine(line, columnNo) 192 || !validateComments && isBlockCommentEnd(line, columnNo); 193 } 194 195 /** 196 * Checks if the {@code line} at {@code columnNo} is a single space, and not 197 * preceded by another space. 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 character at {@code columnNo} is a space, and 202 * not preceded by another space. 203 */ 204 private static boolean isSingleSpace(int[] line, int columnNo) { 205 return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1); 206 } 207 208 /** 209 * Checks if the {@code line} at {@code columnNo} is a space. 210 * 211 * @param line Unicode code point array of line in the file to examine. 212 * @param columnNo The column position in the {@code line} to examine. 213 * @return {@code true} if the character at {@code columnNo} is a space. 214 */ 215 private static boolean isSpace(int[] line, int columnNo) { 216 return line[columnNo] == ' '; 217 } 218 219 /** 220 * Checks if the {@code line} up to and including {@code columnNo} is all 221 * non-whitespace text encountered. 222 * 223 * @param line Unicode code point array of line in the file to examine. 224 * @param columnNo The column position in the {@code line} to examine. 225 * @return {@code true} if the column position is the first non-whitespace 226 * text on the {@code line}. 227 */ 228 private static boolean isFirstInLine(int[] line, int columnNo) { 229 return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo)); 230 } 231 232 /** 233 * Checks if the {@code line} at {@code columnNo} is the end of a comment, 234 * '*/'. 235 * 236 * @param line Unicode code point array of line in the file to examine. 237 * @param columnNo The column position in the {@code line} to examine. 238 * @return {@code true} if the previous text is an end comment block. 239 */ 240 private static boolean isBlockCommentEnd(int[] line, int columnNo) { 241 final int[] strippedLine = CodePointUtil 242 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo)); 243 return CodePointUtil.endsWith(strippedLine, "*/"); 244 } 245 246}