1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks.header;
21
22 import java.io.BufferedInputStream;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.LineNumberReader;
27 import java.net.URI;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Set;
32 import java.util.regex.Pattern;
33 import java.util.regex.PatternSyntaxException;
34 import java.util.stream.Collectors;
35
36 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
37 import com.puppycrawl.tools.checkstyle.PropertyType;
38 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
39 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
40 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
41 import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
42 import com.puppycrawl.tools.checkstyle.api.FileText;
43 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
44
45
46
47
48
49
50
51
52
53
54 @FileStatefulCheck
55 public class MultiFileRegexpHeaderCheck
56 extends AbstractFileSetCheck implements ExternalResourceHolder {
57
58
59
60 public static final int MISMATCH_CODE = -1;
61
62
63
64
65
66 public static final String MSG_HEADER_MISSING = "multi.file.regexp.header.missing";
67
68
69
70
71
72 public static final String MSG_HEADER_MISMATCH = "multi.file.regexp.header.mismatch";
73
74
75
76
77 private static final String EMPTY_LINE_PATTERN = "^$";
78
79
80
81
82 private static final Pattern BLANK_LINE = Pattern.compile(EMPTY_LINE_PATTERN);
83
84
85
86
87
88 private final List<HeaderFileMetadata> headerFilesMetadata = new ArrayList<>();
89
90
91
92
93
94
95
96 @XdocsPropertyType(PropertyType.STRING)
97 private String headerFiles;
98
99
100
101
102
103
104
105
106
107
108
109 public void setHeaderFiles(String... headerFiles) {
110 final String[] files;
111 if (headerFiles == null) {
112 files = CommonUtil.EMPTY_STRING_ARRAY;
113 }
114 else {
115 files = headerFiles.clone();
116 }
117
118 headerFilesMetadata.clear();
119
120 for (final String headerFile : files) {
121 headerFilesMetadata.add(HeaderFileMetadata.createFromFile(headerFile));
122 }
123 }
124
125
126
127
128
129
130
131 public String getConfiguredHeaderPaths() {
132 return headerFilesMetadata.stream()
133 .map(HeaderFileMetadata::getHeaderFilePath)
134 .collect(Collectors.joining(", "));
135 }
136
137 @Override
138 public Set<String> getExternalResourceLocations() {
139 return headerFilesMetadata.stream()
140 .map(HeaderFileMetadata::getHeaderFileUri)
141 .map(URI::toASCIIString)
142 .collect(Collectors.toUnmodifiableSet());
143 }
144
145 @Override
146 protected void processFiltered(File file, FileText fileText) {
147 if (!headerFilesMetadata.isEmpty()) {
148 final List<MatchResult> matchResult = headerFilesMetadata.stream()
149 .map(headerFile -> matchHeader(fileText, headerFile))
150 .toList();
151
152 if (matchResult.stream().noneMatch(match -> match.isMatching)) {
153 final MatchResult mismatch = matchResult.get(0);
154 final String allConfiguredHeaderPaths = getConfiguredHeaderPaths();
155 log(mismatch.lineNumber, mismatch.messageKey,
156 mismatch.messageArg, allConfiguredHeaderPaths);
157 }
158 }
159 }
160
161
162
163
164
165
166
167
168 private static MatchResult matchHeader(FileText fileText, HeaderFileMetadata headerFile) {
169 final int fileSize = fileText.size();
170 final List<Pattern> headerPatterns = headerFile.getHeaderPatterns();
171 final int headerPatternSize = headerPatterns.size();
172
173 int mismatchLine = MISMATCH_CODE;
174 int index;
175 for (index = 0; index < headerPatternSize && index < fileSize; index++) {
176 if (!headerPatterns.get(index).matcher(fileText.get(index)).find()) {
177 mismatchLine = index;
178 break;
179 }
180 }
181 if (index < headerPatternSize) {
182 mismatchLine = index;
183 }
184
185 final MatchResult matchResult;
186 if (mismatchLine == MISMATCH_CODE) {
187 matchResult = MatchResult.matching();
188 }
189 else {
190 matchResult = createMismatchResult(headerFile, fileText, mismatchLine);
191 }
192 return matchResult;
193 }
194
195
196
197
198
199
200
201
202
203 private static MatchResult createMismatchResult(HeaderFileMetadata headerFile,
204 FileText fileText, int mismatchLine) {
205 final String messageKey;
206 final int lineToLog;
207 final String messageArg;
208
209 if (headerFile.getHeaderPatterns().size() > fileText.size()) {
210 messageKey = MSG_HEADER_MISSING;
211 lineToLog = 1;
212 messageArg = headerFile.getHeaderFilePath();
213 }
214 else {
215 messageKey = MSG_HEADER_MISMATCH;
216 lineToLog = mismatchLine + 1;
217 final String lineContent = headerFile.getLineContents().get(mismatchLine);
218 if (lineContent.isEmpty()) {
219 messageArg = EMPTY_LINE_PATTERN;
220 }
221 else {
222 messageArg = lineContent;
223 }
224 }
225 return MatchResult.mismatch(lineToLog, messageKey, messageArg);
226 }
227
228
229
230
231
232
233
234
235
236 public static List<String> getLines(String headerFile, URI uri) {
237 final List<String> readerLines = new ArrayList<>();
238 try (LineNumberReader lineReader = new LineNumberReader(
239 new InputStreamReader(
240 new BufferedInputStream(uri.toURL().openStream()),
241 StandardCharsets.UTF_8)
242 )) {
243 String line;
244 do {
245 line = lineReader.readLine();
246 if (line != null) {
247 readerLines.add(line);
248 }
249 } while (line != null);
250 }
251 catch (final IOException exc) {
252 throw new IllegalArgumentException("unable to load header file " + headerFile, exc);
253 }
254
255 if (readerLines.isEmpty()) {
256 throw new IllegalArgumentException("Header file is empty: " + headerFile);
257 }
258 return readerLines;
259 }
260
261
262
263
264 private static final class HeaderFileMetadata {
265
266 private final URI headerFileUri;
267
268 private final String headerFilePath;
269
270 private final List<Pattern> headerPatterns;
271
272 private final List<String> lineContents;
273
274
275
276
277
278
279
280
281
282 private HeaderFileMetadata(
283 URI headerFileUri, String headerFilePath,
284 List<Pattern> headerPatterns, List<String> lineContents
285 ) {
286 this.headerFileUri = headerFileUri;
287 this.headerFilePath = headerFilePath;
288 this.headerPatterns = headerPatterns;
289 this.lineContents = lineContents;
290 }
291
292
293
294
295
296
297
298
299
300 public static HeaderFileMetadata createFromFile(String headerPath) {
301 if (CommonUtil.isBlank(headerPath)) {
302 throw new IllegalArgumentException("Header file is not set");
303 }
304 try {
305 final URI uri = CommonUtil.getUriByFilename(headerPath);
306 final List<String> readerLines = getLines(headerPath, uri);
307 final List<Pattern> patterns = readerLines.stream()
308 .map(HeaderFileMetadata::createPatternFromLine)
309 .toList();
310 return new HeaderFileMetadata(uri, headerPath, patterns, readerLines);
311 }
312 catch (CheckstyleException exc) {
313 throw new IllegalArgumentException(
314 "Error reading or corrupted header file: " + headerPath, exc);
315 }
316 }
317
318
319
320
321
322
323
324 private static Pattern createPatternFromLine(String line) {
325 final Pattern result;
326 if (line.isEmpty()) {
327 result = BLANK_LINE;
328 }
329 else {
330 result = Pattern.compile(validateRegex(line));
331 }
332 return result;
333 }
334
335
336
337
338
339
340 public URI getHeaderFileUri() {
341 return headerFileUri;
342 }
343
344
345
346
347
348
349 public String getHeaderFilePath() {
350 return headerFilePath;
351 }
352
353
354
355
356
357
358 public List<Pattern> getHeaderPatterns() {
359 return List.copyOf(headerPatterns);
360 }
361
362
363
364
365
366
367 public List<String> getLineContents() {
368 return List.copyOf(lineContents);
369 }
370
371
372
373
374
375
376
377
378
379
380
381 private static String validateRegex(String input) {
382 try {
383 Pattern.compile(input);
384 return input;
385 }
386 catch (final PatternSyntaxException exc) {
387 throw new IllegalArgumentException("Invalid regex pattern: " + input, exc);
388 }
389 }
390 }
391
392
393
394
395 private static final class MatchResult {
396
397 private final boolean isMatching;
398
399 private final int lineNumber;
400
401 private final String messageKey;
402
403 private final String messageArg;
404
405
406
407
408
409
410
411
412
413 private MatchResult(boolean isMatching, int lineNumber, String messageKey,
414 String messageArg) {
415 this.isMatching = isMatching;
416 this.lineNumber = lineNumber;
417 this.messageKey = messageKey;
418 this.messageArg = messageArg;
419 }
420
421
422
423
424
425
426 public static MatchResult matching() {
427 return new MatchResult(true, 0, null, null);
428 }
429
430
431
432
433
434
435
436
437
438 public static MatchResult mismatch(int lineNumber, String messageKey,
439 String messageArg) {
440 return new MatchResult(false, lineNumber, messageKey, messageArg);
441 }
442 }
443 }