001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.filters;
021
022import java.util.Objects;
023import java.util.regex.Pattern;
024
025import javax.annotation.Nullable;
026
027import com.puppycrawl.tools.checkstyle.api.AuditEvent;
028import com.puppycrawl.tools.checkstyle.api.Filter;
029
030/**
031 * This filter element is immutable and processes {@link AuditEvent}
032 * objects based on the criteria of file, check, module id, line, and
033 * column. It rejects an AuditEvent if the following match:
034 * <ul>
035 *   <li>the event's file name; and</li>
036 *   <li>the check name or the module identifier; and</li>
037 *   <li>(optionally) the event's line is in the filter's line CSV; and</li>
038 *   <li>(optionally) the check's columns is in the filter's column CSV.</li>
039 * </ul>
040 * If none of the criteria are configured, the element accepts all events.
041 *
042 */
043public class SuppressFilterElement
044    implements Filter {
045
046    /** The regexp to match file names against. */
047    private final Pattern fileRegexp;
048
049    /** The regexp to match check names against. */
050    private final Pattern checkRegexp;
051
052    /** The regexp to match message names against. */
053    private final Pattern messageRegexp;
054
055    /** Module id filter. */
056    private final String moduleId;
057
058    /** Line number filter. */
059    private final CsvFilterElement lineFilter;
060
061    /** CSV for line number filter. */
062    private final String linesCsv;
063
064    /** Column number filter. */
065    private final CsvFilterElement columnFilter;
066
067    /** CSV for column number filter. */
068    private final String columnsCsv;
069
070    /**
071     * Creates a {@code SuppressFilterElement} instance.
072     *
073     * @param files regular expression for filtered file names
074     * @param checks regular expression for filtered check classes
075     * @param message regular expression for messages.
076     * @param moduleId the module id
077     * @param lines CSV for lines
078     * @param columns CSV for columns
079     */
080    public SuppressFilterElement(Pattern files, Pattern checks, Pattern message, String moduleId,
081            String lines, String columns) {
082        fileRegexp = files;
083        checkRegexp = checks;
084        messageRegexp = message;
085        this.moduleId = moduleId;
086        if (lines == null) {
087            linesCsv = null;
088            lineFilter = null;
089        }
090        else {
091            linesCsv = lines;
092            lineFilter = new CsvFilterElement(lines);
093        }
094        if (columns == null) {
095            columnsCsv = null;
096            columnFilter = null;
097        }
098        else {
099            columnsCsv = columns;
100            columnFilter = new CsvFilterElement(columns);
101        }
102    }
103
104    /**
105     * Constructs a {@code SuppressFilterElement} using regular expressions
106     * as {@code String}s. These are internally compiled into {@code Pattern}
107     * objects and passed to the main constructor.
108     *
109     * @param files   regular expression for names of filtered files.
110     * @param checks  regular expression for filtered check classes.
111     * @param message regular expression for messages.
112     * @param modId   the id
113     * @param lines   lines CSV values and ranges for line number filtering.
114     * @param columns columns CSV values and ranges for column number filtering.
115     */
116    public SuppressFilterElement(String files, String checks,
117                                 String message, String modId, String lines, String columns) {
118        this(toPattern(files), toPattern(checks), toPattern(message),
119                modId, lines, columns);
120    }
121
122    /**
123     * Converts a string into a compiled {@code Pattern}, or return {@code null}
124     * if input is {@code null}.
125     *
126     * @param regex the regular expression as a string, may be {@code null}.
127     * @return the compiled {@code Pattern}, or {@code null} if input is {@code null}.
128     */
129    private static Pattern toPattern(String regex) {
130        final Pattern result;
131        if (regex != null) {
132            result = Pattern.compile(regex);
133        }
134        else {
135            result = null;
136        }
137        return result;
138    }
139
140    @Override
141    public boolean accept(AuditEvent event) {
142        return isNotConfigured()
143                || !isFileNameAndModuleNameMatching(event)
144                || !isMessageNameMatching(event)
145                || !isLineAndColumnMatching(event);
146    }
147
148    /**
149     * Checks whether none of the suppression criteria have been configured. With every
150     * criterion unset, each individual matching check trivially reports a match, which would
151     * otherwise make {@link #accept(AuditEvent)} reject every event. An element with nothing
152     * to match against must instead accept all events.
153     *
154     * @return true if no suppression criteria are set
155     */
156    private boolean isNotConfigured() {
157        return fileRegexp == null && checkRegexp == null && messageRegexp == null
158                && moduleId == null && lineFilter == null && columnFilter == null;
159    }
160
161    /**
162     * Is matching by file name, module id, and Check name.
163     *
164     * @param event event
165     * @return true if it is matching
166     */
167    private boolean isFileNameAndModuleNameMatching(AuditEvent event) {
168        return event.getFileName() != null
169                && (fileRegexp == null || isFileMatch(event.getFileName()))
170                && event.getViolation() != null
171                && (moduleId == null || moduleId.equals(event.getModuleId()))
172                && (checkRegexp == null || checkRegexp.matcher(event.getSourceName()).find());
173    }
174
175    /**
176     * Checks if the given file name matches the file regexp.
177     * If there is no match and the OS uses backslashes (Windows),
178     * it converts backslashes to forward slashes and tries again.
179     *
180     * @param fileName the name of the file to check
181     * @return true if the file matches the regexp
182     */
183    private boolean isFileMatch(String fileName) {
184        return fileRegexp.matcher(fileName).find()
185                || matchAfterSlashNormalization(fileName);
186    }
187
188    /**
189     * Performance optimization: returns the file regex match against the
190     * backslash-normalized file name, but only when normalization changed the
191     * string. {@link String#replace(char, char)} returns the same instance when
192     * there is nothing to replace, so guarding against that case avoids running
193     * the same regex twice on identical input.
194     *
195     * @param fileName the original file name
196     * @return true if the normalized file name matches the file regex
197     */
198    private boolean matchAfterSlashNormalization(String fileName) {
199        final String slashesFileName = fileName.replace('\\', '/');
200        return !slashesFileName.equals(fileName)
201                && fileRegexp.matcher(slashesFileName).find();
202    }
203
204    /**
205     * Is matching by message.
206     *
207     * @param event event
208     * @return true if it is matching or not set.
209     */
210    private boolean isMessageNameMatching(AuditEvent event) {
211        return messageRegexp == null || messageRegexp.matcher(event.getMessage()).find();
212    }
213
214    /**
215     * Whether line and column match.
216     *
217     * @param event event to process.
218     * @return true if line and column are matching or not set.
219     */
220    private boolean isLineAndColumnMatching(AuditEvent event) {
221        return lineFilter == null && columnFilter == null
222                || lineFilter != null && lineFilter.accept(event.getLine())
223                || columnFilter != null && columnFilter.accept(event.getColumn());
224    }
225
226    @Override
227    public int hashCode() {
228        return Objects.hash(getPatternSafely(fileRegexp), getPatternSafely(checkRegexp),
229                getPatternSafely(messageRegexp), moduleId, linesCsv, columnsCsv);
230    }
231
232    @Override
233    public boolean equals(Object other) {
234        if (this == other) {
235            return true;
236        }
237        if (other == null || getClass() != other.getClass()) {
238            return false;
239        }
240        final SuppressFilterElement suppressElement = (SuppressFilterElement) other;
241        return Objects.equals(getPatternSafely(fileRegexp),
242                    getPatternSafely(suppressElement.fileRegexp))
243                && Objects.equals(getPatternSafely(checkRegexp),
244                    getPatternSafely(suppressElement.checkRegexp))
245                && Objects.equals(getPatternSafely(messageRegexp),
246                    getPatternSafely(suppressElement.messageRegexp))
247                && Objects.equals(moduleId, suppressElement.moduleId)
248                && Objects.equals(linesCsv, suppressElement.linesCsv)
249                && Objects.equals(columnsCsv, suppressElement.columnsCsv);
250    }
251
252    /**
253     * Util method to get pattern String value from Pattern object safely, return null if
254     * pattern object is null.
255     *
256     * @param pattern pattern object
257     * @return value of pattern or null
258     */
259    @Nullable
260    private static String getPatternSafely(Pattern pattern) {
261        String result = null;
262        if (pattern != null) {
263            result = pattern.pattern();
264        }
265        return result;
266    }
267
268}