001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle; 021 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.io.OutputStreamWriter; 027import java.io.PrintWriter; 028import java.io.StringWriter; 029import java.nio.charset.StandardCharsets; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Locale; 033 034import com.puppycrawl.tools.checkstyle.api.AuditEvent; 035import com.puppycrawl.tools.checkstyle.api.AuditListener; 036import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 037import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 038 039/** 040 * Simple SARIF logger. 041 * SARIF stands for the static analysis results interchange format. 042 * See <a href="https://sarifweb.azurewebsites.net/">reference</a> 043 */ 044public class SarifLogger extends AbstractAutomaticBean implements AuditListener { 045 046 /** The length of unicode placeholder. */ 047 private static final int UNICODE_LENGTH = 4; 048 049 /** Unicode escaping upper limit. */ 050 private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F; 051 052 /** Input stream buffer size. */ 053 private static final int BUFFER_SIZE = 1024; 054 055 /** The placeholder for message. */ 056 private static final String MESSAGE_PLACEHOLDER = "${message}"; 057 058 /** The placeholder for severity level. */ 059 private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}"; 060 061 /** The placeholder for uri. */ 062 private static final String URI_PLACEHOLDER = "${uri}"; 063 064 /** The placeholder for line. */ 065 private static final String LINE_PLACEHOLDER = "${line}"; 066 067 /** The placeholder for column. */ 068 private static final String COLUMN_PLACEHOLDER = "${column}"; 069 070 /** The placeholder for rule id. */ 071 private static final String RULE_ID_PLACEHOLDER = "${ruleId}"; 072 073 /** The placeholder for version. */ 074 private static final String VERSION_PLACEHOLDER = "${version}"; 075 076 /** The placeholder for results. */ 077 private static final String RESULTS_PLACEHOLDER = "${results}"; 078 079 /** Helper writer that allows easy encoding and printing. */ 080 private final PrintWriter writer; 081 082 /** Close output stream in auditFinished. */ 083 private final boolean closeStream; 084 085 /** The results. */ 086 private final List<String> results = new ArrayList<>(); 087 088 /** Content for the entire report. */ 089 private final String report; 090 091 /** Content for result representing an error with source line and column. */ 092 private final String resultLineColumn; 093 094 /** Content for result representing an error with source line only. */ 095 private final String resultLineOnly; 096 097 /** Content for result representing an error with filename only and without source location. */ 098 private final String resultFileOnly; 099 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}