001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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 * <p> 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 * </p> 038 * 039 * <p> 040 * Setting {@code validateComments} to false will ignore cases like: 041 * </p> 042 * 043 * <pre> 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 * </pre> 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 * <pre> 056 * public long toNanos(long d) { return d; } // 2 violations 057 * public long toMicros(long d) { return d / (C1 / C0); } 058 * </pre> 059 * <ul> 060 * <li> 061 * Property {@code validateComments} - Control whether to validate whitespaces 062 * surrounding comments. 063 * Type is {@code boolean}. 064 * Default value is {@code false}. 065 * </li> 066 * </ul> 067 * <p> 068 * To configure the check: 069 * </p> 070 * 071 * <pre> 072 * <module name="SingleSpaceSeparator"/> 073 * </pre> 074 * <p>Example:</p> 075 * <pre> 076 * int foo() { // violation, 3 whitespaces 077 * return 1; // violation, 2 whitespaces 078 * } 079 * int fun1() { // OK, 1 whitespace 080 * return 3; // OK, 1 whitespace 081 * } 082 * void fun2() {} // violation, 2 whitespaces 083 * </pre> 084 * 085 * <p> 086 * To configure the check so that it validates comments: 087 * </p> 088 * 089 * <pre> 090 * <module name="SingleSpaceSeparator"> 091 * <property name="validateComments" value="true"/> 092 * </module> 093 * </pre> 094 * <p>Example:</p> 095 * <pre> 096 * void fun1() {} // violation, 2 whitespaces before the comment starts 097 * void fun2() { return; } /* violation here, 2 whitespaces before the comment starts */ 098 * 099 * /* violation, 2 whitespaces after the comment ends */ int a; 100 * 101 * String s; /* OK, 1 whitespace */ 102 * 103 * /** 104 * * This is a Javadoc comment 105 * */ int b; // violation, 2 whitespaces after the javadoc comment ends 106 * 107 * float f1; // OK, 1 whitespace 108 * 109 * /** 110 * * OK, 1 white space after the doc comment ends 111 * */ float f2; 112 * </pre> 113 * <p> 114 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 115 * </p> 116 * <p> 117 * Violation Message Keys: 118 * </p> 119 * <ul> 120 * <li> 121 * {@code single.space.separator} 122 * </li> 123 * </ul> 124 * 125 * @since 6.19 126 */ 127@StatelessCheck 128public class SingleSpaceSeparatorCheck extends AbstractCheck { 129 130 /** 131 * A key is pointing to the warning message text in "messages.properties" 132 * file. 133 */ 134 public static final String MSG_KEY = "single.space.separator"; 135 136 /** Control whether to validate whitespaces surrounding comments. */ 137 private boolean validateComments; 138 139 /** 140 * Setter to control whether to validate whitespaces surrounding comments. 141 * 142 * @param validateComments {@code true} to validate surrounding whitespaces at comments. 143 */ 144 public void setValidateComments(boolean validateComments) { 145 this.validateComments = validateComments; 146 } 147 148 @Override 149 public int[] getDefaultTokens() { 150 return getRequiredTokens(); 151 } 152 153 @Override 154 public int[] getAcceptableTokens() { 155 return getRequiredTokens(); 156 } 157 158 @Override 159 public int[] getRequiredTokens() { 160 return CommonUtil.EMPTY_INT_ARRAY; 161 } 162 163 @Override 164 public boolean isCommentNodesRequired() { 165 return validateComments; 166 } 167 168 @Override 169 public void beginTree(DetailAST rootAST) { 170 if (rootAST != null) { 171 visitEachToken(rootAST); 172 } 173 } 174 175 /** 176 * Examines every sibling and child of {@code node} for violations. 177 * 178 * @param node The node to start examining. 179 */ 180 private void visitEachToken(DetailAST node) { 181 DetailAST currentNode = node; 182 final DetailAST parent = node.getParent(); 183 184 do { 185 final int columnNo = currentNode.getColumnNo() - 1; 186 187 // in such expression: "j =123", placed at the start of the string index of the second 188 // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal 189 // possible index for the second whitespace between non-whitespace characters. 190 final int minSecondWhitespaceColumnNo = 2; 191 192 if (columnNo >= minSecondWhitespaceColumnNo 193 && !isTextSeparatedCorrectlyFromPrevious( 194 getLineCodePoints(currentNode.getLineNo() - 1), 195 columnNo)) { 196 log(currentNode, MSG_KEY); 197 } 198 if (currentNode.hasChildren()) { 199 currentNode = currentNode.getFirstChild(); 200 } 201 else { 202 while (currentNode.getNextSibling() == null && currentNode.getParent() != parent) { 203 currentNode = currentNode.getParent(); 204 } 205 currentNode = currentNode.getNextSibling(); 206 } 207 } while (currentNode != null); 208 } 209 210 /** 211 * Checks if characters in {@code line} at and around {@code columnNo} has 212 * the correct number of spaces. to return {@code true} the following 213 * conditions must be met: 214 * <ul> 215 * <li> the character at {@code columnNo} is the first in the line. </li> 216 * <li> the character at {@code columnNo} is not separated by whitespaces from 217 * the previous non-whitespace character. </li> 218 * <li> the character at {@code columnNo} is separated by only one whitespace 219 * from the previous non-whitespace character. </li> 220 * <li> {@link #validateComments} is disabled and the previous text is the 221 * end of a block comment. </li> 222 * </ul> 223 * 224 * @param line Unicode code point array of line in the file to examine. 225 * @param columnNo The column position in the {@code line} to examine. 226 * @return {@code true} if the text at {@code columnNo} is separated 227 * correctly from the previous token. 228 */ 229 private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) { 230 return isSingleSpace(line, columnNo) 231 || !CommonUtil.isCodePointWhitespace(line, columnNo) 232 || isFirstInLine(line, columnNo) 233 || !validateComments && isBlockCommentEnd(line, columnNo); 234 } 235 236 /** 237 * Checks if the {@code line} at {@code columnNo} is a single space, and not 238 * preceded by another space. 239 * 240 * @param line Unicode code point array of line in the file to examine. 241 * @param columnNo The column position in the {@code line} to examine. 242 * @return {@code true} if the character at {@code columnNo} is a space, and 243 * not preceded by another space. 244 */ 245 private static boolean isSingleSpace(int[] line, int columnNo) { 246 return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1); 247 } 248 249 /** 250 * Checks if the {@code line} at {@code columnNo} is a space. 251 * 252 * @param line Unicode code point array of line in the file to examine. 253 * @param columnNo The column position in the {@code line} to examine. 254 * @return {@code true} if the character at {@code columnNo} is a space. 255 */ 256 private static boolean isSpace(int[] line, int columnNo) { 257 return line[columnNo] == ' '; 258 } 259 260 /** 261 * Checks if the {@code line} up to and including {@code columnNo} is all 262 * non-whitespace text encountered. 263 * 264 * @param line Unicode code point array of line in the file to examine. 265 * @param columnNo The column position in the {@code line} to examine. 266 * @return {@code true} if the column position is the first non-whitespace 267 * text on the {@code line}. 268 */ 269 private static boolean isFirstInLine(int[] line, int columnNo) { 270 return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo)); 271 } 272 273 /** 274 * Checks if the {@code line} at {@code columnNo} is the end of a comment, 275 * '*/'. 276 * 277 * @param line Unicode code point array of line in the file to examine. 278 * @param columnNo The column position in the {@code line} to examine. 279 * @return {@code true} if the previous text is a end comment block. 280 */ 281 private static boolean isBlockCommentEnd(int[] line, int columnNo) { 282 final int[] strippedLine = CodePointUtil 283 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo)); 284 return CodePointUtil.endsWith(strippedLine, "*/"); 285 } 286 287}