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 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 */ 074 public ChecksAndFilesSuppressionFileGeneratorAuditListener(OutputStream out, 075 OutputStreamOptions outputStreamOptions) { 076 writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); 077 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 078 } 079 080 @Override 081 public void fileStarted(AuditEvent event) { 082 // No code by default 083 } 084 085 @Override 086 public void fileFinished(AuditEvent event) { 087 // No code by default 088 } 089 090 @Override 091 public void auditStarted(AuditEvent event) { 092 // No code by default 093 } 094 095 @Override 096 public void auditFinished(AuditEvent event) { 097 if (isXmlHeaderPrinted) { 098 writer.println("</suppressions>"); 099 } 100 101 writer.flush(); 102 if (closeStream) { 103 writer.close(); 104 } 105 } 106 107 @Override 108 public void addError(AuditEvent event) { 109 printXmlHeader(); 110 111 final Path path = Path.of(event.getFileName()); 112 final Path fileName = path.getFileName(); 113 final String checkName = 114 PackageObjectFactory.getShortFromFullModuleNames(event.getSourceName()); 115 final String moduleIdName = event.getModuleId(); 116 117 final boolean isAlreadyPresent; 118 119 if (fileName != null) { 120 if (moduleIdName == null) { 121 isAlreadyPresent = isFileAndCheckNamePresent(fileName, checkName); 122 } 123 else { 124 isAlreadyPresent = isFileAndModuleIdPresent(fileName, moduleIdName); 125 } 126 } 127 else { 128 isAlreadyPresent = true; 129 } 130 131 if (!isAlreadyPresent) { 132 suppressXmlWriter(fileName, checkName, moduleIdName); 133 } 134 } 135 136 /** 137 * Checks whether the check name is already associated with the given file 138 * in the {@code FilesAndChecksCollector} map. 139 * 140 * @param fileName The path of the file where the violation occurred. 141 * @param checkName The name of the check that triggered the violation. 142 * @return {@code true} if the collector already contains the check name for the file, 143 * {@code false} otherwise. 144 */ 145 private boolean isFileAndCheckNamePresent(Path fileName, String checkName) { 146 boolean isPresent = false; 147 final Set<String> checks = filesAndChecksCollector.get(fileName); 148 if (checks != null) { 149 isPresent = checks.contains(checkName); 150 } 151 return isPresent; 152 } 153 154 /** 155 * Checks the {@code FilesAndModuleIdCollector} map to see if the module ID has 156 * already been recorded for the specified file. 157 * 158 * @param fileName The path of the file where the violation occurred. 159 * @param moduleIdName The module ID associated with the check name which trigger violation. 160 * @return {@code true} if the module ID is not yet recorded for the file, 161 * {@code false} otherwise. 162 */ 163 private boolean isFileAndModuleIdPresent(Path fileName, String moduleIdName) { 164 boolean isPresent = false; 165 final Set<String> moduleIds = filesAndModuleIdCollector.get(fileName); 166 if (moduleIds != null) { 167 isPresent = moduleIds.contains(moduleIdName); 168 } 169 return isPresent; 170 } 171 172 @Override 173 public void addException(AuditEvent event, Throwable throwable) { 174 throw new UnsupportedOperationException("Operation is not supported"); 175 } 176 177 /** 178 * Prints XML suppression with check/id and file name. 179 * 180 * @param fileName The file path associated with the check or module ID. 181 * @param checkName The check name to write if {@code moduleIdName} is {@code null}. 182 * @param moduleIdName The module ID name to write if {@code null}, {@code checkName} is 183 * used instead. 184 */ 185 private void suppressXmlWriter(Path fileName, String checkName, String moduleIdName) { 186 writer.println(" <suppress"); 187 writer.print(" files=\""); 188 writer.print(fileName); 189 writer.println(QUOTE_CHAR); 190 191 if (moduleIdName == null) { 192 writer.print(" checks=\""); 193 writer.print(checkName); 194 } 195 else { 196 writer.print(" id=\""); 197 writer.print(moduleIdName); 198 } 199 writer.println("\"/>"); 200 addCheckOrModuleId(fileName, checkName, moduleIdName); 201 } 202 203 /** 204 * Adds either the check name or module ID to the corresponding collector map 205 * for the specified file path. 206 * 207 * @param fileName The path of the file associated with the check or module ID. 208 * @param checkName The name of the check to add if {@code moduleIdName} is {@code null}. 209 * @param moduleIdName The name of the module ID to add if {@code null}, {@code checkName} is 210 * used instead. 211 */ 212 private void addCheckOrModuleId(Path fileName, String checkName, String moduleIdName) { 213 if (moduleIdName == null) { 214 addToCollector(filesAndChecksCollector, fileName, checkName); 215 } 216 else { 217 addToCollector(filesAndModuleIdCollector, fileName, moduleIdName); 218 } 219 } 220 221 /** 222 * Adds a value (either a check name or module ID) to the set associated with the given file 223 * in the specified collector map. 224 * 225 * @param collector The map that collects values (check names or module IDs) for each file. 226 * @param fileName The file path for which the value should be recorded. 227 * @param value the check name or module ID to add to the set for the specified file. 228 */ 229 private static void addToCollector(Map<Path, Set<String>> collector, 230 Path fileName, String value) { 231 final Set<String> values = collector.computeIfAbsent(fileName, key -> new HashSet<>()); 232 values.add(value); 233 } 234 235 /** 236 * Prints XML header if only it was not printed before. 237 */ 238 private void printXmlHeader() { 239 if (!isXmlHeaderPrinted) { 240 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 241 writer.println("<!DOCTYPE suppressions PUBLIC"); 242 writer.println(" \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\""); 243 writer.println(" \"https://checkstyle.org/dtds/configuration_1_3.dtd\">"); 244 writer.println("<suppressions>"); 245 isXmlHeaderPrinted = true; 246 } 247 } 248 249 @Override 250 protected void finishLocalSetup() { 251 // No code by default 252 } 253}