001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.regexp; 021 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.FileContents; 029import com.puppycrawl.tools.checkstyle.api.FileText; 030import com.puppycrawl.tools.checkstyle.api.LineColumn; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032 033/** 034 * <div> 035 * Checks that a specified pattern exists, exists less than 036 * a set number of times, or does not exist in the file. 037 * </div> 038 * 039 * <p> 040 * This check combines all the functionality provided by 041 * <a href="https://checkstyle.org/checks/header/regexpheader.html">RegexpHeader</a> 042 * except supplying the regular expression from a file. 043 * </p> 044 * 045 * <p> 046 * It differs from them in that it works in multiline mode. Its regular expression 047 * can span multiple lines and it checks this against the whole file at once. 048 * The others work in single-line mode. Their single or multiple regular expressions 049 * can only span one line. They check each of these against each line in the file in turn. 050 * </p> 051 * 052 * <p> 053 * <b>Note:</b> Because of the different mode of operation there may be some 054 * changes in the regular expressions used to achieve a particular end. 055 * </p> 056 * 057 * <p> 058 * In multiline mode... 059 * </p> 060 * <ul> 061 * <li> 062 * {@code ^} means the beginning of a line, as opposed to beginning of the input. 063 * </li> 064 * <li> 065 * For beginning of the input use {@code \A}. 066 * </li> 067 * <li> 068 * {@code $} means the end of a line, as opposed to the end of the input. 069 * </li> 070 * <li> 071 * For end of input use {@code \Z}. 072 * </li> 073 * <li> 074 * Each line in the file is terminated with a line feed character. 075 * </li> 076 * </ul> 077 * 078 * <p> 079 * <b>Note:</b> Not all regular expression engines are created equal. 080 * Some provide extra functions that others do not and some elements 081 * of the syntax may vary. This check makes use of the 082 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/package-summary.html"> 083 * java.util.regex package</a>; please check its documentation for details 084 * of how to construct a regular expression to achieve a particular goal. 085 * </p> 086 * 087 * <p> 088 * <b>Note:</b> When entering a regular expression as a parameter in 089 * the XML config file you must also take into account the XML rules. e.g. 090 * if you want to match a {@literal <} symbol you need to enter &lt;. 091 * The regular expression should be entered on one line. 092 * </p> 093 * 094 * <p> 095 * <b>Note:</b> To search for parentheses () in a regular expression 096 * you must escape them like \(\). This is required by the regexp engine, 097 * otherwise it will think they are special instruction characters. 098 * </p> 099 * 100 * <p> 101 * <b>Note:</b> To search for things that mean something in XML, like 102 * {@literal <} you need to escape them like &lt;. This is required so the 103 * XML parser does not act on them, but instead passes the correct 104 * character to the regexp engine. 105 * </p> 106 * 107 * @since 4.0 108 */ 109@FileStatefulCheck 110public class RegexpCheck extends AbstractCheck { 111 112 /** 113 * A key is pointing to the warning message text in "messages.properties" 114 * file. 115 */ 116 public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp"; 117 118 /** 119 * A key is pointing to the warning message text in "messages.properties" 120 * file. 121 */ 122 public static final String MSG_REQUIRED_REGEXP = "required.regexp"; 123 124 /** 125 * A key is pointing to the warning message text in "messages.properties" 126 * file. 127 */ 128 public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp"; 129 130 /** Default duplicate limit. */ 131 private static final int DEFAULT_DUPLICATE_LIMIT = -1; 132 133 /** Default error report limit. */ 134 private static final int DEFAULT_ERROR_LIMIT = 100; 135 136 /** Error count exceeded message. */ 137 private static final String ERROR_LIMIT_EXCEEDED_MESSAGE = 138 "The error limit has been exceeded, " 139 + "the check is aborting, there may be more unreported errors."; 140 141 /** 142 * Specify message which is used to notify about violations, 143 * if empty then the default (hard-coded) message is used. 144 */ 145 private String message; 146 147 /** Control whether to ignore matches found within comments. */ 148 private boolean ignoreComments; 149 150 /** Control whether the pattern is required or illegal. */ 151 private boolean illegalPattern; 152 153 /** Specify the maximum number of violations before the check will abort. */ 154 private int errorLimit = DEFAULT_ERROR_LIMIT; 155 156 /** 157 * Control whether to check for duplicates of a required pattern, 158 * any negative value means no checking for duplicates, 159 * any positive value is used as the maximum number of allowed duplicates, 160 * if the limit is exceeded violations will be logged. 161 */ 162 private int duplicateLimit; 163 164 /** Boolean to say if we should check for duplicates. */ 165 private boolean checkForDuplicates; 166 167 /** Specify the pattern to match against. */ 168 private Pattern format = Pattern.compile("^$", Pattern.MULTILINE); 169 170 /** 171 * Creates a new {@code RegexpCheck} instance. 172 */ 173 public RegexpCheck() { 174 // no code by default 175 } 176 177 /** 178 * Setter to specify message which is used to notify about violations, 179 * if empty then the default (hard-coded) message is used. 180 * 181 * @param message custom message which should be used in report. 182 * @since 4.0 183 */ 184 public void setMessage(String message) { 185 this.message = message; 186 } 187 188 /** 189 * Setter to control whether to ignore matches found within comments. 190 * 191 * @param ignoreComments True if comments should be ignored. 192 * @since 4.0 193 */ 194 public void setIgnoreComments(boolean ignoreComments) { 195 this.ignoreComments = ignoreComments; 196 } 197 198 /** 199 * Setter to control whether the pattern is required or illegal. 200 * 201 * @param illegalPattern True if pattern is not allowed. 202 * @since 4.0 203 */ 204 public void setIllegalPattern(boolean illegalPattern) { 205 this.illegalPattern = illegalPattern; 206 } 207 208 /** 209 * Setter to specify the maximum number of violations before the check will abort. 210 * 211 * @param errorLimit the number of errors to report. 212 * @since 4.0 213 */ 214 public void setErrorLimit(int errorLimit) { 215 this.errorLimit = errorLimit; 216 } 217 218 /** 219 * Setter to control whether to check for duplicates of a required pattern, 220 * any negative value means no checking for duplicates, 221 * any positive value is used as the maximum number of allowed duplicates, 222 * if the limit is exceeded violations will be logged. 223 * 224 * @param duplicateLimit negative values mean no duplicate checking, 225 * any positive value is used as the limit. 226 * @since 4.0 227 */ 228 public void setDuplicateLimit(int duplicateLimit) { 229 this.duplicateLimit = duplicateLimit; 230 checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT; 231 } 232 233 /** 234 * Setter to specify the pattern to match against. 235 * 236 * @param pattern the new pattern 237 * @since 4.0 238 */ 239 public final void setFormat(Pattern pattern) { 240 format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE); 241 } 242 243 @Override 244 public int[] getDefaultTokens() { 245 return getRequiredTokens(); 246 } 247 248 @Override 249 public int[] getAcceptableTokens() { 250 return getRequiredTokens(); 251 } 252 253 @Override 254 public int[] getRequiredTokens() { 255 return CommonUtil.EMPTY_INT_ARRAY; 256 } 257 258 @Override 259 public void beginTree(DetailAST rootAST) { 260 processRegexpMatches(); 261 } 262 263 /** 264 * Processes the regexp matches and logs the number of errors in the file. 265 * 266 */ 267 @SuppressWarnings("deprecation") 268 private void processRegexpMatches() { 269 final Matcher matcher = format.matcher(getFileContents().getText().getFullText()); 270 int errorCount = 0; 271 int matchCount = 0; 272 final FileText text = getFileContents().getText(); 273 while (errorCount < errorLimit && matcher.find()) { 274 final LineColumn start = text.lineColumn(matcher.start()); 275 final int startLine = start.getLine(); 276 277 final boolean ignore = isIgnore(startLine, text, start, matcher); 278 if (!ignore) { 279 matchCount++; 280 if (illegalPattern || checkForDuplicates 281 && matchCount - 1 > duplicateLimit) { 282 errorCount++; 283 logMessage(startLine, errorCount); 284 } 285 } 286 } 287 if (!illegalPattern && matchCount == 0) { 288 final String msg = getMessage(errorCount); 289 log(1, MSG_REQUIRED_REGEXP, msg); 290 } 291 } 292 293 /** 294 * Detect ignore situation. 295 * 296 * @param startLine position of line 297 * @param text file text 298 * @param start line column 299 * @param matcher The matcher 300 * @return true is that need to be ignored 301 */ 302 @SuppressWarnings("deprecation") 303 private boolean isIgnore(int startLine, FileText text, LineColumn start, Matcher matcher) { 304 final LineColumn end; 305 if (matcher.end() == 0) { 306 end = text.lineColumn(0); 307 } 308 else { 309 end = text.lineColumn(matcher.end() - 1); 310 } 311 boolean ignore = false; 312 if (ignoreComments) { 313 final FileContents theFileContents = getFileContents(); 314 final int startColumn = start.getColumn(); 315 final int endLine = end.getLine(); 316 final int endColumn = end.getColumn(); 317 ignore = theFileContents.hasIntersectionWithComment(startLine, 318 startColumn, endLine, endColumn); 319 } 320 return ignore; 321 } 322 323 /** 324 * Displays the right message. 325 * 326 * @param lineNumber the line number the message relates to. 327 * @param errorCount number of errors in the file. 328 */ 329 private void logMessage(int lineNumber, int errorCount) { 330 final String msg = getMessage(errorCount); 331 332 if (illegalPattern) { 333 log(lineNumber, MSG_ILLEGAL_REGEXP, msg); 334 } 335 else { 336 log(lineNumber, MSG_DUPLICATE_REGEXP, msg); 337 } 338 } 339 340 /** 341 * Provide right message. 342 * 343 * @param errorCount number of errors in the file. 344 * @return message for violation. 345 */ 346 private String getMessage(int errorCount) { 347 String msg; 348 349 if (message == null || message.isEmpty()) { 350 msg = format.pattern(); 351 } 352 else { 353 msg = message; 354 } 355 356 if (errorCount >= errorLimit) { 357 msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg; 358 } 359 360 return msg; 361 } 362 363}