1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle;
21
22 import java.io.OutputStream;
23 import java.io.OutputStreamWriter;
24 import java.io.PrintWriter;
25 import java.nio.charset.StandardCharsets;
26 import java.nio.file.Path;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.Map;
30 import java.util.Set;
31
32 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
33 import com.puppycrawl.tools.checkstyle.api.AuditListener;
34
35 /**
36 * Generates <b>suppressions.xml</b> file, based on violations occurred.
37 * See issue <a href="https://github.com/checkstyle/checkstyle/issues/5983">#5983</a>
38 */
39 public final class ChecksAndFilesSuppressionFileGeneratorAuditListener
40 extends AbstractAutomaticBean
41 implements AuditListener {
42
43 /** The " quote character. */
44 private static final String QUOTE_CHAR = "\"";
45
46 /**
47 * Helper writer that allows easy encoding and printing.
48 */
49 private final PrintWriter writer;
50
51 /** Close output stream in auditFinished. */
52 private final boolean closeStream;
53
54 /**
55 * Collects the check names corresponds to file name.
56 */
57 private final Map<Path, Set<String>> filesAndChecksCollector = new HashMap<>();
58
59 /**
60 * Collects the module ids corresponds to file name.
61 */
62 private final Map<Path, Set<String>> filesAndModuleIdCollector = new HashMap<>();
63
64 /** Determines if xml header is printed. */
65 private boolean isXmlHeaderPrinted;
66
67 /**
68 * Creates a new {@code ChecksAndFilesSuppressionFileGeneratorAuditListener} instance.
69 * Sets the output to a defined stream.
70 *
71 * @param out the output stream
72 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
73 * @throws IllegalArgumentException if outputStreamOptions is null.
74 */
75 public ChecksAndFilesSuppressionFileGeneratorAuditListener(OutputStream out,
76 OutputStreamOptions outputStreamOptions) {
77 if (outputStreamOptions == null) {
78 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
79 }
80
81 writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
82 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
83 }
84
85 @Override
86 public void fileStarted(AuditEvent event) {
87 // No code by default
88 }
89
90 @Override
91 public void fileFinished(AuditEvent event) {
92 // No code by default
93 }
94
95 @Override
96 public void auditStarted(AuditEvent event) {
97 // No code by default
98 }
99
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 }