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 final String version = SarifLogger.class.getPackage().getImplementationVersion();
174 final String rendered = report
175 .replace(VERSION_PLACEHOLDER, String.valueOf(version))
176 .replace(RESULTS_PLACEHOLDER, String.join(",\n", results));
177 writer.print(rendered);
178 if (closeStream) {
179 writer.close();
180 }
181 else {
182 writer.flush();
183 }
184 }
185
186 @Override
187 public void addError(AuditEvent event) {
188 if (event.getColumn() > 0) {
189 results.add(resultLineColumn
190 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
191 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
192 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()))
193 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
194 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
195 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
196 );
197 }
198 else {
199 results.add(resultLineOnly
200 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
201 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
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 }
208
209 @Override
210 public void addException(AuditEvent event, Throwable throwable) {
211 final StringWriter stringWriter = new StringWriter();
212 final PrintWriter printer = new PrintWriter(stringWriter);
213 throwable.printStackTrace(printer);
214 if (event.getFileName() == null) {
215 results.add(resultErrorOnly
216 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
217 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
218 );
219 }
220 else {
221 results.add(resultFileOnly
222 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
223 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
224 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
225 );
226 }
227 }
228
229 @Override
230 public void fileStarted(AuditEvent event) {
231
232 }
233
234 @Override
235 public void fileFinished(AuditEvent event) {
236
237 }
238
239
240
241
242
243
244
245 private static String renderFileNameUri(final String fileName) {
246 String normalized =
247 A_SPACE_PATTERN
248 .matcher(TWO_BACKSLASHES_PATTERN.matcher(fileName).replaceAll("/"))
249 .replaceAll("%20");
250 if (WINDOWS_DRIVE_LETTER_PATTERN.matcher(normalized).find()) {
251 normalized = '/' + normalized;
252 }
253 return "file:" + normalized;
254 }
255
256
257
258
259
260
261
262 private static String renderSeverityLevel(SeverityLevel severityLevel) {
263 final String renderedSeverityLevel;
264 switch (severityLevel) {
265 case IGNORE:
266 renderedSeverityLevel = "none";
267 break;
268 case INFO:
269 renderedSeverityLevel = "note";
270 break;
271 case WARNING:
272 renderedSeverityLevel = "warning";
273 break;
274 case ERROR:
275 default:
276 renderedSeverityLevel = "error";
277 break;
278 }
279 return renderedSeverityLevel;
280 }
281
282
283
284
285
286
287
288
289 public static String escape(String value) {
290 final int length = value.length();
291 final StringBuilder sb = new StringBuilder(length);
292 for (int i = 0; i < length; i++) {
293 final char chr = value.charAt(i);
294 switch (chr) {
295 case '"':
296 sb.append("\\\"");
297 break;
298 case '\\':
299 sb.append(TWO_BACKSLASHES);
300 break;
301 case '\b':
302 sb.append("\\b");
303 break;
304 case '\f':
305 sb.append("\\f");
306 break;
307 case '\n':
308 sb.append("\\n");
309 break;
310 case '\r':
311 sb.append("\\r");
312 break;
313 case '\t':
314 sb.append("\\t");
315 break;
316 case '/':
317 sb.append("\\/");
318 break;
319 default:
320 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
321 sb.append(escapeUnicode1F(chr));
322 }
323 else {
324 sb.append(chr);
325 }
326 break;
327 }
328 }
329 return sb.toString();
330 }
331
332
333
334
335
336
337
338 private static String escapeUnicode1F(char chr) {
339 final String hexString = Integer.toHexString(chr);
340 return "\\u"
341 + "0".repeat(UNICODE_LENGTH - hexString.length())
342 + hexString.toUpperCase(Locale.US);
343 }
344
345
346
347
348
349
350
351
352 public static String readResource(String name) throws IOException {
353 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
354 ByteArrayOutputStream result = new ByteArrayOutputStream()) {
355 if (inputStream == null) {
356 throw new IOException("Cannot find the resource " + name);
357 }
358 final byte[] buffer = new byte[BUFFER_SIZE];
359 int length = inputStream.read(buffer);
360 while (length != -1) {
361 result.write(buffer, 0, length);
362 length = inputStream.read(buffer);
363 }
364 return result.toString(StandardCharsets.UTF_8);
365 }
366 }
367 }