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.header; 021 022import java.io.File; 023import java.util.ArrayList; 024import java.util.BitSet; 025import java.util.List; 026import java.util.regex.Pattern; 027import java.util.regex.PatternSyntaxException; 028 029import com.puppycrawl.tools.checkstyle.StatelessCheck; 030import com.puppycrawl.tools.checkstyle.api.FileText; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 033 034/** 035 * <div> 036 * Checks the header of a source file against a header that contains a 037 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Pattern.html"> 038 * pattern</a> for each line of the source header. 039 * </div> 040 * 041 * @since 3.2 042 */ 043@StatelessCheck 044public class RegexpHeaderCheck extends AbstractHeaderCheck { 045 046 /** 047 * A key is pointing to the warning message text in "messages.properties" file. 048 */ 049 public static final String MSG_HEADER_MISSING = "header.missing"; 050 051 /** 052 * A key is pointing to the warning message text in "messages.properties" file. 053 */ 054 public static final String MSG_HEADER_MISMATCH = "header.mismatch"; 055 056 /** Regex pattern for a blank line. */ 057 private static final String EMPTY_LINE_PATTERN = "^$"; 058 059 /** Compiled regex pattern for a blank line. */ 060 private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN); 061 062 /** The compiled regular expressions. */ 063 private final List<Pattern> headerRegexps = new ArrayList<>(); 064 065 /** Specify the line numbers to repeat (zero or more times). */ 066 private BitSet multiLines = new BitSet(); 067 068 /** 069 * Creates a new {@code RegexpHeaderCheck} instance. 070 */ 071 public RegexpHeaderCheck() { 072 // no code by default 073 } 074 075 /** 076 * Setter to specify the line numbers to repeat (zero or more times). 077 * 078 * @param list line numbers to repeat in header. 079 * @since 3.4 080 */ 081 public void setMultiLines(int... list) { 082 multiLines = TokenUtil.asBitSet(list); 083 } 084 085 @Override 086 protected void processFiltered(File file, FileText fileText) { 087 final int headerSize = getHeaderLines().size(); 088 final int fileSize = fileText.size(); 089 090 if (headerSize - multiLines.cardinality() > fileSize) { 091 log(1, MSG_HEADER_MISSING); 092 } 093 else { 094 int headerLineNo = 0; 095 int index; 096 for (index = 0; headerLineNo < headerSize && index < fileSize; index++) { 097 final String line = fileText.get(index); 098 boolean isMatch = isMatch(line, headerLineNo); 099 while (!isMatch && isMultiLine(headerLineNo)) { 100 headerLineNo++; 101 isMatch = headerLineNo == headerSize 102 || isMatch(line, headerLineNo); 103 } 104 if (!isMatch) { 105 log(index + 1, MSG_HEADER_MISMATCH, getHeaderLine(headerLineNo)); 106 break; 107 } 108 if (!isMultiLine(headerLineNo)) { 109 headerLineNo++; 110 } 111 } 112 if (index == fileSize) { 113 // if file finished, but we have at least one non-multi-line 114 // header isn't completed 115 logFirstSinglelineLine(headerLineNo, headerSize); 116 } 117 } 118 } 119 120 /** 121 * Returns the line from the header. Where the line is blank return the regexp pattern 122 * for a blank line. 123 * 124 * @param headerLineNo header line number to return 125 * @return the line from the header 126 */ 127 private String getHeaderLine(int headerLineNo) { 128 String line = getHeaderLines().get(headerLineNo); 129 if (line.isEmpty()) { 130 line = EMPTY_LINE_PATTERN; 131 } 132 return line; 133 } 134 135 /** 136 * Logs warning if any non-multiline lines left in header regexp. 137 * 138 * @param startHeaderLine header line number to start from 139 * @param headerSize whole header size 140 */ 141 private void logFirstSinglelineLine(int startHeaderLine, int headerSize) { 142 for (int lineNum = startHeaderLine; lineNum < headerSize; lineNum++) { 143 if (!isMultiLine(lineNum)) { 144 log(1, MSG_HEADER_MISSING); 145 break; 146 } 147 } 148 } 149 150 /** 151 * Checks if a code line matches the required header line. 152 * 153 * @param line the code line 154 * @param headerLineNo the header line number. 155 * @return true if and only if the line matches the required header line. 156 */ 157 private boolean isMatch(String line, int headerLineNo) { 158 return headerRegexps.get(headerLineNo).matcher(line).find(); 159 } 160 161 /** 162 * Returns true if line is multiline header lines or false. 163 * 164 * @param lineNo a line number 165 * @return if {@code lineNo} is one of the repeat header lines. 166 */ 167 private boolean isMultiLine(int lineNo) { 168 return multiLines.get(lineNo + 1); 169 } 170 171 @Override 172 protected void postProcessHeaderLines() { 173 final List<String> headerLines = getHeaderLines(); 174 for (String line : headerLines) { 175 try { 176 if (line.isEmpty()) { 177 headerRegexps.add(BLANK_LINE); 178 } 179 else { 180 headerRegexps.add(Pattern.compile(line)); 181 } 182 } 183 catch (final PatternSyntaxException exc) { 184 throw new IllegalArgumentException("line " 185 + (headerRegexps.size() + 1) 186 + " in header specification" 187 + " is not a regular expression", exc); 188 } 189 } 190 } 191 192 /** 193 * Setter to define the required header specified inline. 194 * Individual header lines must be separated by the string {@code "\n"} 195 * (even on platforms with a different line separator). 196 * For header lines containing {@code "\n\n"} checkstyle will forcefully 197 * expect an empty line to exist. See examples below. 198 * Regular expressions must not span multiple lines. 199 * 200 * @param header the header value to validate and set (in that order) 201 * @since 5.0 202 */ 203 @Override 204 public void setHeader(String header) { 205 if (!CommonUtil.isBlank(header)) { 206 if (!CommonUtil.isPatternValid(header)) { 207 throw new IllegalArgumentException("Unable to parse format: " + header); 208 } 209 super.setHeader(header); 210 } 211 } 212 213}