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