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