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 final 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     * @throws IllegalArgumentException if outputStreamOptions is null.
074     */
075    public ChecksAndFilesSuppressionFileGeneratorAuditListener(OutputStream out,
076                                           OutputStreamOptions outputStreamOptions) {
077        if (outputStreamOptions == null) {
078            throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
079        }
080
081        writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
082        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
083    }
084
085    @Override
086    public void fileStarted(AuditEvent event) {
087        // No code by default
088    }
089
090    @Override
091    public void fileFinished(AuditEvent event) {
092        // No code by default
093    }
094
095    @Override
096    public void auditStarted(AuditEvent event) {
097        // No code by default
098    }
099
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}