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" 069 * file. 070 */ 071 public static final String MSG_KEY = "single.space.separator"; 072 073 /** Control whether to validate whitespaces surrounding comments. */ 074 private boolean validateComments; 075 076 /** 077 * Creates a new {@code SingleSpaceSeparatorCheck} instance. 078 */ 079 public SingleSpaceSeparatorCheck() { 080 // no code by default 081 } 082 083 /** 084 * Setter to control whether to validate whitespaces surrounding comments. 085 * 086 * @param validateComments {@code true} to validate surrounding whitespaces at comments. 087 * @since 6.19 088 */ 089 public void setValidateComments(boolean validateComments) { 090 this.validateComments = validateComments; 091 } 092 093 @Override 094 public int[] getDefaultTokens() { 095 return getRequiredTokens(); 096 } 097 098 @Override 099 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 * '*/'. 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}