001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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.lang.ref.WeakReference;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.Objects;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import java.util.regex.PatternSyntaxException;
031
032import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
033import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
034import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
035import com.puppycrawl.tools.checkstyle.api.FileContents;
036import com.puppycrawl.tools.checkstyle.api.TextBlock;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
038
039/**
040 * <p>
041 * Filter {@code SuppressionCommentFilter} uses pairs of comments to suppress audit events.
042 * </p>
043 * <p>
044 * Rationale:
045 * Sometimes there are legitimate reasons for violating a check. When
046 * this is a matter of the code in question and not personal
047 * preference, the best place to override the policy is in the code
048 * itself. Semi-structured comments can be associated with the check.
049 * This is sometimes superior to a separate suppressions file, which
050 * must be kept up-to-date as the source file is edited.
051 * </p>
052 * <p>
053 * Note that the suppression comment should be put before the violation.
054 * You can use more than one suppression comment each on separate line.
055 * </p>
056 * <p>
057 * Attention: This filter may only be specified within the TreeWalker module
058 * ({@code &lt;module name="TreeWalker"/&gt;}) and only applies to checks which are also
059 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, a
060 * <a href="https://checkstyle.org/config_filters.html#SuppressWithPlainTextCommentFilter">
061 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
062 * </p>
063 * <p>
064 * {@code offCommentFormat} and {@code onCommentFormat} must have equal
065 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
066 * paren counts</a>.
067 * </p>
068 * <p>
069 * SuppressionCommentFilter can suppress Checks that have Treewalker as parent module.
070 * </p>
071 * <ul>
072 * <li>
073 * Property {@code offCommentFormat} - Specify comment pattern to
074 * trigger filter to begin suppression.
075 * Type is {@code java.util.regex.Pattern}.
076 * Default value is {@code "CHECKSTYLE:OFF"}.
077 * </li>
078 * <li>
079 * Property {@code onCommentFormat} - Specify comment pattern to trigger filter to end suppression.
080 * Type is {@code java.util.regex.Pattern}.
081 * Default value is {@code "CHECKSTYLE:ON"}.
082 * </li>
083 * <li>
084 * Property {@code checkFormat} - Specify check pattern to suppress.
085 * Type is {@code java.util.regex.Pattern}.
086 * Default value is {@code ".*"}.
087 * </li>
088 * <li>
089 * Property {@code messageFormat} - Specify message pattern to suppress.
090 * Type is {@code java.util.regex.Pattern}.
091 * Default value is {@code null}.
092 * </li>
093 * <li>
094 * Property {@code idFormat} - Specify check ID pattern to suppress.
095 * Type is {@code java.util.regex.Pattern}.
096 * Default value is {@code null}.
097 * </li>
098 * <li>
099 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}).
100 * Type is {@code boolean}.
101 * Default value is {@code true}.
102 * </li>
103 * <li>
104 * Property {@code checkC} - Control whether to check C style comments ({@code &#47;* ... *&#47;}).
105 * Type is {@code boolean}.
106 * Default value is {@code true}.
107 * </li>
108 * </ul>
109 * <p>
110 * To configure a filter to suppress audit events between a comment containing
111 * {@code CHECKSTYLE:OFF} and a comment containing {@code CHECKSTYLE:ON}:
112 * </p>
113 * <pre>
114 * &lt;module name="TreeWalker"&gt;
115 *               ...
116 *   &lt;module name="SuppressionCommentFilter"/&gt;
117 *               ...
118 * &lt;/module&gt;
119 * </pre>
120 * <p>
121 * To configure a filter to suppress audit events between a comment containing line
122 * {@code BEGIN GENERATED CODE} and a comment containing line {@code END GENERATED CODE}:
123 * </p>
124 * <pre>
125 * &lt;module name="SuppressionCommentFilter"&gt;
126 *   &lt;property name="offCommentFormat" value="BEGIN GENERATED CODE"/&gt;
127 *   &lt;property name="onCommentFormat" value="END GENERATED CODE"/&gt;
128 * &lt;/module&gt;
129 * </pre>
130 * <pre>
131 * //BEGIN GENERATED CODE
132 * &#64;Override
133 * public boolean equals(Object obj) { ... } // No violation events will be reported
134 *
135 * &#64;Override
136 * public int hashCode() { ... } // No violation events will be reported
137 * //END GENERATED CODE
138 * . . .
139 * </pre>
140 * <p>
141 * To configure a filter so that {@code // stop constant check} and
142 * {@code // resume constant check} marks legitimate constant names:
143 * </p>
144 * <pre>
145 * &lt;module name="SuppressionCommentFilter"&gt;
146 *   &lt;property name="offCommentFormat" value="stop constant check"/&gt;
147 *   &lt;property name="onCommentFormat" value="resume constant check"/&gt;
148 *   &lt;property name="checkFormat" value="ConstantNameCheck"/&gt;
149 * &lt;/module&gt;
150 * </pre>
151 * <pre>
152 * //stop constant check
153 * public static final int someConstant; // won't warn here
154 * //resume constant check
155 * public static final int someConstant; // will warn here as constant's name doesn't match the
156 * // pattern "^[A-Z][A-Z0-9]*$"
157 * </pre>
158 * <p>
159 * To configure a filter so that {@code UNUSED OFF: <i>var</i>} and
160 * {@code UNUSED ON: <i>var</i>} marks a variable or parameter known not to be
161 * used by the code by matching the variable name in the message:
162 * </p>
163 * <pre>
164 * &lt;module name="SuppressionCommentFilter"&gt;
165 *   &lt;property name="offCommentFormat" value="UNUSED OFF\: (\w+)"/&gt;
166 *   &lt;property name="onCommentFormat" value="UNUSED ON\: (\w+)"/&gt;
167 *   &lt;property name="checkFormat" value="Unused"/&gt;
168 *   &lt;property name="messageFormat" value="^Unused \w+ '$1'.$"/&gt;
169 * &lt;/module&gt;
170 * </pre>
171 * <pre>
172 * private static void foo(int a, int b) // UNUSED OFF: b
173 * {
174 * System.out.println(a);
175 * }
176 *
177 * private static void foo1(int a, int b) // UNUSED ON: b
178 * {
179 * System.out.println(a);
180 * }
181 * </pre>
182 * <p>
183 * To configure a filter so that name of suppressed check mentioned in comment
184 * {@code CSOFF: <i>regexp</i>} and {@code CSON: <i>regexp</i>} mark a matching check:
185 * </p>
186 * <pre>
187 * &lt;module name="SuppressionCommentFilter"&gt;
188 *   &lt;property name="offCommentFormat" value="CSOFF\: ([\w\|]+)"/&gt;
189 *   &lt;property name="onCommentFormat" value="CSON\: ([\w\|]+)"/&gt;
190 *   &lt;property name="checkFormat" value="$1"/&gt;
191 * &lt;/module&gt;
192 * </pre>
193 * <pre>
194 * public static final int lowerCaseConstant; // CSOFF: ConstantNameCheck
195 * public static final int lowerCaseConstant1; // CSON: ConstantNameCheck
196 * </pre>
197 * <p>
198 * To configure a filter to suppress all audit events between a comment containing
199 * {@code CHECKSTYLE_OFF: ALMOST_ALL} and a comment containing
200 * {@code CHECKSTYLE_OFF: ALMOST_ALL} except for the <em>EqualsHashCode</em> check:
201 * </p>
202 * <pre>
203 * &lt;module name="SuppressionCommentFilter"&gt;
204 *   &lt;property name="offCommentFormat" value="CHECKSTYLE_OFF: ALMOST_ALL"/&gt;
205 *   &lt;property name="onCommentFormat" value="CHECKSTYLE_ON: ALMOST_ALL"/&gt;
206 *   &lt;property name="checkFormat" value="^((?!(EqualsHashCode)).)*$"/&gt;
207 * &lt;/module&gt;
208 * </pre>
209 * <pre>
210 * public static final int array []; // CHECKSTYLE_OFF: ALMOST_ALL
211 * private String [] strArray;
212 * private int array1 []; // CHECKSTYLE_ON: ALMOST_ALL
213 * </pre>
214 * <p>
215 * To configure a filter to suppress Check's violation message
216 * <b>which matches specified message in messageFormat</b>
217 * (so suppression will be not only by Check's name, but by message text
218 * additionally, as the same Check could report different by message format violations)
219 * between a comment containing {@code stop} and comment containing {@code resume}:
220 * </p>
221 * <pre>
222 * &lt;module name="SuppressionCommentFilter"&gt;
223 *   &lt;property name="offCommentFormat" value="stop"/&gt;
224 *   &lt;property name="onCommentFormat" value="resume"/&gt;
225 *   &lt;property name="checkFormat" value="IllegalTypeCheck"/&gt;
226 *   &lt;property name="messageFormat"
227 *       value="^Declaring variables, return values or parameters of type 'GregorianCalendar'
228 *         is not allowed.$"/&gt;
229 * &lt;/module&gt;
230 * </pre>
231 * <p>
232 * Code before filter above is applied with Check's audit events:
233 * </p>
234 * <pre>
235 * ...
236 * // Warning below: Declaring variables, return values or parameters of type 'GregorianCalendar'
237 * // is not allowed.
238 * GregorianCalendar calendar;
239 * // Warning below here: Declaring variables, return values or parameters of type 'HashSet'
240 * // is not allowed.
241 * HashSet hashSet;
242 * ...
243 * </pre>
244 * <p>
245 * Code after filter is applied:
246 * </p>
247 * <pre>
248 * ...
249 * //stop
250 * GregorianCalendar calendar; // No warning here as it is suppressed by filter.
251 * HashSet hashSet;
252 * // Warning above here: Declaring variables, return values or parameters of type 'HashSet'
253 * //is not allowed.
254 *
255 * //resume
256 * ...
257 * </pre>
258 * <p>
259 * It is possible to specify an ID of checks, so that it can be leveraged by the
260 * SuppressionCommentFilter to skip validations. The following examples show how
261 * to skip validations near code that is surrounded with {@code // CSOFF &lt;ID&gt; (reason)}
262 * and {@code // CSON &lt;ID&gt;}, where ID is the ID of checks you want to suppress.
263 * </p>
264 * <p>
265 * Examples of Checkstyle checks configuration:
266 * </p>
267 * <pre>
268 * &lt;module name="RegexpSinglelineJava"&gt;
269 *   &lt;property name="id" value="ignore"/&gt;
270 *   &lt;property name="format" value="^.*@Ignore\s*$"/&gt;
271 *   &lt;property name="message" value="@Ignore should have a reason."/&gt;
272 * &lt;/module&gt;
273 *
274 * &lt;module name="RegexpSinglelineJava"&gt;
275 *   &lt;property name="id" value="systemout"/&gt;
276 *   &lt;property name="format" value="^.*System\.(out|err).*$"/&gt;
277 *   &lt;property name="message" value="Don't use System.out/err, use SLF4J instead."/&gt;
278 * &lt;/module&gt;
279 * </pre>
280 * <p>
281 * Example of SuppressionCommentFilter configuration (checkFormat which is set
282 * to '$1' points that ID of the checks is in the first group of offCommentFormat
283 * and onCommentFormat regular expressions):
284 * </p>
285 * <pre>
286 * &lt;module name="SuppressionCommentFilter"&gt;
287 *   &lt;property name="offCommentFormat" value="CSOFF (\w+) \(\w+\)"/&gt;
288 *   &lt;property name="onCommentFormat" value="CSON (\w+)"/&gt;
289 *   &lt;property name="idFormat" value="$1"/&gt;
290 * &lt;/module&gt;
291 * </pre>
292 * <pre>
293 * // CSOFF ignore (test has not been implemented yet)
294 * &#64;Ignore // should NOT fail RegexpSinglelineJava
295 * &#64;Test
296 * public void testMethod() { }
297 * // CSON ignore
298 *
299 * // CSOFF systemout (debug)
300 * public static void foo() {
301 *   System.out.println("Debug info."); // should NOT fail RegexpSinglelineJava
302 * }
303 * // CSON systemout
304 * </pre>
305 * <p>
306 * Example of how to configure the check to suppress more than one checks.
307 * </p>
308 * <pre>
309 * &lt;module name="SuppressionCommentFilter"&gt;
310 *   &lt;property name="offCommentFormat" value="@cs-\: ([\w\|]+)"/&gt;
311 *   &lt;property name="checkFormat" value="$1"/&gt;
312 * &lt;/module&gt;
313 * </pre>
314 * <pre>
315 * // @cs-: ClassDataAbstractionCoupling
316 * // @cs-: MagicNumber
317 * &#64;Service // no violations from ClassDataAbstractionCoupling here
318 * &#64;Transactional
319 * public class UserService {
320 *   private int value = 10022; // no violations from MagicNumber here
321 * }
322 * </pre>
323 * <p>
324 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
325 * </p>
326 *
327 * @since 3.5
328 */
329public class SuppressionCommentFilter
330    extends AutomaticBean
331    implements TreeWalkerFilter {
332
333    /**
334     * Enum to be used for switching checkstyle reporting for tags.
335     */
336    public enum TagType {
337
338        /**
339         * Switch reporting on.
340         */
341        ON,
342        /**
343         * Switch reporting off.
344         */
345        OFF,
346
347    }
348
349    /** Turns checkstyle reporting off. */
350    private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
351
352    /** Turns checkstyle reporting on. */
353    private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
354
355    /** Control all checks. */
356    private static final String DEFAULT_CHECK_FORMAT = ".*";
357
358    /** Tagged comments. */
359    private final List<Tag> tags = new ArrayList<>();
360
361    /** Control whether to check C style comments ({@code &#47;* ... *&#47;}). */
362    private boolean checkC = true;
363
364    /** Control whether to check C++ style comments ({@code //}). */
365    // -@cs[AbbreviationAsWordInName] we can not change it as,
366    // Check property is a part of API (used in configurations)
367    private boolean checkCPP = true;
368
369    /** Specify comment pattern to trigger filter to begin suppression. */
370    private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
371
372    /** Specify comment pattern to trigger filter to end suppression. */
373    private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
374
375    /** Specify check pattern to suppress. */
376    private String checkFormat = DEFAULT_CHECK_FORMAT;
377
378    /** Specify message pattern to suppress. */
379    private String messageFormat;
380
381    /** Specify check ID pattern to suppress. */
382    private String idFormat;
383
384    /**
385     * References the current FileContents for this filter.
386     * Since this is a weak reference to the FileContents, the FileContents
387     * can be reclaimed as soon as the strong references in TreeWalker
388     * are reassigned to the next FileContents, at which time filtering for
389     * the current FileContents is finished.
390     */
391    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
392
393    /**
394     * Setter to specify comment pattern to trigger filter to begin suppression.
395     *
396     * @param pattern a pattern.
397     */
398    public final void setOffCommentFormat(Pattern pattern) {
399        offCommentFormat = pattern;
400    }
401
402    /**
403     * Setter to specify comment pattern to trigger filter to end suppression.
404     *
405     * @param pattern a pattern.
406     */
407    public final void setOnCommentFormat(Pattern pattern) {
408        onCommentFormat = pattern;
409    }
410
411    /**
412     * Returns FileContents for this filter.
413     *
414     * @return the FileContents for this filter.
415     */
416    private FileContents getFileContents() {
417        return fileContentsReference.get();
418    }
419
420    /**
421     * Set the FileContents for this filter.
422     *
423     * @param fileContents the FileContents for this filter.
424     * @noinspection WeakerAccess
425     */
426    public void setFileContents(FileContents fileContents) {
427        fileContentsReference = new WeakReference<>(fileContents);
428    }
429
430    /**
431     * Setter to specify check pattern to suppress.
432     *
433     * @param format a {@code String} value
434     */
435    public final void setCheckFormat(String format) {
436        checkFormat = format;
437    }
438
439    /**
440     * Setter to specify message pattern to suppress.
441     *
442     * @param format a {@code String} value
443     */
444    public void setMessageFormat(String format) {
445        messageFormat = format;
446    }
447
448    /**
449     * Setter to specify check ID pattern to suppress.
450     *
451     * @param format a {@code String} value
452     */
453    public void setIdFormat(String format) {
454        idFormat = format;
455    }
456
457    /**
458     * Setter to control whether to check C++ style comments ({@code //}).
459     *
460     * @param checkCpp {@code true} if C++ comments are checked.
461     */
462    // -@cs[AbbreviationAsWordInName] We can not change it as,
463    // check's property is a part of API (used in configurations).
464    public void setCheckCPP(boolean checkCpp) {
465        checkCPP = checkCpp;
466    }
467
468    /**
469     * Setter to control whether to check C style comments ({@code &#47;* ... *&#47;}).
470     *
471     * @param checkC {@code true} if C comments are checked.
472     */
473    public void setCheckC(boolean checkC) {
474        this.checkC = checkC;
475    }
476
477    @Override
478    protected void finishLocalSetup() {
479        // No code by default
480    }
481
482    @Override
483    public boolean accept(TreeWalkerAuditEvent event) {
484        boolean accepted = true;
485
486        if (event.getViolation() != null) {
487            // Lazy update. If the first event for the current file, update file
488            // contents and tag suppressions
489            final FileContents currentContents = event.getFileContents();
490
491            if (getFileContents() != currentContents) {
492                setFileContents(currentContents);
493                tagSuppressions();
494            }
495            final Tag matchTag = findNearestMatch(event);
496            accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
497        }
498        return accepted;
499    }
500
501    /**
502     * Finds the nearest comment text tag that matches an audit event.
503     * The nearest tag is before the line and column of the event.
504     *
505     * @param event the {@code TreeWalkerAuditEvent} to match.
506     * @return The {@code Tag} nearest event.
507     */
508    private Tag findNearestMatch(TreeWalkerAuditEvent event) {
509        Tag result = null;
510        for (Tag tag : tags) {
511            if (tag.getLine() > event.getLine()
512                || tag.getLine() == event.getLine()
513                    && tag.getColumn() > event.getColumn()) {
514                break;
515            }
516            if (tag.isMatch(event)) {
517                result = tag;
518            }
519        }
520        return result;
521    }
522
523    /**
524     * Collects all the suppression tags for all comments into a list and
525     * sorts the list.
526     */
527    private void tagSuppressions() {
528        tags.clear();
529        final FileContents contents = getFileContents();
530        if (checkCPP) {
531            tagSuppressions(contents.getSingleLineComments().values());
532        }
533        if (checkC) {
534            final Collection<List<TextBlock>> cComments = contents
535                    .getBlockComments().values();
536            cComments.forEach(this::tagSuppressions);
537        }
538        Collections.sort(tags);
539    }
540
541    /**
542     * Appends the suppressions in a collection of comments to the full
543     * set of suppression tags.
544     *
545     * @param comments the set of comments.
546     */
547    private void tagSuppressions(Collection<TextBlock> comments) {
548        for (TextBlock comment : comments) {
549            final int startLineNo = comment.getStartLineNo();
550            final String[] text = comment.getText();
551            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
552            for (int i = 1; i < text.length; i++) {
553                tagCommentLine(text[i], startLineNo + i, 0);
554            }
555        }
556    }
557
558    /**
559     * Tags a string if it matches the format for turning
560     * checkstyle reporting on or the format for turning reporting off.
561     *
562     * @param text the string to tag.
563     * @param line the line number of text.
564     * @param column the column number of text.
565     */
566    private void tagCommentLine(String text, int line, int column) {
567        final Matcher offMatcher = offCommentFormat.matcher(text);
568        if (offMatcher.find()) {
569            addTag(offMatcher.group(0), line, column, TagType.OFF);
570        }
571        else {
572            final Matcher onMatcher = onCommentFormat.matcher(text);
573            if (onMatcher.find()) {
574                addTag(onMatcher.group(0), line, column, TagType.ON);
575            }
576        }
577    }
578
579    /**
580     * Adds a {@code Tag} to the list of all tags.
581     *
582     * @param text the text of the tag.
583     * @param line the line number of the tag.
584     * @param column the column number of the tag.
585     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
586     */
587    private void addTag(String text, int line, int column, TagType reportingOn) {
588        final Tag tag = new Tag(line, column, text, reportingOn, this);
589        tags.add(tag);
590    }
591
592    /**
593     * A Tag holds a suppression comment and its location, and determines
594     * whether the suppression turns checkstyle reporting on or off.
595     */
596    private static final class Tag
597        implements Comparable<Tag> {
598
599        /** The text of the tag. */
600        private final String text;
601
602        /** The line number of the tag. */
603        private final int line;
604
605        /** The column number of the tag. */
606        private final int column;
607
608        /** Determines whether the suppression turns checkstyle reporting on. */
609        private final TagType tagType;
610
611        /** The parsed check regexp, expanded for the text of this tag. */
612        private final Pattern tagCheckRegexp;
613
614        /** The parsed message regexp, expanded for the text of this tag. */
615        private final Pattern tagMessageRegexp;
616
617        /** The parsed check ID regexp, expanded for the text of this tag. */
618        private final Pattern tagIdRegexp;
619
620        /**
621         * Constructs a tag.
622         *
623         * @param line the line number.
624         * @param column the column number.
625         * @param text the text of the suppression.
626         * @param tagType {@code ON} if the tag turns checkstyle reporting.
627         * @param filter the {@code SuppressionCommentFilter} with the context
628         * @throws IllegalArgumentException if unable to parse expanded text.
629         */
630        /* package */ Tag(int line, int column, String text, TagType tagType,
631                   SuppressionCommentFilter filter) {
632            this.line = line;
633            this.column = column;
634            this.text = text;
635            this.tagType = tagType;
636
637            final Pattern commentFormat;
638            if (this.tagType == TagType.ON) {
639                commentFormat = filter.onCommentFormat;
640            }
641            else {
642                commentFormat = filter.offCommentFormat;
643            }
644
645            // Expand regexp for check and message
646            // Does not intern Patterns with Utils.getPattern()
647            String format = "";
648            try {
649                format = CommonUtil.fillTemplateWithStringsByRegexp(
650                        filter.checkFormat, text, commentFormat);
651                tagCheckRegexp = Pattern.compile(format);
652
653                if (filter.messageFormat == null) {
654                    tagMessageRegexp = null;
655                }
656                else {
657                    format = CommonUtil.fillTemplateWithStringsByRegexp(
658                            filter.messageFormat, text, commentFormat);
659                    tagMessageRegexp = Pattern.compile(format);
660                }
661
662                if (filter.idFormat == null) {
663                    tagIdRegexp = null;
664                }
665                else {
666                    format = CommonUtil.fillTemplateWithStringsByRegexp(
667                            filter.idFormat, text, commentFormat);
668                    tagIdRegexp = Pattern.compile(format);
669                }
670            }
671            catch (final PatternSyntaxException ex) {
672                throw new IllegalArgumentException(
673                    "unable to parse expanded comment " + format, ex);
674            }
675        }
676
677        /**
678         * Returns line number of the tag in the source file.
679         *
680         * @return the line number of the tag in the source file.
681         */
682        public int getLine() {
683            return line;
684        }
685
686        /**
687         * Determines the column number of the tag in the source file.
688         * Will be 0 for all lines of multiline comment, except the
689         * first line.
690         *
691         * @return the column number of the tag in the source file.
692         */
693        public int getColumn() {
694            return column;
695        }
696
697        /**
698         * Determines whether the suppression turns checkstyle reporting on or
699         * off.
700         *
701         * @return {@code ON} if the suppression turns reporting on.
702         */
703        public TagType getTagType() {
704            return tagType;
705        }
706
707        /**
708         * Compares the position of this tag in the file
709         * with the position of another tag.
710         *
711         * @param object the tag to compare with this one.
712         * @return a negative number if this tag is before the other tag,
713         *     0 if they are at the same position, and a positive number if this
714         *     tag is after the other tag.
715         */
716        @Override
717        public int compareTo(Tag object) {
718            final int result;
719            if (line == object.line) {
720                result = Integer.compare(column, object.column);
721            }
722            else {
723                result = Integer.compare(line, object.line);
724            }
725            return result;
726        }
727
728        /**
729         * Indicates whether some other object is "equal to" this one.
730         * Suppression on enumeration is needed so code stays consistent.
731         *
732         * @noinspection EqualsCalledOnEnumConstant
733         */
734        @Override
735        public boolean equals(Object other) {
736            if (this == other) {
737                return true;
738            }
739            if (other == null || getClass() != other.getClass()) {
740                return false;
741            }
742            final Tag tag = (Tag) other;
743            return Objects.equals(line, tag.line)
744                    && Objects.equals(column, tag.column)
745                    && Objects.equals(tagType, tag.tagType)
746                    && Objects.equals(text, tag.text)
747                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
748                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
749                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
750        }
751
752        @Override
753        public int hashCode() {
754            return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
755                    tagIdRegexp);
756        }
757
758        /**
759         * Determines whether the source of an audit event
760         * matches the text of this tag.
761         *
762         * @param event the {@code TreeWalkerAuditEvent} to check.
763         * @return true if the source of event matches the text of this tag.
764         */
765        public boolean isMatch(TreeWalkerAuditEvent event) {
766            return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
767        }
768
769        /**
770         * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
771         *
772         * @param event {@link TreeWalkerAuditEvent} instance.
773         * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
774         */
775        private boolean isCheckMatch(TreeWalkerAuditEvent event) {
776            final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
777            return checkMatcher.find();
778        }
779
780        /**
781         * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
782         *
783         * @param event {@link TreeWalkerAuditEvent} instance.
784         * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
785         */
786        private boolean isIdMatch(TreeWalkerAuditEvent event) {
787            boolean match = true;
788            if (tagIdRegexp != null) {
789                if (event.getModuleId() == null) {
790                    match = false;
791                }
792                else {
793                    final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
794                    match = idMatcher.find();
795                }
796            }
797            return match;
798        }
799
800        /**
801         * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
802         *
803         * @param event {@link TreeWalkerAuditEvent} instance.
804         * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
805         */
806        private boolean isMessageMatch(TreeWalkerAuditEvent event) {
807            boolean match = true;
808            if (tagMessageRegexp != null) {
809                final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
810                match = messageMatcher.find();
811            }
812            return match;
813        }
814
815        @Override
816        public String toString() {
817            return "Tag[text='" + text + '\''
818                    + ", line=" + line
819                    + ", column=" + column
820                    + ", type=" + tagType
821                    + ", tagCheckRegexp=" + tagCheckRegexp
822                    + ", tagMessageRegexp=" + tagMessageRegexp
823                    + ", tagIdRegexp=" + tagIdRegexp + ']';
824        }
825
826    }
827
828}