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