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