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.SeverityLevel; 037 038/** 039 * Simple SARIF logger. 040 * SARIF stands for the static analysis results interchange format. 041 * See <a href="https://sarifweb.azurewebsites.net/">reference</a> 042 */ 043public class SarifLogger extends AbstractAutomaticBean implements AuditListener { 044 045 /** The length of unicode placeholder. */ 046 private static final int UNICODE_LENGTH = 4; 047 048 /** Unicode escaping upper limit. */ 049 private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F; 050 051 /** Input stream buffer size. */ 052 private static final int BUFFER_SIZE = 1024; 053 054 /** The placeholder for message. */ 055 private static final String MESSAGE_PLACEHOLDER = "${message}"; 056 057 /** The placeholder for severity level. */ 058 private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}"; 059 060 /** The placeholder for uri. */ 061 private static final String URI_PLACEHOLDER = "${uri}"; 062 063 /** The placeholder for line. */ 064 private static final String LINE_PLACEHOLDER = "${line}"; 065 066 /** The placeholder for column. */ 067 private static final String COLUMN_PLACEHOLDER = "${column}"; 068 069 /** The placeholder for rule id. */ 070 private static final String RULE_ID_PLACEHOLDER = "${ruleId}"; 071 072 /** The placeholder for version. */ 073 private static final String VERSION_PLACEHOLDER = "${version}"; 074 075 /** The placeholder for results. */ 076 private static final String RESULTS_PLACEHOLDER = "${results}"; 077 078 /** Helper writer that allows easy encoding and printing. */ 079 private final PrintWriter writer; 080 081 /** Close output stream in auditFinished. */ 082 private final boolean closeStream; 083 084 /** The results. */ 085 private final List<String> results = new ArrayList<>(); 086 087 /** Content for the entire report. */ 088 private final String report; 089 090 /** Content for result representing an error with source line and column. */ 091 private final String resultLineColumn; 092 093 /** Content for result representing an error with source line only. */ 094 private final String resultLineOnly; 095 096 /** Content for result representing an error with filename only and without source location. */ 097 private final String resultFileOnly; 098 099 /** Content for result representing an error without filename or location. */ 100 private final String resultErrorOnly; 101 102 /** 103 * Creates a new {@code SarifLogger} instance. 104 * 105 * @param outputStream where to log audit events 106 * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished() 107 * @throws IllegalArgumentException if outputStreamOptions is null 108 * @throws IOException if there is reading errors. 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 // No code by default 132 } 133 134 @Override 135 public void auditStarted(AuditEvent event) { 136 // No code by default 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 // No need to implement this method in this class 200 } 201 202 @Override 203 public void fileFinished(AuditEvent event) { 204 // No need to implement this method in this class 205 } 206 207 /** 208 * Render the severity level into SARIF severity level. 209 * 210 * @param severityLevel the Severity level. 211 * @return the rendered severity level in string. 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 * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F. 235 * See <a href="https://www.ietf.org/rfc/rfc4627.txt">reference</a> - 2.5. Strings 236 * 237 * @param value the value to escape. 238 * @return the escaped value if necessary. 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 * Escape the character between 0x00 to 0x1F in JSON. 285 * 286 * @param chr the character to be escaped. 287 * @return the escaped string. 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 * Read string from given resource. 298 * 299 * @param name name of the desired resource 300 * @return the string content from the give resource 301 * @throws IOException if there is reading errors 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}