View Javadoc
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 }