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}