001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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 AutomaticBean 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 */ 111 public SarifLogger( 112 OutputStream outputStream, 113 OutputStreamOptions outputStreamOptions) throws IOException { 114 if (outputStreamOptions == null) { 115 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null"); 116 } 117 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); 118 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 119 report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template"); 120 resultLineColumn = 121 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template"); 122 resultLineOnly = 123 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template"); 124 resultFileOnly = 125 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template"); 126 resultErrorOnly = 127 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template"); 128 } 129 130 @Override 131 protected void finishLocalSetup() { 132 // No code by default 133 } 134 135 @Override 136 public void auditStarted(AuditEvent event) { 137 // No code by default 138 } 139 140 @Override 141 public void auditFinished(AuditEvent event) { 142 final String version = SarifLogger.class.getPackage().getImplementationVersion(); 143 final String rendered = report 144 .replace(VERSION_PLACEHOLDER, String.valueOf(version)) 145 .replace(RESULTS_PLACEHOLDER, String.join(",\n", results)); 146 writer.print(rendered); 147 if (closeStream) { 148 writer.close(); 149 } 150 else { 151 writer.flush(); 152 } 153 } 154 155 @Override 156 public void addError(AuditEvent event) { 157 if (event.getColumn() > 0) { 158 results.add(resultLineColumn 159 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 160 .replace(URI_PLACEHOLDER, event.getFileName()) 161 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn())) 162 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 163 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 164 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 165 ); 166 } 167 else { 168 results.add(resultLineOnly 169 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 170 .replace(URI_PLACEHOLDER, event.getFileName()) 171 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 172 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 173 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 174 ); 175 } 176 } 177 178 @Override 179 public void addException(AuditEvent event, Throwable throwable) { 180 final StringWriter stringWriter = new StringWriter(); 181 final PrintWriter printer = new PrintWriter(stringWriter); 182 throwable.printStackTrace(printer); 183 if (event.getFileName() == null) { 184 results.add(resultErrorOnly 185 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 186 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 187 ); 188 } 189 else { 190 results.add(resultFileOnly 191 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 192 .replace(URI_PLACEHOLDER, event.getFileName()) 193 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 194 ); 195 } 196 } 197 198 @Override 199 public void fileStarted(AuditEvent event) { 200 // No need to implement this method in this class 201 } 202 203 @Override 204 public void fileFinished(AuditEvent event) { 205 // No need to implement this method in this class 206 } 207 208 /** 209 * Render the severity level into SARIF severity level. 210 * 211 * @param severityLevel the Severity level. 212 * @return the rendered severity level in string. 213 */ 214 private static String renderSeverityLevel(SeverityLevel severityLevel) { 215 final String renderedSeverityLevel; 216 switch (severityLevel) { 217 case IGNORE: 218 renderedSeverityLevel = "none"; 219 break; 220 case INFO: 221 renderedSeverityLevel = "note"; 222 break; 223 case WARNING: 224 renderedSeverityLevel = "warning"; 225 break; 226 case ERROR: 227 default: 228 renderedSeverityLevel = "error"; 229 break; 230 } 231 return renderedSeverityLevel; 232 } 233 234 /** 235 * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F. 236 * See <a href="https://www.ietf.org/rfc/rfc4627.txt">reference</a> - 2.5. Strings 237 * 238 * @param value the value to escape. 239 * @return the escaped value if necessary. 240 */ 241 public static String escape(String value) { 242 final int length = value.length(); 243 final StringBuilder sb = new StringBuilder(length); 244 for (int i = 0; i < length; i++) { 245 final char chr = value.charAt(i); 246 switch (chr) { 247 case '"': 248 sb.append("\\\""); 249 break; 250 case '\\': 251 sb.append("\\\\"); 252 break; 253 case '\b': 254 sb.append("\\b"); 255 break; 256 case '\f': 257 sb.append("\\f"); 258 break; 259 case '\n': 260 sb.append("\\n"); 261 break; 262 case '\r': 263 sb.append("\\r"); 264 break; 265 case '\t': 266 sb.append("\\t"); 267 break; 268 case '/': 269 sb.append("\\/"); 270 break; 271 default: 272 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) { 273 sb.append(escapeUnicode1F(chr)); 274 } 275 else { 276 sb.append(chr); 277 } 278 break; 279 } 280 } 281 return sb.toString(); 282 } 283 284 /** 285 * Escape the character between 0x00 to 0x1F in JSON. 286 * 287 * @param chr the character to be escaped. 288 * @return the escaped string. 289 */ 290 private static String escapeUnicode1F(char chr) { 291 final String hexString = Integer.toHexString(chr); 292 return "\\u" 293 + "0".repeat(UNICODE_LENGTH - hexString.length()) 294 + hexString.toUpperCase(Locale.US); 295 } 296 297 /** 298 * Read string from given resource. 299 * 300 * @param name name of the desired resource 301 * @return the string content from the give resource 302 * @throws IOException if there is reading errors 303 */ 304 public static String readResource(String name) throws IOException { 305 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name); 306 ByteArrayOutputStream result = new ByteArrayOutputStream()) { 307 if (inputStream == null) { 308 throw new IOException("Cannot find the resource " + name); 309 } 310 final byte[] buffer = new byte[BUFFER_SIZE]; 311 int length = inputStream.read(buffer); 312 while (length != -1) { 313 result.write(buffer, 0, length); 314 length = inputStream.read(buffer); 315 } 316 return result.toString(StandardCharsets.UTF_8); 317 } 318 } 319}