1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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/17/docs/api/java.base/java/util/regex/Pattern.html">
38 * pattern</a> for each line of the source header.
39 * </div>
40 *
41 * @since 3.2
42 */
43 @StatelessCheck
44 public class RegexpHeaderCheck extends AbstractHeaderCheck {
45
46 /**
47 * A key is pointing to the warning message text in "messages.properties"
48 * file.
49 */
50 public static final String MSG_HEADER_MISSING = "header.missing";
51
52 /**
53 * A key is pointing to the warning message text in "messages.properties"
54 * file.
55 */
56 public static final String MSG_HEADER_MISMATCH = "header.mismatch";
57
58 /** Regex pattern for a blank line. */
59 private static final String EMPTY_LINE_PATTERN = "^$";
60
61 /** Compiled regex pattern for a blank line. */
62 private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN);
63
64 /** The compiled regular expressions. */
65 private final List<Pattern> headerRegexps = new ArrayList<>();
66
67 /** Specify the line numbers to repeat (zero or more times). */
68 private BitSet multiLines = new BitSet();
69
70 /**
71 * Creates a new {@code RegexpHeaderCheck} instance.
72 */
73 public RegexpHeaderCheck() {
74 // no code by default
75 }
76
77 /**
78 * Setter to specify the line numbers to repeat (zero or more times).
79 *
80 * @param list line numbers to repeat in header.
81 * @since 3.4
82 */
83 public void setMultiLines(int... list) {
84 multiLines = TokenUtil.asBitSet(list);
85 }
86
87 @Override
88 protected void processFiltered(File file, FileText fileText) {
89 final int headerSize = getHeaderLines().size();
90 final int fileSize = fileText.size();
91
92 if (headerSize - multiLines.cardinality() > fileSize) {
93 log(1, MSG_HEADER_MISSING);
94 }
95 else {
96 int headerLineNo = 0;
97 int index;
98 for (index = 0; headerLineNo < headerSize && index < fileSize; index++) {
99 final String line = fileText.get(index);
100 boolean isMatch = isMatch(line, headerLineNo);
101 while (!isMatch && isMultiLine(headerLineNo)) {
102 headerLineNo++;
103 isMatch = headerLineNo == headerSize
104 || isMatch(line, headerLineNo);
105 }
106 if (!isMatch) {
107 log(index + 1, MSG_HEADER_MISMATCH, getHeaderLine(headerLineNo));
108 break;
109 }
110 if (!isMultiLine(headerLineNo)) {
111 headerLineNo++;
112 }
113 }
114 if (index == fileSize) {
115 // if file finished, but we have at least one non-multi-line
116 // header isn't completed
117 logFirstSinglelineLine(headerLineNo, headerSize);
118 }
119 }
120 }
121
122 /**
123 * Returns the line from the header. Where the line is blank return the regexp pattern
124 * for a blank line.
125 *
126 * @param headerLineNo header line number to return
127 * @return the line from the header
128 */
129 private String getHeaderLine(int headerLineNo) {
130 String line = getHeaderLines().get(headerLineNo);
131 if (line.isEmpty()) {
132 line = EMPTY_LINE_PATTERN;
133 }
134 return line;
135 }
136
137 /**
138 * Logs warning if any non-multiline lines left in header regexp.
139 *
140 * @param startHeaderLine header line number to start from
141 * @param headerSize whole header size
142 */
143 private void logFirstSinglelineLine(int startHeaderLine, int headerSize) {
144 for (int lineNum = startHeaderLine; lineNum < headerSize; lineNum++) {
145 if (!isMultiLine(lineNum)) {
146 log(1, MSG_HEADER_MISSING);
147 break;
148 }
149 }
150 }
151
152 /**
153 * Checks if a code line matches the required header line.
154 *
155 * @param line the code line
156 * @param headerLineNo the header line number.
157 * @return true if and only if the line matches the required header line.
158 */
159 private boolean isMatch(String line, int headerLineNo) {
160 return headerRegexps.get(headerLineNo).matcher(line).find();
161 }
162
163 /**
164 * Returns true if line is multiline header lines or false.
165 *
166 * @param lineNo a line number
167 * @return if {@code lineNo} is one of the repeat header lines.
168 */
169 private boolean isMultiLine(int lineNo) {
170 return multiLines.get(lineNo + 1);
171 }
172
173 @Override
174 protected void postProcessHeaderLines() {
175 final List<String> headerLines = getHeaderLines();
176 for (String line : headerLines) {
177 try {
178 if (line.isEmpty()) {
179 headerRegexps.add(BLANK_LINE);
180 }
181 else {
182 headerRegexps.add(Pattern.compile(line));
183 }
184 }
185 catch (final PatternSyntaxException exc) {
186 throw new IllegalArgumentException("line "
187 + (headerRegexps.size() + 1)
188 + " in header specification"
189 + " is not a regular expression", exc);
190 }
191 }
192 }
193
194 /**
195 * Setter to define the required header specified inline.
196 * Individual header lines must be separated by the string {@code "\n"}
197 * (even on platforms with a different line separator).
198 * For header lines containing {@code "\n\n"} checkstyle will forcefully
199 * expect an empty line to exist. See examples below.
200 * Regular expressions must not span multiple lines.
201 *
202 * @param header the header value to validate and set (in that order)
203 * @since 5.0
204 */
205 @Override
206 public void setHeader(String header) {
207 if (!CommonUtil.isBlank(header)) {
208 if (!CommonUtil.isPatternValid(header)) {
209 throw new IllegalArgumentException("Unable to parse format: " + header);
210 }
211 super.setHeader(header);
212 }
213 }
214
215 }