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; 023 024import com.puppycrawl.tools.checkstyle.StatelessCheck; 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.utils.CodePointUtil; 028import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 029 030/** 031 * <div> 032 * Checks that non-whitespace characters are separated by no more than one 033 * whitespace. Separating characters by tabs or multiple spaces will be 034 * reported. Currently, the check doesn't permit horizontal alignment. To inspect 035 * whitespaces before and after comments, set the property 036 * {@code validateComments} to true. 037 * </div> 038 * 039 * <p> 040 * Setting {@code validateComments} to false will ignore cases like: 041 * </p> 042 * 043 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 044 * int i; // Multiple whitespaces before comment tokens will be ignored. 045 * private void foo(int /* whitespaces before and after block-comments will be 046 * ignored */ i) { 047 * </code></pre></div> 048 * 049 * <p> 050 * Sometimes, users like to space similar items on different lines to the same 051 * column position for easier reading. This feature isn't supported by this 052 * check, so both braces in the following case will be reported as violations. 053 * </p> 054 * 055 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 056 * public long toNanos(long d) { return d; } // 2 violations 057 * public long toMicros(long d) { return d / (C1 / C0); } 058 * </code></pre></div> 059 * 060 * @since 6.19 061 */ 062@StatelessCheck 063public class SingleSpaceSeparatorCheck extends AbstractCheck { 064 065 /** 066 * A key is pointing to the warning message text in "messages.properties" 067 * file. 068 */ 069 public static final String MSG_KEY = "single.space.separator"; 070 071 /** Control whether to validate whitespaces surrounding comments. */ 072 private boolean validateComments; 073 074 /** 075 * Creates a new {@code SingleSpaceSeparatorCheck} instance. 076 */ 077 public SingleSpaceSeparatorCheck() { 078 // no code by default 079 } 080 081 /** 082 * Setter to control whether to validate whitespaces surrounding comments. 083 * 084 * @param validateComments {@code true} to validate surrounding whitespaces at comments. 085 * @since 6.19 086 */ 087 public void setValidateComments(boolean validateComments) { 088 this.validateComments = validateComments; 089 } 090 091 @Override 092 public int[] getDefaultTokens() { 093 return getRequiredTokens(); 094 } 095 096 @Override 097 public int[] getAcceptableTokens() { 098 return getRequiredTokens(); 099 } 100 101 @Override 102 public int[] getRequiredTokens() { 103 return CommonUtil.EMPTY_INT_ARRAY; 104 } 105 106 @Override 107 public boolean isCommentNodesRequired() { 108 return validateComments; 109 } 110 111 @Override 112 public void beginTree(DetailAST rootAST) { 113 if (rootAST != null) { 114 visitEachToken(rootAST); 115 } 116 } 117 118 /** 119 * Examines every sibling and child of {@code node} for violations. 120 * 121 * @param node The node to start examining. 122 */ 123 private void visitEachToken(DetailAST node) { 124 DetailAST currentNode = node; 125 126 do { 127 final int columnNo = currentNode.getColumnNo() - 1; 128 129 // in such expression: "j =123", placed at the start of the string index of the second 130 // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal 131 // possible index for the second whitespace between non-whitespace characters. 132 final int minSecondWhitespaceColumnNo = 2; 133 134 if (columnNo >= minSecondWhitespaceColumnNo 135 && !isTextSeparatedCorrectlyFromPrevious( 136 getLineCodePoints(currentNode.getLineNo() - 1), 137 columnNo)) { 138 log(currentNode, MSG_KEY); 139 } 140 if (currentNode.hasChildren()) { 141 currentNode = currentNode.getFirstChild(); 142 } 143 else { 144 while (currentNode.getNextSibling() == null && currentNode.getParent() != null) { 145 currentNode = currentNode.getParent(); 146 } 147 currentNode = currentNode.getNextSibling(); 148 } 149 } while (currentNode != null); 150 } 151 152 /** 153 * Checks if characters in {@code line} at and around {@code columnNo} has 154 * the correct number of spaces. to return {@code true} the following 155 * conditions must be met: 156 * <ul> 157 * <li> the character at {@code columnNo} is the first in the line. </li> 158 * <li> the character at {@code columnNo} is not separated by whitespaces from 159 * the previous non-whitespace character. </li> 160 * <li> the character at {@code columnNo} is separated by only one whitespace 161 * from the previous non-whitespace character. </li> 162 * <li> {@link #validateComments} is disabled and the previous text is the 163 * end of a block comment. </li> 164 * </ul> 165 * 166 * @param line Unicode code point array of line in the file to examine. 167 * @param columnNo The column position in the {@code line} to examine. 168 * @return {@code true} if the text at {@code columnNo} is separated 169 * correctly from the previous token. 170 */ 171 private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) { 172 return isSingleSpace(line, columnNo) 173 || !CommonUtil.isCodePointWhitespace(line, columnNo) 174 || isFirstInLine(line, columnNo) 175 || !validateComments && isBlockCommentEnd(line, columnNo); 176 } 177 178 /** 179 * Checks if the {@code line} at {@code columnNo} is a single space, and not 180 * preceded by another space. 181 * 182 * @param line Unicode code point array of line in the file to examine. 183 * @param columnNo The column position in the {@code line} to examine. 184 * @return {@code true} if the character at {@code columnNo} is a space, and 185 * not preceded by another space. 186 */ 187 private static boolean isSingleSpace(int[] line, int columnNo) { 188 return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1); 189 } 190 191 /** 192 * Checks if the {@code line} at {@code columnNo} is a space. 193 * 194 * @param line Unicode code point array of line in the file to examine. 195 * @param columnNo The column position in the {@code line} to examine. 196 * @return {@code true} if the character at {@code columnNo} is a space. 197 */ 198 private static boolean isSpace(int[] line, int columnNo) { 199 return line[columnNo] == ' '; 200 } 201 202 /** 203 * Checks if the {@code line} up to and including {@code columnNo} is all 204 * non-whitespace text encountered. 205 * 206 * @param line Unicode code point array of line in the file to examine. 207 * @param columnNo The column position in the {@code line} to examine. 208 * @return {@code true} if the column position is the first non-whitespace 209 * text on the {@code line}. 210 */ 211 private static boolean isFirstInLine(int[] line, int columnNo) { 212 return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo)); 213 } 214 215 /** 216 * Checks if the {@code line} at {@code columnNo} is the end of a comment, 217 * '*/'. 218 * 219 * @param line Unicode code point array of line in the file to examine. 220 * @param columnNo The column position in the {@code line} to examine. 221 * @return {@code true} if the previous text is an end comment block. 222 */ 223 private static boolean isBlockCommentEnd(int[] line, int columnNo) { 224 final int[] strippedLine = CodePointUtil 225 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo)); 226 return CodePointUtil.endsWith(strippedLine, "*/"); 227 } 228 229}