1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2024 the original author or authors. 4 // 5 // This library is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 2.1 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 /////////////////////////////////////////////////////////////////////////////////////////////// 19 20 package com.puppycrawl.tools.checkstyle.checks.whitespace; 21 22 import java.util.Arrays; 23 24 import com.puppycrawl.tools.checkstyle.StatelessCheck; 25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 26 import com.puppycrawl.tools.checkstyle.api.DetailAST; 27 import com.puppycrawl.tools.checkstyle.utils.CodePointUtil; 28 import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 29 30 /** 31 * <div> 32 * Checks that non-whitespace characters are separated by no more than one 33 * whitespace. Separating characters by tabs or multiple spaces will be 34 * reported. Currently, the check doesn't permit horizontal alignment. To inspect 35 * whitespaces before and after comments, set the property 36 * {@code validateComments} to true. 37 * </div> 38 * 39 * <p> 40 * Setting {@code validateComments} to false will ignore cases like: 41 * </p> 42 * 43 * <pre> 44 * int i; // Multiple whitespaces before comment tokens will be ignored. 45 * private void foo(int /* whitespaces before and after block-comments will be 46 * ignored */ i) { 47 * </pre> 48 * 49 * <p> 50 * Sometimes, users like to space similar items on different lines to the same 51 * column position for easier reading. This feature isn't supported by this 52 * check, so both braces in the following case will be reported as violations. 53 * </p> 54 * 55 * <pre> 56 * public long toNanos(long d) { return d; } // 2 violations 57 * public long toMicros(long d) { return d / (C1 / C0); } 58 * </pre> 59 * <ul> 60 * <li> 61 * Property {@code validateComments} - Control whether to validate whitespaces 62 * surrounding comments. 63 * Type is {@code boolean}. 64 * Default value is {@code false}. 65 * </li> 66 * </ul> 67 * 68 * <p> 69 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 70 * </p> 71 * 72 * <p> 73 * Violation Message Keys: 74 * </p> 75 * <ul> 76 * <li> 77 * {@code single.space.separator} 78 * </li> 79 * </ul> 80 * 81 * @since 6.19 82 */ 83 @StatelessCheck 84 public class SingleSpaceSeparatorCheck extends AbstractCheck { 85 86 /** 87 * A key is pointing to the warning message text in "messages.properties" 88 * file. 89 */ 90 public static final String MSG_KEY = "single.space.separator"; 91 92 /** Control whether to validate whitespaces surrounding comments. */ 93 private boolean validateComments; 94 95 /** 96 * Setter to control whether to validate whitespaces surrounding comments. 97 * 98 * @param validateComments {@code true} to validate surrounding whitespaces at comments. 99 * @since 6.19 100 */ 101 public void setValidateComments(boolean validateComments) { 102 this.validateComments = validateComments; 103 } 104 105 @Override 106 public int[] getDefaultTokens() { 107 return getRequiredTokens(); 108 } 109 110 @Override 111 public int[] getAcceptableTokens() { 112 return getRequiredTokens(); 113 } 114 115 @Override 116 public int[] getRequiredTokens() { 117 return CommonUtil.EMPTY_INT_ARRAY; 118 } 119 120 @Override 121 public boolean isCommentNodesRequired() { 122 return validateComments; 123 } 124 125 @Override 126 public void beginTree(DetailAST rootAST) { 127 if (rootAST != null) { 128 visitEachToken(rootAST); 129 } 130 } 131 132 /** 133 * Examines every sibling and child of {@code node} for violations. 134 * 135 * @param node The node to start examining. 136 */ 137 private void visitEachToken(DetailAST node) { 138 DetailAST currentNode = node; 139 140 do { 141 final int columnNo = currentNode.getColumnNo() - 1; 142 143 // in such expression: "j =123", placed at the start of the string index of the second 144 // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal 145 // possible index for the second whitespace between non-whitespace characters. 146 final int minSecondWhitespaceColumnNo = 2; 147 148 if (columnNo >= minSecondWhitespaceColumnNo 149 && !isTextSeparatedCorrectlyFromPrevious( 150 getLineCodePoints(currentNode.getLineNo() - 1), 151 columnNo)) { 152 log(currentNode, MSG_KEY); 153 } 154 if (currentNode.hasChildren()) { 155 currentNode = currentNode.getFirstChild(); 156 } 157 else { 158 while (currentNode.getNextSibling() == null && currentNode.getParent() != null) { 159 currentNode = currentNode.getParent(); 160 } 161 currentNode = currentNode.getNextSibling(); 162 } 163 } while (currentNode != null); 164 } 165 166 /** 167 * Checks if characters in {@code line} at and around {@code columnNo} has 168 * the correct number of spaces. to return {@code true} the following 169 * conditions must be met: 170 * <ul> 171 * <li> the character at {@code columnNo} is the first in the line. </li> 172 * <li> the character at {@code columnNo} is not separated by whitespaces from 173 * the previous non-whitespace character. </li> 174 * <li> the character at {@code columnNo} is separated by only one whitespace 175 * from the previous non-whitespace character. </li> 176 * <li> {@link #validateComments} is disabled and the previous text is the 177 * end of a block comment. </li> 178 * </ul> 179 * 180 * @param line Unicode code point array of line in the file to examine. 181 * @param columnNo The column position in the {@code line} to examine. 182 * @return {@code true} if the text at {@code columnNo} is separated 183 * correctly from the previous token. 184 */ 185 private boolean isTextSeparatedCorrectlyFromPrevious(int[] line, int columnNo) { 186 return isSingleSpace(line, columnNo) 187 || !CommonUtil.isCodePointWhitespace(line, columnNo) 188 || isFirstInLine(line, columnNo) 189 || !validateComments && isBlockCommentEnd(line, columnNo); 190 } 191 192 /** 193 * Checks if the {@code line} at {@code columnNo} is a single space, and not 194 * preceded by another space. 195 * 196 * @param line Unicode code point array of line in the file to examine. 197 * @param columnNo The column position in the {@code line} to examine. 198 * @return {@code true} if the character at {@code columnNo} is a space, and 199 * not preceded by another space. 200 */ 201 private static boolean isSingleSpace(int[] line, int columnNo) { 202 return isSpace(line, columnNo) && !CommonUtil.isCodePointWhitespace(line, columnNo - 1); 203 } 204 205 /** 206 * Checks if the {@code line} at {@code columnNo} is a space. 207 * 208 * @param line Unicode code point array of line in the file to examine. 209 * @param columnNo The column position in the {@code line} to examine. 210 * @return {@code true} if the character at {@code columnNo} is a space. 211 */ 212 private static boolean isSpace(int[] line, int columnNo) { 213 return line[columnNo] == ' '; 214 } 215 216 /** 217 * Checks if the {@code line} up to and including {@code columnNo} is all 218 * non-whitespace text encountered. 219 * 220 * @param line Unicode code point array of line in the file to examine. 221 * @param columnNo The column position in the {@code line} to examine. 222 * @return {@code true} if the column position is the first non-whitespace 223 * text on the {@code line}. 224 */ 225 private static boolean isFirstInLine(int[] line, int columnNo) { 226 return CodePointUtil.isBlank(Arrays.copyOfRange(line, 0, columnNo)); 227 } 228 229 /** 230 * Checks if the {@code line} at {@code columnNo} is the end of a comment, 231 * '*/'. 232 * 233 * @param line Unicode code point array of line in the file to examine. 234 * @param columnNo The column position in the {@code line} to examine. 235 * @return {@code true} if the previous text is an end comment block. 236 */ 237 private static boolean isBlockCommentEnd(int[] line, int columnNo) { 238 final int[] strippedLine = CodePointUtil 239 .stripTrailing(Arrays.copyOfRange(line, 0, columnNo)); 240 return CodePointUtil.endsWith(strippedLine, "*/"); 241 } 242 243 }