View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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   * Simple SARIF logger.
41   * SARIF stands for the static analysis results interchange format.
42   * See <a href="https://sarifweb.azurewebsites.net/">reference</a>
43   */
44  public class SarifLogger extends AbstractAutomaticBean implements AuditListener {
45  
46      /** The length of unicode placeholder. */
47      private static final int UNICODE_LENGTH = 4;
48  
49      /** Unicode escaping upper limit. */
50      private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
51  
52      /** Input stream buffer size. */
53      private static final int BUFFER_SIZE = 1024;
54  
55      /** The placeholder for message. */
56      private static final String MESSAGE_PLACEHOLDER = "${message}";
57  
58      /** The placeholder for severity level. */
59      private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
60  
61      /** The placeholder for uri. */
62      private static final String URI_PLACEHOLDER = "${uri}";
63  
64      /** The placeholder for line. */
65      private static final String LINE_PLACEHOLDER = "${line}";
66  
67      /** The placeholder for column. */
68      private static final String COLUMN_PLACEHOLDER = "${column}";
69  
70      /** The placeholder for rule id. */
71      private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
72  
73      /** The placeholder for version. */
74      private static final String VERSION_PLACEHOLDER = "${version}";
75  
76      /** The placeholder for results. */
77      private static final String RESULTS_PLACEHOLDER = "${results}";
78  
79      /** Helper writer that allows easy encoding and printing. */
80      private final PrintWriter writer;
81  
82      /** Close output stream in auditFinished. */
83      private final boolean closeStream;
84  
85      /** The results. */
86      private final List<String> results = new ArrayList<>();
87  
88      /** Content for the entire report. */
89      private final String report;
90  
91      /** Content for result representing an error with source line and column. */
92      private final String resultLineColumn;
93  
94      /** Content for result representing an error with source line only. */
95      private final String resultLineOnly;
96  
97      /** Content for result representing an error with filename only and without source location. */
98      private final String resultFileOnly;
99  
100     /** Content for result representing an error without filename or location. */
101     private final String resultErrorOnly;
102 
103     /**
104      * Creates a new {@code SarifLogger} instance.
105      *
106      * @param outputStream where to log audit events
107      * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
108      * @throws IllegalArgumentException if outputStreamOptions is null
109      * @throws IOException if there is reading errors.
110      * @noinspection deprecation
111      * @noinspectionreason We are forced to keep AutomaticBean compatability
112      *     because of maven-checkstyle-plugin. Until #12873.
113      */
114     public SarifLogger(
115         OutputStream outputStream,
116         AutomaticBean.OutputStreamOptions outputStreamOptions) throws IOException {
117         this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
118     }
119 
120     /**
121      * Creates a new {@code SarifLogger} instance.
122      *
123      * @param outputStream where to log audit events
124      * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
125      * @throws IllegalArgumentException if outputStreamOptions is null
126      * @throws IOException if there is reading errors.
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         // No code by default
150     }
151 
152     @Override
153     public void auditStarted(AuditEvent event) {
154         // No code by default
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         // No need to implement this method in this class
218     }
219 
220     @Override
221     public void fileFinished(AuditEvent event) {
222         // No need to implement this method in this class
223     }
224 
225     /**
226      * Render the severity level into SARIF severity level.
227      *
228      * @param severityLevel the Severity level.
229      * @return the rendered severity level in string.
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      * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F.
253      * See <a href="https://www.ietf.org/rfc/rfc4627.txt">reference</a> - 2.5. Strings
254      *
255      * @param value the value to escape.
256      * @return the escaped value if necessary.
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      * Escape the character between 0x00 to 0x1F in JSON.
303      *
304      * @param chr the character to be escaped.
305      * @return the escaped string.
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      * Read string from given resource.
316      *
317      * @param name name of the desired resource
318      * @return the string content from the give resource
319      * @throws IOException if there is reading errors
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 }