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/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 * Setter to specify the line numbers to repeat (zero or more times).
72 *
73 * @param list line numbers to repeat in header.
74 * @since 3.4
75 */
76 public void setMultiLines(int... list) {
77 multiLines = TokenUtil.asBitSet(list);
78 }
79
80 @Override
81 protected void processFiltered(File file, FileText fileText) {
82 final int headerSize = getHeaderLines().size();
83 final int fileSize = fileText.size();
84
85 if (headerSize - multiLines.cardinality() > fileSize) {
86 log(1, MSG_HEADER_MISSING);
87 }
88 else {
89 int headerLineNo = 0;
90 int index;
91 for (index = 0; headerLineNo < headerSize && index < fileSize; index++) {
92 final String line = fileText.get(index);
93 boolean isMatch = isMatch(line, headerLineNo);
94 while (!isMatch && isMultiLine(headerLineNo)) {
95 headerLineNo++;
96 isMatch = headerLineNo == headerSize
97 || isMatch(line, headerLineNo);
98 }
99 if (!isMatch) {
100 log(index + 1, MSG_HEADER_MISMATCH, getHeaderLine(headerLineNo));
101 break;
102 }
103 if (!isMultiLine(headerLineNo)) {
104 headerLineNo++;
105 }
106 }
107 if (index == fileSize) {
108 // if file finished, but we have at least one non-multi-line
109 // header isn't completed
110 logFirstSinglelineLine(headerLineNo, headerSize);
111 }
112 }
113 }
114
115 /**
116 * Returns the line from the header. Where the line is blank return the regexp pattern
117 * for a blank line.
118 *
119 * @param headerLineNo header line number to return
120 * @return the line from the header
121 */
122 private String getHeaderLine(int headerLineNo) {
123 String line = getHeaderLines().get(headerLineNo);
124 if (line.isEmpty()) {
125 line = EMPTY_LINE_PATTERN;
126 }
127 return line;
128 }
129
130 /**
131 * Logs warning if any non-multiline lines left in header regexp.
132 *
133 * @param startHeaderLine header line number to start from
134 * @param headerSize whole header size
135 */
136 private void logFirstSinglelineLine(int startHeaderLine, int headerSize) {
137 for (int lineNum = startHeaderLine; lineNum < headerSize; lineNum++) {
138 if (!isMultiLine(lineNum)) {
139 log(1, MSG_HEADER_MISSING);
140 break;
141 }
142 }
143 }
144
145 /**
146 * Checks if a code line matches the required header line.
147 *
148 * @param line the code line
149 * @param headerLineNo the header line number.
150 * @return true if and only if the line matches the required header line.
151 */
152 private boolean isMatch(String line, int headerLineNo) {
153 return headerRegexps.get(headerLineNo).matcher(line).find();
154 }
155
156 /**
157 * Returns true if line is multiline header lines or false.
158 *
159 * @param lineNo a line number
160 * @return if {@code lineNo} is one of the repeat header lines.
161 */
162 private boolean isMultiLine(int lineNo) {
163 return multiLines.get(lineNo + 1);
164 }
165
166 @Override
167 protected void postProcessHeaderLines() {
168 final List<String> headerLines = getHeaderLines();
169 for (String line : headerLines) {
170 try {
171 if (line.isEmpty()) {
172 headerRegexps.add(BLANK_LINE);
173 }
174 else {
175 headerRegexps.add(Pattern.compile(line));
176 }
177 }
178 catch (final PatternSyntaxException exc) {
179 throw new IllegalArgumentException("line "
180 + (headerRegexps.size() + 1)
181 + " in header specification"
182 + " is not a regular expression", exc);
183 }
184 }
185 }
186
187 /**
188 * Setter to define the required header specified inline.
189 * Individual header lines must be separated by the string {@code "\n"}
190 * (even on platforms with a different line separator).
191 * For header lines containing {@code "\n\n"} checkstyle will forcefully
192 * expect an empty line to exist. See examples below.
193 * Regular expressions must not span multiple lines.
194 *
195 * @param header the header value to validate and set (in that order)
196 * @since 5.0
197 */
198 @Override
199 public void setHeader(String header) {
200 if (!CommonUtil.isBlank(header)) {
201 if (!CommonUtil.isPatternValid(header)) {
202 throw new IllegalArgumentException("Unable to parse format: " + header);
203 }
204 super.setHeader(header);
205 }
206 }
207
208 }