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) throws IOException {
177 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
178 }
179
180
181
182
183
184
185
186
187
188 public SarifLogger(
189 OutputStream outputStream,
190 OutputStreamOptions outputStreamOptions) throws IOException {
191 if (outputStreamOptions == null) {
192 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
193 }
194 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
195 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
196 loadModuleMetadata();
197 report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
198 resultLineColumn =
199 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
200 resultLineOnly =
201 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
202 resultFileOnly =
203 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
204 resultErrorOnly =
205 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
206 rule = readResource("/com/puppycrawl/tools/checkstyle/sarif/Rule.template");
207 messageStrings =
208 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageStrings.template");
209 messageTextOnly =
210 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageTextOnly.template");
211 messageWithId =
212 readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageWithId.template");
213 }
214
215
216
217
218 private void loadModuleMetadata() {
219 final List<ModuleDetails> allModules =
220 XmlMetaReader.readAllModulesIncludingThirdPartyIfAny();
221 for (ModuleDetails module : allModules) {
222 allModuleMetadata.put(module.getFullQualifiedName(), module);
223 }
224 }
225
226 @Override
227 protected void finishLocalSetup() {
228
229 }
230
231 @Override
232 public void auditStarted(AuditEvent event) {
233
234 }
235
236 @Override
237 public void auditFinished(AuditEvent event) {
238 String rendered = replaceVersionString(report);
239 rendered = rendered
240 .replace(RESULTS_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, results))
241 .replace(RULES_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, generateRules()));
242 writer.print(rendered);
243 if (closeStream) {
244 writer.close();
245 }
246 else {
247 writer.flush();
248 }
249 }
250
251
252
253
254
255
256 private List<String> generateRules() {
257 final List<String> result = new ArrayList<>();
258 for (Map.Entry<RuleKey, ModuleDetails> entry : ruleMetadata.entrySet()) {
259 final RuleKey ruleKey = entry.getKey();
260 final ModuleDetails module = entry.getValue();
261 final String shortDescription;
262 final String fullDescription;
263 final String messageStringsFragment;
264 if (module == null) {
265 shortDescription = CommonUtil.baseClassName(ruleKey.sourceName());
266 fullDescription = "No description available";
267 messageStringsFragment = "";
268 }
269 else {
270 shortDescription = module.getName();
271 fullDescription = module.getDescription();
272 messageStringsFragment = String.join(COMMA_LINE_SEPARATOR,
273 generateMessageStrings(module));
274 }
275 result.add(rule
276 .replace(RULE_ID_PLACEHOLDER, ruleKey.toRuleId())
277 .replace("${shortDescription}", shortDescription)
278 .replace("${fullDescription}", escape(fullDescription))
279 .replace("${messageStrings}", messageStringsFragment));
280 }
281 return result;
282 }
283
284
285
286
287
288
289
290 private List<String> generateMessageStrings(ModuleDetails module) {
291 final Map<String, String> messages = getMessages(module);
292 return module.getViolationMessageKeys().stream()
293 .filter(messages::containsKey)
294 .map(key -> {
295 final String message = messages.get(key);
296 return messageStrings
297 .replace("${key}", key)
298 .replace("${text}", escape(message));
299 })
300 .toList();
301 }
302
303
304
305
306
307
308
309 private static Map<String, String> getMessages(ModuleDetails moduleDetails) {
310 final String fullQualifiedName = moduleDetails.getFullQualifiedName();
311 final Map<String, String> result = new LinkedHashMap<>();
312 try {
313 final int lastDot = fullQualifiedName.lastIndexOf('.');
314 final String packageName = fullQualifiedName.substring(0, lastDot);
315 final String bundleName = packageName + ".messages";
316 final Class<?> moduleClass = Class.forName(fullQualifiedName);
317 final ResourceBundle bundle = ResourceBundle.getBundle(
318 bundleName,
319 Locale.ROOT,
320 moduleClass.getClassLoader(),
321 new LocalizedMessage.Utf8Control()
322 );
323 for (String key : moduleDetails.getViolationMessageKeys()) {
324 result.put(key, bundle.getString(key));
325 }
326 }
327 catch (ClassNotFoundException | MissingResourceException ignored) {
328
329
330 }
331 return result;
332 }
333
334
335
336
337
338
339
340 private static String replaceVersionString(String report) {
341 final String version = SarifLogger.class.getPackage().getImplementationVersion();
342 return report.replace(VERSION_PLACEHOLDER, Objects.toString(version, "null"));
343 }
344
345 @Override
346 public void addError(AuditEvent event) {
347 final RuleKey ruleKey = cacheRuleMetadata(event);
348 final String message = generateMessage(ruleKey, event);
349 if (event.getColumn() > 0) {
350 results.add(fillTemplate(resultLineColumn, Map.of(
351 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
352 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
353 COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()),
354 LINE_PLACEHOLDER, Integer.toString(event.getLine()),
355 MESSAGE_PLACEHOLDER, message,
356 RULE_ID_PLACEHOLDER, ruleKey.toRuleId())));
357 }
358 else {
359 results.add(fillTemplate(resultLineOnly, Map.of(
360 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
361 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
362 LINE_PLACEHOLDER, Integer.toString(event.getLine()),
363 MESSAGE_PLACEHOLDER, message,
364 RULE_ID_PLACEHOLDER, ruleKey.toRuleId())));
365 }
366 }
367
368
369
370
371
372
373
374 private RuleKey cacheRuleMetadata(AuditEvent event) {
375 final String sourceName = event.getSourceName();
376 final RuleKey key = new RuleKey(sourceName, event.getModuleId());
377 final ModuleDetails module = allModuleMetadata.get(sourceName);
378 ruleMetadata.putIfAbsent(key, module);
379 return key;
380 }
381
382
383
384
385
386
387
388
389 private String generateMessage(RuleKey ruleKey, AuditEvent event) {
390 final String violationKey = event.getViolation().getKey();
391 final ModuleDetails module = ruleMetadata.get(ruleKey);
392 final String result;
393 if (module != null && module.getViolationMessageKeys().contains(violationKey)) {
394 result = messageWithId
395 .replace(MESSAGE_ID_PLACEHOLDER, violationKey)
396 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
397 }
398 else {
399 result = messageTextOnly
400 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
401 }
402 return result;
403 }
404
405 @Override
406 public void addException(AuditEvent event, Throwable throwable) {
407 final StringWriter stringWriter = new StringWriter();
408 final PrintWriter printer = new PrintWriter(stringWriter);
409 throwable.printStackTrace(printer);
410 final String message = messageTextOnly
411 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(stringWriter.toString()));
412 if (event.getFileName() == null) {
413 results.add(fillTemplate(resultErrorOnly, Map.of(
414 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
415 MESSAGE_PLACEHOLDER, message)));
416 }
417 else {
418 results.add(fillTemplate(resultFileOnly, Map.of(
419 SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()),
420 URI_PLACEHOLDER, renderFileNameUri(event.getFileName()),
421 MESSAGE_PLACEHOLDER, message)));
422 }
423 }
424
425 @Override
426 public void fileStarted(AuditEvent event) {
427
428 }
429
430 @Override
431 public void fileFinished(AuditEvent event) {
432
433 }
434
435
436
437
438
439
440
441
442
443
444
445 private static String fillTemplate(String template, Map<String, String> values) {
446 final Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
447 final StringBuilder result = new StringBuilder(256);
448 while (matcher.find()) {
449 final String placeholder = matcher.group();
450 final String value = values.getOrDefault(placeholder, placeholder);
451 matcher.appendReplacement(result, Matcher.quoteReplacement(value));
452 }
453 matcher.appendTail(result);
454 return result.toString();
455 }
456
457
458
459
460
461
462
463 private static String renderFileNameUri(final String fileName) {
464 final String withoutSpaces =
465 A_SPACE_PATTERN
466 .matcher(TWO_BACKSLASHES_PATTERN.matcher(fileName).replaceAll("/"))
467 .replaceAll("%20");
468 String normalized = A_QUOTE_PATTERN.matcher(withoutSpaces).replaceAll("%22");
469 if (WINDOWS_DRIVE_LETTER_PATTERN.matcher(normalized).find()) {
470 normalized = '/' + normalized;
471 }
472 return "file:" + normalized;
473 }
474
475
476
477
478
479
480
481 private static String renderSeverityLevel(SeverityLevel severityLevel) {
482 return switch (severityLevel) {
483 case IGNORE -> "none";
484 case INFO -> "note";
485 case WARNING -> "warning";
486 case ERROR -> "error";
487 };
488 }
489
490
491
492
493
494
495
496
497 public static String escape(String value) {
498 final int length = value.length();
499 final StringBuilder sb = new StringBuilder(length);
500 for (int i = 0; i < length; i++) {
501 final char chr = value.charAt(i);
502 final String replacement = switch (chr) {
503 case '"' -> "\\\"";
504 case '\\' -> TWO_BACKSLASHES;
505 case '\b' -> "\\b";
506 case '\f' -> "\\f";
507 case '\n' -> "\\n";
508 case '\r' -> "\\r";
509 case '\t' -> "\\t";
510 case '/' -> "\\/";
511 default -> {
512 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
513 yield escapeUnicode1F(chr);
514 }
515 yield Character.toString(chr);
516 }
517 };
518 sb.append(replacement);
519 }
520
521 return sb.toString();
522 }
523
524
525
526
527
528
529
530 private static String escapeUnicode1F(char chr) {
531 final String hexString = Integer.toHexString(chr);
532 return "\\u"
533 + "0".repeat(UNICODE_LENGTH - hexString.length())
534 + hexString.toUpperCase(Locale.US);
535 }
536
537
538
539
540
541
542
543
544 public static String readResource(String name) throws IOException {
545 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
546 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
547 if (inputStream == null) {
548 throw new IOException("Cannot find the resource " + name);
549 }
550 final byte[] buffer = new byte[BUFFER_SIZE];
551 int length = 0;
552 while (length != -1) {
553 result.write(buffer, 0, length);
554 length = inputStream.read(buffer);
555 }
556 return result.toString(StandardCharsets.UTF_8);
557 }
558 }
559
560
561
562
563
564
565
566 private record RuleKey(String sourceName, String moduleId) {
567
568
569
570
571
572 private String toRuleId() {
573 final String result;
574 if (moduleId == null) {
575 result = sourceName;
576 }
577 else {
578 result = sourceName + '#' + moduleId;
579 }
580 return result;
581 }
582 }
583
584 }