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 final String renderedSeverityLevel;
273 switch (severityLevel) {
274 case IGNORE:
275 renderedSeverityLevel = "none";
276 break;
277 case INFO:
278 renderedSeverityLevel = "note";
279 break;
280 case WARNING:
281 renderedSeverityLevel = "warning";
282 break;
283 case ERROR:
284 default:
285 renderedSeverityLevel = "error";
286 break;
287 }
288 return renderedSeverityLevel;
289 }
290
291
292
293
294
295
296
297
298 public static String escape(String value) {
299 final int length = value.length();
300 final StringBuilder sb = new StringBuilder(length);
301 for (int i = 0; i < length; i++) {
302 final char chr = value.charAt(i);
303 switch (chr) {
304 case '"':
305 sb.append("\\\"");
306 break;
307 case '\\':
308 sb.append(TWO_BACKSLASHES);
309 break;
310 case '\b':
311 sb.append("\\b");
312 break;
313 case '\f':
314 sb.append("\\f");
315 break;
316 case '\n':
317 sb.append("\\n");
318 break;
319 case '\r':
320 sb.append("\\r");
321 break;
322 case '\t':
323 sb.append("\\t");
324 break;
325 case '/':
326 sb.append("\\/");
327 break;
328 default:
329 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
330 sb.append(escapeUnicode1F(chr));
331 }
332 else {
333 sb.append(chr);
334 }
335 break;
336 }
337 }
338 return sb.toString();
339 }
340
341
342
343
344
345
346
347 private static String escapeUnicode1F(char chr) {
348 final String hexString = Integer.toHexString(chr);
349 return "\\u"
350 + "0".repeat(UNICODE_LENGTH - hexString.length())
351 + hexString.toUpperCase(Locale.US);
352 }
353
354
355
356
357
358
359
360
361 public static String readResource(String name) throws IOException {
362 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
363 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
364 if (inputStream == null) {
365 throw new IOException("Cannot find the resource " + name);
366 }
367 final byte[] buffer = new byte[BUFFER_SIZE];
368 int length = 0;
369 while (length != -1) {
370 result.write(buffer, 0, length);
371 length = inputStream.read(buffer);
372 }
373 return result.toString(StandardCharsets.UTF_8);
374 }
375 }
376 }