1 /////////////////////////////////////////////////////////////////////////////////////////////// 2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules. 3 // Copyright (C) 2001-2025 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.header; 21 22 import java.io.File; 23 import java.util.ArrayList; 24 import java.util.BitSet; 25 import java.util.List; 26 import java.util.regex.Pattern; 27 import java.util.regex.PatternSyntaxException; 28 29 import com.puppycrawl.tools.checkstyle.StatelessCheck; 30 import com.puppycrawl.tools.checkstyle.api.FileText; 31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 32 import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 33 34 /** 35 * <div> 36 * Checks the header of a source file against a header that contains a 37 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Pattern.html"> 38 * pattern</a> for each line of the source header. 39 * </div> 40 * <ul> 41 * <li> 42 * Property {@code charset} - Specify the character encoding to use when reading the headerFile. 43 * Type is {@code java.lang.String}. 44 * Default value is {@code the charset property of the parent 45 * <a href="https://checkstyle.org/config.html#Checker">Checker</a> module}. 46 * </li> 47 * <li> 48 * Property {@code fileExtensions} - Specify the file extensions of the files to process. 49 * Type is {@code java.lang.String[]}. 50 * Default value is {@code ""}. 51 * </li> 52 * <li> 53 * Property {@code header} - Define the required header specified inline. 54 * Individual header lines must be separated by the string {@code "\n"} 55 * (even on platforms with a different line separator). 56 * For header lines containing {@code "\n\n"} checkstyle will 57 * forcefully expect an empty line to exist. See examples below. 58 * Regular expressions must not span multiple lines. 59 * Type is {@code java.lang.String}. 60 * Default value is {@code null}. 61 * </li> 62 * <li> 63 * Property {@code headerFile} - Specify the name of the file containing the required header. 64 * Type is {@code java.net.URI}. 65 * Default value is {@code null}. 66 * </li> 67 * <li> 68 * Property {@code multiLines} - Specify the line numbers to repeat (zero or more times). 69 * Type is {@code int[]}. 70 * Default value is {@code ""}. 71 * </li> 72 * </ul> 73 * 74 * <p> 75 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 76 * </p> 77 * 78 * <p> 79 * Violation Message Keys: 80 * </p> 81 * <ul> 82 * <li> 83 * {@code header.mismatch} 84 * </li> 85 * <li> 86 * {@code header.missing} 87 * </li> 88 * </ul> 89 * 90 * @since 6.9 91 */ 92 @StatelessCheck 93 public class RegexpHeaderCheck extends AbstractHeaderCheck { 94 95 /** 96 * A key is pointing to the warning message text in "messages.properties" 97 * file. 98 */ 99 public static final String MSG_HEADER_MISSING = "header.missing"; 100 101 /** 102 * A key is pointing to the warning message text in "messages.properties" 103 * file. 104 */ 105 public static final String MSG_HEADER_MISMATCH = "header.mismatch"; 106 107 /** Regex pattern for a blank line. **/ 108 private static final String EMPTY_LINE_PATTERN = "^$"; 109 110 /** Compiled regex pattern for a blank line. **/ 111 private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN); 112 113 /** The compiled regular expressions. */ 114 private final List<Pattern> headerRegexps = new ArrayList<>(); 115 116 /** Specify the line numbers to repeat (zero or more times). */ 117 private BitSet multiLines = new BitSet(); 118 119 /** 120 * Setter to specify the line numbers to repeat (zero or more times). 121 * 122 * @param list line numbers to repeat in header. 123 * @since 3.4 124 */ 125 public void setMultiLines(int... list) { 126 multiLines = TokenUtil.asBitSet(list); 127 } 128 129 @Override 130 protected void processFiltered(File file, FileText fileText) { 131 final int headerSize = getHeaderLines().size(); 132 final int fileSize = fileText.size(); 133 134 if (headerSize - multiLines.cardinality() > fileSize) { 135 log(1, MSG_HEADER_MISSING); 136 } 137 else { 138 int headerLineNo = 0; 139 int index; 140 for (index = 0; headerLineNo < headerSize && index < fileSize; index++) { 141 final String line = fileText.get(index); 142 boolean isMatch = isMatch(line, headerLineNo); 143 while (!isMatch && isMultiLine(headerLineNo)) { 144 headerLineNo++; 145 isMatch = headerLineNo == headerSize 146 || isMatch(line, headerLineNo); 147 } 148 if (!isMatch) { 149 log(index + 1, MSG_HEADER_MISMATCH, getHeaderLine(headerLineNo)); 150 break; 151 } 152 if (!isMultiLine(headerLineNo)) { 153 headerLineNo++; 154 } 155 } 156 if (index == fileSize) { 157 // if file finished, but we have at least one non-multi-line 158 // header isn't completed 159 logFirstSinglelineLine(headerLineNo, headerSize); 160 } 161 } 162 } 163 164 /** 165 * Returns the line from the header. Where the line is blank return the regexp pattern 166 * for a blank line. 167 * 168 * @param headerLineNo header line number to return 169 * @return the line from the header 170 */ 171 private String getHeaderLine(int headerLineNo) { 172 String line = getHeaderLines().get(headerLineNo); 173 if (line.isEmpty()) { 174 line = EMPTY_LINE_PATTERN; 175 } 176 return line; 177 } 178 179 /** 180 * Logs warning if any non-multiline lines left in header regexp. 181 * 182 * @param startHeaderLine header line number to start from 183 * @param headerSize whole header size 184 */ 185 private void logFirstSinglelineLine(int startHeaderLine, int headerSize) { 186 for (int lineNum = startHeaderLine; lineNum < headerSize; lineNum++) { 187 if (!isMultiLine(lineNum)) { 188 log(1, MSG_HEADER_MISSING); 189 break; 190 } 191 } 192 } 193 194 /** 195 * Checks if a code line matches the required header line. 196 * 197 * @param line the code line 198 * @param headerLineNo the header line number. 199 * @return true if and only if the line matches the required header line. 200 */ 201 private boolean isMatch(String line, int headerLineNo) { 202 return headerRegexps.get(headerLineNo).matcher(line).find(); 203 } 204 205 /** 206 * Returns true if line is multiline header lines or false. 207 * 208 * @param lineNo a line number 209 * @return if {@code lineNo} is one of the repeat header lines. 210 */ 211 private boolean isMultiLine(int lineNo) { 212 return multiLines.get(lineNo + 1); 213 } 214 215 @Override 216 protected void postProcessHeaderLines() { 217 final List<String> headerLines = getHeaderLines(); 218 for (String line : headerLines) { 219 try { 220 if (line.isEmpty()) { 221 headerRegexps.add(BLANK_LINE); 222 } 223 else { 224 headerRegexps.add(Pattern.compile(line)); 225 } 226 } 227 catch (final PatternSyntaxException ex) { 228 throw new IllegalArgumentException("line " 229 + (headerRegexps.size() + 1) 230 + " in header specification" 231 + " is not a regular expression", ex); 232 } 233 } 234 } 235 236 /** 237 * Setter to define the required header specified inline. 238 * Individual header lines must be separated by the string {@code "\n"} 239 * (even on platforms with a different line separator). 240 * For header lines containing {@code "\n\n"} checkstyle will forcefully 241 * expect an empty line to exist. See examples below. 242 * Regular expressions must not span multiple lines. 243 * 244 * @param header the header value to validate and set (in that order) 245 * @since 5.0 246 */ 247 @Override 248 public void setHeader(String header) { 249 if (!CommonUtil.isBlank(header)) { 250 if (!CommonUtil.isPatternValid(header)) { 251 throw new IllegalArgumentException("Unable to parse format: " + header); 252 } 253 super.setHeader(header); 254 } 255 } 256 257 }