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 {@literal &}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 {@literal &}lt;. This is required so the 103 * XML parser does not act on them, but instead passes the correct character to the regexp engine. 104 * </p> 105 * 106 * @since 4.0 107 */ 108@FileStatefulCheck 109public class RegexpCheck extends AbstractCheck { 110 111 /** 112 * A key is pointing to the warning message text in "messages.properties" file. 113 */ 114 public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp"; 115 116 /** 117 * A key is pointing to the warning message text in "messages.properties" file. 118 */ 119 public static final String MSG_REQUIRED_REGEXP = "required.regexp"; 120 121 /** 122 * A key is pointing to the warning message text in "messages.properties" file. 123 */ 124 public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp"; 125 126 /** Default duplicate limit. */ 127 private static final int DEFAULT_DUPLICATE_LIMIT = -1; 128 129 /** Default error report limit. */ 130 private static final int DEFAULT_ERROR_LIMIT = 100; 131 132 /** Error count exceeded message. */ 133 private static final String ERROR_LIMIT_EXCEEDED_MESSAGE = 134 "The error limit has been exceeded, " 135 + "the check is aborting, there may be more unreported errors."; 136 137 /** 138 * Specify message which is used to notify about violations, 139 * if empty then the default (hard-coded) message is used. 140 */ 141 private String message; 142 143 /** Control whether to ignore matches found within comments. */ 144 private boolean ignoreComments; 145 146 /** Control whether the pattern is required or illegal. */ 147 private boolean illegalPattern; 148 149 /** Specify the maximum number of violations before the check will abort. */ 150 private int errorLimit = DEFAULT_ERROR_LIMIT; 151 152 /** 153 * Control whether to check for duplicates of a required pattern, 154 * any negative value means no checking for duplicates, 155 * any positive value is used as the maximum number of allowed duplicates, 156 * if the limit is exceeded violations will be logged. 157 */ 158 private int duplicateLimit; 159 160 /** Boolean to say if we should check for duplicates. */ 161 private boolean checkForDuplicates; 162 163 /** Specify the pattern to match against. */ 164 private Pattern format = Pattern.compile("^$", Pattern.MULTILINE); 165 166 /** 167 * Creates a new {@code RegexpCheck} instance. 168 */ 169 public RegexpCheck() { 170 // no code by default 171 } 172 173 /** 174 * Setter to specify message which is used to notify about violations, 175 * if empty then the default (hard-coded) message is used. 176 * 177 * @param message custom message which should be used in report. 178 * @since 4.0 179 */ 180 public void setMessage(String message) { 181 this.message = message; 182 } 183 184 /** 185 * Setter to control whether to ignore matches found within comments. 186 * 187 * @param ignoreComments True if comments should be ignored. 188 * @since 4.0 189 */ 190 public void setIgnoreComments(boolean ignoreComments) { 191 this.ignoreComments = ignoreComments; 192 } 193 194 /** 195 * Setter to control whether the pattern is required or illegal. 196 * 197 * @param illegalPattern True if pattern is not allowed. 198 * @since 4.0 199 */ 200 public void setIllegalPattern(boolean illegalPattern) { 201 this.illegalPattern = illegalPattern; 202 } 203 204 /** 205 * Setter to specify the maximum number of violations before the check will abort. 206 * 207 * @param errorLimit the number of errors to report. 208 * @since 4.0 209 */ 210 public void setErrorLimit(int errorLimit) { 211 this.errorLimit = errorLimit; 212 } 213 214 /** 215 * Setter to control whether to check for duplicates of a required pattern, 216 * any negative value means no checking for duplicates, 217 * any positive value is used as the maximum number of allowed duplicates, 218 * if the limit is exceeded violations will be logged. 219 * 220 * @param duplicateLimit negative values mean no duplicate checking, 221 * any positive value is used as the limit. 222 * @since 4.0 223 */ 224 public void setDuplicateLimit(int duplicateLimit) { 225 this.duplicateLimit = duplicateLimit; 226 checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT; 227 } 228 229 /** 230 * Setter to specify the pattern to match against. 231 * 232 * @param pattern the new pattern 233 * @since 4.0 234 */ 235 public final void setFormat(Pattern pattern) { 236 format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE); 237 } 238 239 @Override 240 public int[] getDefaultTokens() { 241 return getRequiredTokens(); 242 } 243 244 @Override 245 public int[] getAcceptableTokens() { 246 return getRequiredTokens(); 247 } 248 249 @Override 250 public int[] getRequiredTokens() { 251 return CommonUtil.EMPTY_INT_ARRAY; 252 } 253 254 @Override 255 public void beginTree(DetailAST rootAST) { 256 processRegexpMatches(); 257 } 258 259 /** 260 * Processes the regexp matches and logs the number of errors in the file. 261 * 262 */ 263 @SuppressWarnings("deprecation") 264 private void processRegexpMatches() { 265 final Matcher matcher = format.matcher(getFileContents().getText().getFullText()); 266 int errorCount = 0; 267 int matchCount = 0; 268 final FileText text = getFileContents().getText(); 269 while (errorCount < errorLimit && matcher.find()) { 270 final LineColumn start = text.lineColumn(matcher.start()); 271 final int startLine = start.getLine(); 272 273 final boolean ignore = isIgnore(startLine, text, start, matcher); 274 if (!ignore) { 275 matchCount++; 276 if (illegalPattern || checkForDuplicates 277 && matchCount - 1 > duplicateLimit) { 278 errorCount++; 279 logMessage(startLine, errorCount); 280 } 281 } 282 } 283 if (!illegalPattern && matchCount == 0) { 284 final String msg = getMessage(errorCount); 285 log(1, MSG_REQUIRED_REGEXP, msg); 286 } 287 } 288 289 /** 290 * Detect ignore situation. 291 * 292 * @param startLine position of line 293 * @param text file text 294 * @param start line column 295 * @param matcher The matcher 296 * @return true is that need to be ignored 297 */ 298 @SuppressWarnings("deprecation") 299 private boolean isIgnore(int startLine, FileText text, LineColumn start, Matcher matcher) { 300 final LineColumn end; 301 if (matcher.end() == 0) { 302 end = text.lineColumn(0); 303 } 304 else { 305 end = text.lineColumn(matcher.end() - 1); 306 } 307 boolean ignore = false; 308 if (ignoreComments) { 309 final FileContents theFileContents = getFileContents(); 310 final int startColumn = start.getColumn(); 311 final int endLine = end.getLine(); 312 final int endColumn = end.getColumn(); 313 ignore = theFileContents.hasIntersectionWithComment(startLine, 314 startColumn, endLine, endColumn); 315 } 316 return ignore; 317 } 318 319 /** 320 * Displays the right message. 321 * 322 * @param lineNumber the line number the message relates to. 323 * @param errorCount number of errors in the file. 324 */ 325 private void logMessage(int lineNumber, int errorCount) { 326 final String msg = getMessage(errorCount); 327 328 if (illegalPattern) { 329 log(lineNumber, MSG_ILLEGAL_REGEXP, msg); 330 } 331 else { 332 log(lineNumber, MSG_DUPLICATE_REGEXP, msg); 333 } 334 } 335 336 /** 337 * Provide right message. 338 * 339 * @param errorCount number of errors in the file. 340 * @return message for violation. 341 */ 342 private String getMessage(int errorCount) { 343 String msg; 344 345 if (message == null || message.isEmpty()) { 346 msg = format.pattern(); 347 } 348 else { 349 msg = message; 350 } 351 352 if (errorCount >= errorLimit) { 353 msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg; 354 } 355 356 return msg; 357 } 358 359}