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