View Javadoc
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 }