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