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.List;
32 import java.util.Locale;
33 import java.util.regex.Pattern;
34
35 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
36 import com.puppycrawl.tools.checkstyle.api.AuditListener;
37 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
38 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
39
40
41
42
43
44
45 public class SarifLogger extends AbstractAutomaticBean implements AuditListener {
46
47
48 private static final int UNICODE_LENGTH = 4;
49
50
51 private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
52
53
54 private static final int BUFFER_SIZE = 1024;
55
56
57 private static final String MESSAGE_PLACEHOLDER = "${message}";
58
59
60 private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
61
62
63 private static final String URI_PLACEHOLDER = "${uri}";
64
65
66 private static final String LINE_PLACEHOLDER = "${line}";
67
68
69 private static final String COLUMN_PLACEHOLDER = "${column}";
70
71
72 private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
73
74
75 private static final String VERSION_PLACEHOLDER = "${version}";
76
77
78 private static final String RESULTS_PLACEHOLDER = "${results}";
79
80
81 private static final String TWO_BACKSLASHES = "\\\\";
82
83
84 private static final Pattern A_SPACE_PATTERN = Pattern.compile(" ");
85
86
87 private static final Pattern TWO_BACKSLASHES_PATTERN = Pattern.compile(TWO_BACKSLASHES);
88
89
90 private static final Pattern WINDOWS_DRIVE_LETTER_PATTERN =
91 Pattern.compile("\\A[A-Z]:", Pattern.CASE_INSENSITIVE);
92
93
94 private final PrintWriter writer;
95
96
97 private final boolean closeStream;
98
99
100 private final List<String> results = new ArrayList<>();
101
102
103 private final String report;
104
105
106 private final String resultLineColumn;
107
108
109 private final String resultLineOnly;
110
111
112 private final String resultFileOnly;
113
114
115 private final String resultErrorOnly;
116
117
118
119
120
121
122
123
124
125
126
127
128 public SarifLogger(
129 OutputStream outputStream,
130 AutomaticBean.OutputStreamOptions outputStreamOptions) throws IOException {
131 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
132 }
133
134
135
136
137
138
139
140
141
142 public SarifLogger(
143 OutputStream outputStream,
144 OutputStreamOptions outputStreamOptions) throws IOException {
145 if (outputStreamOptions == null) {
146 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
147 }
148 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
149 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
150 report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
151 resultLineColumn =
152 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
153 resultLineOnly =
154 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
155 resultFileOnly =
156 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
157 resultErrorOnly =
158 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
159 }
160
161 @Override
162 protected void finishLocalSetup() {
163
164 }
165
166 @Override
167 public void auditStarted(AuditEvent event) {
168
169 }
170
171 @Override
172 public void auditFinished(AuditEvent event) {
173 String rendered = replaceVersionString(report);
174 rendered = rendered.replace(RESULTS_PLACEHOLDER, String.join(",\n", results));
175 writer.print(rendered);
176 if (closeStream) {
177 writer.close();
178 }
179 else {
180 writer.flush();
181 }
182 }
183
184
185
186
187
188
189
190 private static String replaceVersionString(String report) {
191 final String version = SarifLogger.class.getPackage().getImplementationVersion();
192 return report.replace(VERSION_PLACEHOLDER, String.valueOf(version));
193 }
194
195 @Override
196 public void addError(AuditEvent event) {
197 if (event.getColumn() > 0) {
198 results.add(resultLineColumn
199 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
200 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
201 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()))
202 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
203 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
204 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
205 );
206 }
207 else {
208 results.add(resultLineOnly
209 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
210 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
211 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
212 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
213 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
214 );
215 }
216 }
217
218 @Override
219 public void addException(AuditEvent event, Throwable throwable) {
220 final StringWriter stringWriter = new StringWriter();
221 final PrintWriter printer = new PrintWriter(stringWriter);
222 throwable.printStackTrace(printer);
223 if (event.getFileName() == null) {
224 results.add(resultErrorOnly
225 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
226 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
227 );
228 }
229 else {
230 results.add(resultFileOnly
231 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
232 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
233 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
234 );
235 }
236 }
237
238 @Override
239 public void fileStarted(AuditEvent event) {
240
241 }
242
243 @Override
244 public void fileFinished(AuditEvent event) {
245
246 }
247
248
249
250
251
252
253
254 private static String renderFileNameUri(final String fileName) {
255 String normalized =
256 A_SPACE_PATTERN
257 .matcher(TWO_BACKSLASHES_PATTERN.matcher(fileName).replaceAll("/"))
258 .replaceAll("%20");
259 if (WINDOWS_DRIVE_LETTER_PATTERN.matcher(normalized).find()) {
260 normalized = '/' + normalized;
261 }
262 return "file:" + normalized;
263 }
264
265
266
267
268
269
270
271 private static String renderSeverityLevel(SeverityLevel severityLevel) {
272 return switch (severityLevel) {
273 case IGNORE -> "none";
274 case INFO -> "note";
275 case WARNING -> "warning";
276 case ERROR -> "error";
277 };
278 }
279
280
281
282
283
284
285
286
287 public static String escape(String value) {
288 final int length = value.length();
289 final StringBuilder sb = new StringBuilder(length);
290 for (int i = 0; i < length; i++) {
291 final char chr = value.charAt(i);
292 final String replacement = switch (chr) {
293 case '"' -> "\\\"";
294 case '\\' -> TWO_BACKSLASHES;
295 case '\b' -> "\\b";
296 case '\f' -> "\\f";
297 case '\n' -> "\\n";
298 case '\r' -> "\\r";
299 case '\t' -> "\\t";
300 case '/' -> "\\/";
301 default -> {
302 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
303 yield escapeUnicode1F(chr);
304 }
305 yield Character.toString(chr);
306 }
307 };
308 sb.append(replacement);
309 }
310
311 return sb.toString();
312 }
313
314
315
316
317
318
319
320 private static String escapeUnicode1F(char chr) {
321 final String hexString = Integer.toHexString(chr);
322 return "\\u"
323 + "0".repeat(UNICODE_LENGTH - hexString.length())
324 + hexString.toUpperCase(Locale.US);
325 }
326
327
328
329
330
331
332
333
334 public static String readResource(String name) throws IOException {
335 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
336 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
337 if (inputStream == null) {
338 throw new IOException("Cannot find the resource " + name);
339 }
340 final byte[] buffer = new byte[BUFFER_SIZE];
341 int length = 0;
342 while (length != -1) {
343 result.write(buffer, 0, length);
344 length = inputStream.read(buffer);
345 }
346 return result.toString(StandardCharsets.UTF_8);
347 }
348 }
349 }