001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.OutputStream; 023import java.io.OutputStreamWriter; 024import java.io.PrintWriter; 025import java.nio.charset.StandardCharsets; 026import java.nio.file.Path; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.Map; 030import java.util.Set; 031 032import com.puppycrawl.tools.checkstyle.api.AuditEvent; 033import com.puppycrawl.tools.checkstyle.api.AuditListener; 034 035/** 036 * Generates <b>suppressions.xml</b> file, based on violations occurred. 037 * See issue <a href="https://github.com/checkstyle/checkstyle/issues/5983">#5983</a> 038 */ 039public final class ChecksAndFilesSuppressionFileGeneratorAuditListener 040 extends AbstractAutomaticBean 041 implements AuditListener { 042 043 /** The " quote character. */ 044 private static final String QUOTE_CHAR = "\""; 045 046 /** 047 * Helper writer that allows easy encoding and printing. 048 */ 049 private final PrintWriter writer; 050 051 /** Close output stream in auditFinished. */ 052 private final boolean closeStream; 053 054 /** 055 * Collects the check names corrosponds to file name. 056 */ 057 private final Map<Path, Set<String>> filesAndChecksCollector = new HashMap<>(); 058 059 /** 060 * Collects the module ids corrosponds to file name. 061 */ 062 private final Map<Path, Set<String>> filesAndModuleIdCollector = new HashMap<>(); 063 064 /** Determines if xml header is printed. */ 065 private boolean isXmlHeaderPrinted; 066 067 /** 068 * Creates a new {@code ChecksAndFilesSuppressionFileGeneratorAuditListener} instance. 069 * Sets the output to a defined stream. 070 * 071 * @param out the output stream 072 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished() 073 * @throws IllegalArgumentException if outputStreamOptions is null. 074 */ 075 public ChecksAndFilesSuppressionFileGeneratorAuditListener(OutputStream out, 076 OutputStreamOptions outputStreamOptions) { 077 if (outputStreamOptions == null) { 078 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null"); 079 } 080 081 writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 082 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 083 } 084 085 @Override 086 public void fileStarted(AuditEvent event) { 087 // No code by default 088 } 089 090 @Override 091 public void fileFinished(AuditEvent event) { 092 // No code by default 093 } 094 095 @Override 096 public void auditStarted(AuditEvent event) { 097 // No code by default 098 } 099 100 @Override 101 public void auditFinished(AuditEvent event) { 102 if (isXmlHeaderPrinted) { 103 writer.println("</suppressions>"); 104 } 105 106 writer.flush(); 107 if (closeStream) { 108 writer.close(); 109 } 110 } 111 112 @Override 113 public void addError(AuditEvent event) { 114 printXmlHeader(); 115 116 final Path path = Path.of(event.getFileName()); 117 final Path fileName = path.getFileName(); 118 final String checkName = 119 PackageObjectFactory.getShortFromFullModuleNames(event.getSourceName()); 120 final String moduleIdName = event.getModuleId(); 121 122 final boolean isAlreadyPresent; 123 124 if (fileName != null) { 125 if (moduleIdName == null) { 126 isAlreadyPresent = isFileAndCheckNamePresent(fileName, checkName); 127 } 128 else { 129 isAlreadyPresent = isFileAndModuleIdPresent(fileName, moduleIdName); 130 } 131 } 132 else { 133 isAlreadyPresent = true; 134 } 135 136 if (!isAlreadyPresent) { 137 suppressXmlWriter(fileName, checkName, moduleIdName); 138 } 139 } 140 141 /** 142 * Checks whether the check name is already associated with the given file 143 * in the {@code FilesAndChecksCollector} map. 144 * 145 * @param fileName The path of the file where the violation occurred. 146 * @param checkName The name of the check that triggered the violation. 147 * @return {@code true} if the collector already contains the check name for the file, 148 * {@code false} otherwise. 149 */ 150 private boolean isFileAndCheckNamePresent(Path fileName, String checkName) { 151 boolean isPresent = false; 152 final Set<String> checks = filesAndChecksCollector.get(fileName); 153 if (checks != null) { 154 isPresent = checks.contains(checkName); 155 } 156 return isPresent; 157 } 158 159 /** 160 * Checks the {@code FilesAndModuleIdCollector} map to see if the module ID has 161 * already been recorded for the specified file. 162 * 163 * @param fileName The path of the file where the violation occurred. 164 * @param moduleIdName The module ID associated with the check name which trigger violation. 165 * @return {@code true} if the module ID is not yet recorded for the file, 166 * {@code false} otherwise. 167 */ 168 private boolean isFileAndModuleIdPresent(Path fileName, String moduleIdName) { 169 boolean isPresent = false; 170 final Set<String> moduleIds = filesAndModuleIdCollector.get(fileName); 171 if (moduleIds != null) { 172 isPresent = moduleIds.contains(moduleIdName); 173 } 174 return isPresent; 175 } 176 177 @Override 178 public void addException(AuditEvent event, Throwable throwable) { 179 throw new UnsupportedOperationException("Operation is not supported"); 180 } 181 182 /** 183 * Prints XML suppression with check/id and file name. 184 * 185 * @param fileName The file path associated with the check or module ID. 186 * @param checkName The check name to write if {@code moduleIdName} is {@code null}. 187 * @param moduleIdName The module ID name to write if {@code null}, {@code checkName} is 188 * used instead. 189 */ 190 private void suppressXmlWriter(Path fileName, String checkName, String moduleIdName) { 191 writer.println(" <suppress"); 192 writer.print(" files=\""); 193 writer.print(fileName); 194 writer.println(QUOTE_CHAR); 195 196 if (moduleIdName == null) { 197 writer.print(" checks=\""); 198 writer.print(checkName); 199 } 200 else { 201 writer.print(" id=\""); 202 writer.print(moduleIdName); 203 } 204 writer.println("\"/>"); 205 addCheckOrModuleId(fileName, checkName, moduleIdName); 206 } 207 208 /** 209 * Adds either the check name or module ID to the corresponding collector map 210 * for the specified file path. 211 * 212 * @param fileName The path of the file associated with the check or module ID. 213 * @param checkName The name of the check to add if {@code moduleIdName} is {@code null}. 214 * @param moduleIdName The name of the module ID to add if {@code null}, {@code checkName} is 215 * used instead. 216 */ 217 private void addCheckOrModuleId(Path fileName, String checkName, String moduleIdName) { 218 if (moduleIdName == null) { 219 addToCollector(filesAndChecksCollector, fileName, checkName); 220 } 221 else { 222 addToCollector(filesAndModuleIdCollector, fileName, moduleIdName); 223 } 224 } 225 226 /** 227 * Adds a value (either a check name or module ID) to the set associated with the given file 228 * in the specified collector map. 229 * 230 * @param collector The map that collects values (check names or module IDs) for each file. 231 * @param fileName The file path for which the value should be recorded. 232 * @param value the check name or module ID to add to the set for the specified file. 233 */ 234 private static void addToCollector(Map<Path, Set<String>> collector, 235 Path fileName, String value) { 236 final Set<String> values = collector.computeIfAbsent(fileName, key -> new HashSet<>()); 237 values.add(value); 238 } 239 240 /** 241 * Prints XML header if only it was not printed before. 242 */ 243 private void printXmlHeader() { 244 if (!isXmlHeaderPrinted) { 245 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 246 writer.println("<!DOCTYPE suppressions PUBLIC"); 247 writer.println(" \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\""); 248 writer.println(" \"https://checkstyle.org/dtds/suppressions_1_2.dtd\">"); 249 writer.println("<suppressions>"); 250 isXmlHeaderPrinted = true; 251 } 252 } 253 254 @Override 255 protected void finishLocalSetup() { 256 // No code by default 257 } 258}