1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 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 corrosponds to file name.
56 */
57 private final Map<Path, Set<String>> filesAndChecksCollector = new HashMap<>();
58
59 /**
60 * Collects the module ids corrosponds 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 */
74 public ChecksAndFilesSuppressionFileGeneratorAuditListener(OutputStream out,
75 OutputStreamOptions outputStreamOptions) {
76 writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
77 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
78 }
79
80 @Override
81 public void fileStarted(AuditEvent event) {
82 // No code by default
83 }
84
85 @Override
86 public void fileFinished(AuditEvent event) {
87 // No code by default
88 }
89
90 @Override
91 public void auditStarted(AuditEvent event) {
92 // No code by default
93 }
94
95 @Override
96 public void auditFinished(AuditEvent event) {
97 if (isXmlHeaderPrinted) {
98 writer.println("</suppressions>");
99 }
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 SuppressionFilter Configuration 1.2//EN\"");
243 writer.println(" \"https://checkstyle.org/dtds/suppressions_1_2.dtd\">");
244 writer.println("<suppressions>");
245 isXmlHeaderPrinted = true;
246 }
247 }
248
249 @Override
250 protected void finishLocalSetup() {
251 // No code by default
252 }
253 }