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