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;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.io.OutputStreamWriter;
27 import java.io.PrintWriter;
28 import java.io.StringWriter;
29 import java.nio.charset.StandardCharsets;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.MissingResourceException;
37 import java.util.Objects;
38 import java.util.ResourceBundle;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41
42 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
43 import com.puppycrawl.tools.checkstyle.api.AuditListener;
44 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
45 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
46 import com.puppycrawl.tools.checkstyle.meta.ModuleDetails;
47 import com.puppycrawl.tools.checkstyle.meta.XmlMetaReader;
48 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
49
50
51
52
53
54
55 public final class SarifLogger extends AbstractAutomaticBean implements AuditListener {
56
57
58 private static final int UNICODE_LENGTH = 4;
59
60
61 private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
62
63
64 private static final int BUFFER_SIZE = 1024;
65
66
67 private static final String MESSAGE_PLACEHOLDER = "${message}";
68
69
70 private static final String MESSAGE_TEXT_PLACEHOLDER = "${messageText}";
71
72
73 private static final String MESSAGE_ID_PLACEHOLDER = "${messageId}";
74
75
76 private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
77
78
79 private static final String URI_PLACEHOLDER = "${uri}";
80
81
82 private static final String LINE_PLACEHOLDER = "${line}";
83
84
85 private static final String COLUMN_PLACEHOLDER = "${column}";
86
87
88 private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
89
90
91 private static final String VERSION_PLACEHOLDER = "${version}";
92
93
94 private static final String RESULTS_PLACEHOLDER = "${results}";
95
96
97 private static final String RULES_PLACEHOLDER = "${rules}";
98
99
100 private static final String TWO_BACKSLASHES = "\\\\";
101
102
103 private static final Pattern A_SPACE_PATTERN = Pattern.compile(" ");
104
105
106 private static final Pattern A_QUOTE_PATTERN = Pattern.compile("\"");
107
108
109 private static final Pattern TWO_BACKSLASHES_PATTERN = Pattern.compile(TWO_BACKSLASHES);
110
111
112 private static final Pattern WINDOWS_DRIVE_LETTER_PATTERN =
113 Pattern.compile("\\A[A-Z]:", Pattern.CASE_INSENSITIVE);
114
115
116 private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{\\w+}");
117
118
119 private static final String COMMA_LINE_SEPARATOR = ",\n";
120
121
122 private final PrintWriter writer;
123
124
125 private final boolean closeStream;
126
127
128 private final List<String> results = new ArrayList<>();
129
130
131 private final Map<String, ModuleDetails> allModuleMetadata = new HashMap<>();
132
133
134 private final Map<RuleKey, ModuleDetails> ruleMetadata = new LinkedHashMap<>();
135
136
137 private final String report;
138
139
140 private final String resultLineColumn;
141
142
143 private final String resultLineOnly;
144
145
146 private final String resultFileOnly;
147
148
149 private final String resultErrorOnly;
150
151
152 private final String rule;
153
154
155 private final String messageStrings;
156
157
158 private final String messageTextOnly;
159
160
161 private final String messageWithId;
162
163
164
165
166
167
168
169
170
171
172
173
174 public SarifLogger(
175 OutputStream outputStream,
176 AutomaticBean.OutputStreamOptions outputStreamOptions)
177 throws IOException {
178 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
179 }
180
181
182
183
184
185
186
187
188
189 public SarifLogger(
190 OutputStream outputStream,
191 OutputStreamOptions outputStreamOptions)
192 throws IOException {
193 if (outputStreamOptions == null) {
194 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
195 }
196 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
197 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
198 loadModuleMetadata();
199 report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
200 resultLineColumn =
201 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
202 resultLineOnly =
203 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
204 resultFileOnly =
205 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
206 resultErrorOnly =
207 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
208 rule = readResource("/com/puppycrawl/tools/checkstyle/sarif/Rule.template");
209 messageStrings =
210 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageStrings.template");
211 messageTextOnly =
212 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageTextOnly.template");
213 messageWithId =
214 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageWithId.template");
215 }
216
217
218
219
220 private void loadModuleMetadata() {
221 final List<ModuleDetails> allModules =
222 XmlMetaReader.readAllModulesIncludingThirdPartyIfAny();
223 for (ModuleDetails module : allModules) {
224 allModuleMetadata.put(module.getFullQualifiedName(), module);
225 }
226 }
227
228 @Override
229 protected void finishLocalSetup() {
230
231 }
232
233 @Override
234 public void auditStarted(AuditEvent event) {
235
236 }
237
238 @Override
239 public void auditFinished(AuditEvent event) {
240 String rendered = replaceVersionString(report);
241 rendered = rendered
242 .replace(RESULTS_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, results))
243 .replace(RULES_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, generateRules()));
244 writer.print(rendered);
245 if (closeStream) {
246 writer.close();
247 }
248 else {
249 writer.flush();
250 }
251 }
252
253
254
255
256
257
258 private List<String> generateRules() {
259 final List<String> result = new ArrayList<>();
260 for (Map.Entry<RuleKey, ModuleDetails> entry : ruleMetadata.entrySet()) {
261 final RuleKey ruleKey = entry.getKey();
262 final ModuleDetails module = entry.getValue();
263 final String shortDescription;
264 final String fullDescription;
265 final String messageStringsFragment;
266 if (module == null) {
267 shortDescription = CommonUtil.baseClassName(ruleKey.sourceName());
268 fullDescription = "No description available";
269 messageStringsFragment = "";
270 }
271 else {
272 shortDescription = module.getName();
273 fullDescription = module.getDescription();
274 messageStringsFragment = String.join(COMMA_LINE_SEPARATOR,
275 generateMessageStrings(module));
276 }
277 result.add(rule
278 .replace(RULE_ID_PLACEHOLDER, ruleKey.toRuleId())
279 .replace("${shortDescription}", shortDescription)
280 .replace("${fullDescription}", escape(fullDescription))
281 .replace("${messageStrings}", messageStringsFragment));
282 }
283 return result;
284 }
285
286
287
288
289
290
291
292 private List<String> generateMessageStrings(ModuleDetails module) {
293 final Map<String, String> messages = getMessages(module);
294 return module.getViolationMessageKeys().stream()
295 .filter(messages::containsKey)
296 .map(key -> {
297 final String message = messages.get(key);
298 return messageStrings
299 .replace("${key}", key)
300 .replace("${text}", escape(message));
301 })
302 .toList();
303 }
304
305
306
307
308
309
310
311 private static Map<String, String> getMessages(ModuleDetails moduleDetails) {
312 final String fullQualifiedName = moduleDetails.getFullQualifiedName();
313 final Map<String, String> result = new LinkedHashMap<>();
314 try {
315 final int lastDot = fullQualifiedName.lastIndexOf('.');
316 final String packageName = fullQualifiedName.substring(0, lastDot);
317 final String bundleName = packageName + ".messages";
318 final Class<?> moduleClass = Class.forName(fullQualifiedName);
319 final ResourceBundle bundle = ResourceBundle.getBundle(
320 bundleName,
321 Locale.ROOT,
322 moduleClass.getClassLoader()
323 );
324 for (String key : moduleDetails.getViolationMessageKeys()) {
325 result.put(key, bundle.getString(key));
326 }
327 }
328 catch (ClassNotFoundException | MissingResourceException ignored) {
329
330
331 }
332 return result;
333 }
334
335
336
337
338
339
340
341 private static String replaceVersionString(String report) {
342 final String version = SarifLogger.class.getPackage().getImplementationVersion();
343 return report.replace(VERSION_PLACEHOLDER, Objects.toString(version, "null"));
344 }
345
346 @Override
347 public void addError(AuditEvent event) {
348 final RuleKey ruleKey = cacheRuleMetadata(event);
349 final String message = generateMessage(ruleKey, event);
350 if (event.getColumn() > 0) {
351 results.add(fillTemplate(resultLineColumn, Map.of(
352 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
353 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
354 COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()),
355 LINE_PLACEHOLDER, Integer.toString(event.getLine()),
356 MESSAGE_PLACEHOLDER, message,
357 RULE_ID_PLACEHOLDER, ruleKey.toRuleId())));
358 }
359 else {
360 results.add(fillTemplate(resultLineOnly, Map.of(
361 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
362 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
363 LINE_PLACEHOLDER, Integer.toString(event.getLine()),
364 MESSAGE_PLACEHOLDER, message,
365 RULE_ID_PLACEHOLDER, ruleKey.toRuleId())));
366 }
367 }
368
369
370
371
372
373
374
375 private RuleKey cacheRuleMetadata(AuditEvent event) {
376 final String sourceName = event.getSourceName();
377 final RuleKey key = new RuleKey(sourceName, event.getModuleId());
378 final ModuleDetails module = allModuleMetadata.get(sourceName);
379 ruleMetadata.putIfAbsent(key, module);
380 return key;
381 }
382
383
384
385
386
387
388
389
390 private String generateMessage(RuleKey ruleKey, AuditEvent event) {
391 final String violationKey = event.getViolation().getKey();
392 final ModuleDetails module = ruleMetadata.get(ruleKey);
393 final String result;
394 if (module != null && module.getViolationMessageKeys().contains(violationKey)) {
395 result = messageWithId
396 .replace(MESSAGE_ID_PLACEHOLDER, violationKey)
397 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
398 }
399 else {
400 result = messageTextOnly
401 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
402 }
403 return result;
404 }
405
406 @Override
407 public void addException(AuditEvent event, Throwable throwable) {
408 final StringWriter stringWriter = new StringWriter();
409 final PrintWriter printer = new PrintWriter(stringWriter);
410 throwable.printStackTrace(printer);
411 final String message = messageTextOnly
412 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(stringWriter.toString()));
413 if (event.getFileName() == null) {
414 results.add(fillTemplate(resultErrorOnly, Map.of(
415 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
416 MESSAGE_PLACEHOLDER, message)));
417 }
418 else {
419 results.add(fillTemplate(resultFileOnly, Map.of(
420 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
421 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
422 MESSAGE_PLACEHOLDER, message)));
423 }
424 }
425
426 @Override
427 public void fileStarted(AuditEvent event) {
428
429 }
430
431 @Override
432 public void fileFinished(AuditEvent event) {
433
434 }
435
436
437
438
439
440
441
442
443
444
445
446 private static String fillTemplate(String template, Map<String, String> values) {
447 final Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
448 final StringBuilder result = new StringBuilder(256);
449 while (matcher.find()) {
450 final String placeholder = matcher.group();
451 final String value = values.getOrDefault(placeholder, placeholder);
452 matcher.appendReplacement(result, Matcher.quoteReplacement(value));
453 }
454 matcher.appendTail(result);
455 return result.toString();
456 }
457
458
459
460
461
462
463
464 private static String renderFileNameUri(final String fileName) {
465 final String withoutSpaces =
466 A_SPACE_PATTERN
467 .matcher(TWO_BACKSLASHES_PATTERN.matcher(fileName).replaceAll("/"))
468 .replaceAll("%20");
469 String normalized = A_QUOTE_PATTERN.matcher(withoutSpaces).replaceAll("%22");
470 if (WINDOWS_DRIVE_LETTER_PATTERN.matcher(normalized).find()) {
471 normalized = '/' + normalized;
472 }
473 return "file:" + normalized;
474 }
475
476
477
478
479
480
481
482 private static String renderSeverityLevel(SeverityLevel severityLevel) {
483 return switch (severityLevel) {
484 case IGNORE -> "none";
485 case INFO -> "note";
486 case WARNING -> "warning";
487 case ERROR -> "error";
488 };
489 }
490
491
492
493
494
495
496
497
498 public static String escape(String value) {
499 final int length = value.length();
500 final StringBuilder sb = new StringBuilder(length);
501 for (int index = 0; index < length; index++) {
502 final char chr = value.charAt(index);
503 final String replacement = switch (chr) {
504 case '"' -> "\\\"";
505 case '\\' -> TWO_BACKSLASHES;
506 case '\b' -> "\\b";
507 case '\f' -> "\\f";
508 case '\n' -> "\\n";
509 case '\r' -> "\\r";
510 case '\t' -> "\\t";
511 case '/' -> "\\/";
512 default -> {
513 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
514 yield escapeUnicode1F(chr);
515 }
516 yield Character.toString(chr);
517 }
518 };
519 sb.append(replacement);
520 }
521
522 return sb.toString();
523 }
524
525
526
527
528
529
530
531 private static String escapeUnicode1F(char chr) {
532 final String hexString = Integer.toHexString(chr);
533 return "\\u"
534 + "0".repeat(UNICODE_LENGTH - hexString.length())
535 + hexString.toUpperCase(Locale.US);
536 }
537
538
539
540
541
542
543
544
545 public static String readResource(String name) throws IOException {
546 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
547 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
548 if (inputStream == null) {
549 throw new IOException("Cannot find the resource " + name);
550 }
551 final byte[] buffer = new byte[BUFFER_SIZE];
552 int length = 0;
553 while (length != -1) {
554 result.write(buffer, 0, length);
555 length = inputStream.read(buffer);
556 }
557 return result.toString(StandardCharsets.UTF_8);
558 }
559 }
560
561
562
563
564
565
566
567 private record RuleKey(String sourceName, String moduleId) {
568
569
570
571
572
573 private String toRuleId() {
574 final String result;
575 if (moduleId == null) {
576 result = sourceName;
577 }
578 else {
579 result = sourceName + '#' + moduleId;
580 }
581 return result;
582 }
583 }
584
585 }