• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 23

30 May 2025 04:25PM UTC coverage: 78.377% (-0.2%) from 78.601%
23

push

github

adangel
[core] Add rule to report unnecessary suppression comments/annotations (#5609)

Merge pull request #5609 from oowekyala:new-rule-UnnecessarySuppression

17712 of 23434 branches covered (75.58%)

Branch coverage included in aggregate %.

159 of 328 new or added lines in 22 files covered. (48.48%)

36 existing lines in 4 files now uncovered.

38902 of 48799 relevant lines covered (79.72%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.56
/pmd-core/src/main/java/net/sourceforge/pmd/reporting/ViolationSuppressor.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.reporting;
6

7
import java.util.AbstractSet;
8
import java.util.Collection;
9
import java.util.Collections;
10
import java.util.HashSet;
11
import java.util.Iterator;
12
import java.util.List;
13
import java.util.Optional;
14
import java.util.Set;
15
import java.util.regex.Pattern;
16

17
import org.checkerframework.checker.nullness.qual.NonNull;
18
import org.checkerframework.checker.nullness.qual.Nullable;
19

20
import net.sourceforge.pmd.annotation.Experimental;
21
import net.sourceforge.pmd.lang.ast.AstInfo;
22
import net.sourceforge.pmd.lang.ast.Node;
23
import net.sourceforge.pmd.lang.ast.RootNode;
24
import net.sourceforge.pmd.lang.rule.Rule;
25
import net.sourceforge.pmd.lang.rule.xpath.XPathVersion;
26
import net.sourceforge.pmd.lang.rule.xpath.internal.DeprecatedAttrLogger;
27
import net.sourceforge.pmd.lang.rule.xpath.internal.SaxonXPathRuleQuery;
28
import net.sourceforge.pmd.reporting.Report.SuppressedViolation;
29
import net.sourceforge.pmd.util.DataMap;
30
import net.sourceforge.pmd.util.DataMap.SimpleDataKey;
31
import net.sourceforge.pmd.util.IteratorUtil;
32

33
/**
34
 * An object that suppresses rule violations. Suppressors are used by
35
 * {@link RuleContext} to filter out violations. In PMD 6.0.x,
36
 * the {@link Report} object filtered violations itself - but it has
37
 * no knowledge of language-specific suppressors.
38
 */
39
public interface ViolationSuppressor {
40
    /**
41
     * Suppressor for the violationSuppressRegex property.
42
     */
43
    ViolationSuppressor REGEX_SUPPRESSOR = new ViolationSuppressor() {
1✔
44
        @Override
45
        public String getId() {
46
            return "Regex";
×
47
        }
48

49
        @Override
50
        public @Nullable SuppressedViolation suppressOrNull(RuleViolation rv, @NonNull Node node) {
51
            Optional<Pattern> regex = rv.getRule().getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR); // Regex
1✔
52
            if (regex.isPresent() && rv.getDescription() != null) {
1!
53
                if (regex.get().matcher(rv.getDescription()).matches()) {
×
54
                    return new SuppressedViolation(rv, this, regex.get().pattern());
×
55
                }
56
            }
57
            return null;
1✔
58
        }
59
    };
60

61
    /**
62
     * Suppressor for the violationSuppressXPath property.
63
     */
64
    ViolationSuppressor XPATH_SUPPRESSOR = new ViolationSuppressor() {
1✔
65
        @Override
66
        public String getId() {
67
            return "XPath";
×
68
        }
69

70
        @Override
71
        public @Nullable SuppressedViolation suppressOrNull(RuleViolation rv, @NonNull Node node) {
72
            // todo this should not be implemented via a rule property
73
            //  because the parsed xpath expression should be stored, not a random string
74
            //  this needs to be checked to be a valid xpath expression in the ruleset,
75
            //  not at the time it is evaluated, and also parsed by the XPath parser only once
76
            Rule rule = rv.getRule();
1✔
77
            Optional<String> xpath = rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR);
1✔
78
            if (!xpath.isPresent()) {
1!
79
                return null;
1✔
80
            }
81
            SaxonXPathRuleQuery rq = new SaxonXPathRuleQuery(
×
82
                xpath.get(),
×
83
                XPathVersion.DEFAULT,
84
                rule.getPropertiesByPropertyDescriptor(),
×
85
                node.getAstInfo().getLanguageProcessor().services().getXPathHandler(),
×
86
                DeprecatedAttrLogger.createForSuppression(rv.getRule())
×
87
            );
88
            if (!rq.evaluate(node).isEmpty()) {
×
89
                return new SuppressedViolation(rv, this, xpath.get());
×
90
            }
91
            return null;
×
92
        }
93
    };
94

95
    /**
96
     * Suppressor for regular NOPMD comments.
97
     *
98
     * @implNote This requires special support from the language, namely,
99
     *     the parser must fill in suppression comments through
100
     *     {@link AstInfo#withSuppressionComments(Collection)}.
101
     */
102
    ViolationSuppressor NOPMD_COMMENT_SUPPRESSOR = new ViolationSuppressor() {
1✔
103
        private final SimpleDataKey<Set<SuppressionCommentWrapper>> usedSuppressionComments =
1✔
104
            DataMap.simpleDataKey("pmd.core.comment.suppressor");
1✔
105

106
        @Override
107
        public String getId() {
108
            return "//NOPMD";
1✔
109
        }
110

111
        @Override
112
        public @Nullable SuppressedViolation suppressOrNull(RuleViolation rv, @NonNull Node node) {
113
            AstInfo<? extends RootNode> astInfo = node.getAstInfo();
1✔
114
            SuppressionCommentWrapper wrapper = astInfo.getSuppressionComment(rv.getBeginLine());
1✔
115
            if (wrapper != null) {
1✔
116
                astInfo.getUserMap().computeIfAbsent(usedSuppressionComments, HashSet::new).add(wrapper);
1✔
117
                return new SuppressedViolation(rv, this, wrapper.getUserMessage());
1✔
118
            }
119
            return null;
1✔
120
        }
121

122
        @Override
123
        public Set<UnusedSuppressorNode> getUnusedSuppressors(RootNode tree) {
NEW
124
            Set<SuppressionCommentWrapper> usedSuppressors = tree.getAstInfo().getUserMap().getOrDefault(usedSuppressionComments, Collections.emptySet());
×
NEW
125
            Set<SuppressionCommentWrapper> allSuppressors = new HashSet<>(tree.getAstInfo().getAllSuppressionComments());
×
NEW
126
            allSuppressors.removeAll(usedSuppressors);
×
NEW
127
            return new AbstractSet<UnusedSuppressorNode>() {
×
128
                @Override
129
                public @NonNull Iterator<UnusedSuppressorNode> iterator() {
NEW
130
                    return IteratorUtil.map(
×
NEW
131
                        allSuppressors.iterator(),
×
NEW
132
                        comment -> new UnusedSuppressorNode() {
×
133
                            @Override
134
                            public Reportable getLocation() {
NEW
135
                                return comment.getLocation();
×
136
                            }
137

138
                            @Override
139
                            public String unusedReason() {
NEW
140
                                return "Unnecessary PMD suppression comment";
×
141
                            }
142
                        }
143
                    );
144
                }
145

146
                @Override
147
                public int size() {
NEW
148
                    return allSuppressors.size();
×
149
                }
150
            };
151
        }
152
    };
153

154

155
    /**
156
     * A name, for reporting and documentation purposes.
157
     */
158
    String getId();
159

160

161
    /**
162
     * Returns a {@link SuppressedViolation} if the given violation is
163
     * suppressed by this object. The node and the rule are provided
164
     * for context. Returns null if the violation is not suppressed.
165
     */
166
    @Nullable
167
    SuppressedViolation suppressOrNull(RuleViolation rv, @NonNull Node node);
168

169

170
    /**
171
     * Return the set of suppressor nodes related to this suppressor
172
     * that were not used during the analysis.
173
     * For instance, for an annotation suppressor, the set contains
174
     * suppressor nodes wrapping annotations.
175
     * This must be implemented if this suppressor wants to play well
176
     * with the unused PMD suppression rule.
177
     *
178
     * @param tree Root node of a file
179
     *
180
     * @return A set
181
     *
182
     * @since 7.14.0
183
     */
184
    default Set<UnusedSuppressorNode> getUnusedSuppressors(RootNode tree) {
NEW
185
        return Collections.emptySet();
×
186
    }
187

188

189
    /**
190
     * Apply a list of suppressors on the violation. Returns the violation
191
     * of the first suppressor that matches the input violation. If no
192
     * suppressor matches, then returns null.
193
     */
194
    static @Nullable SuppressedViolation suppressOrNull(List<ViolationSuppressor> suppressorList,
195
                                                        RuleViolation rv,
196
                                                        Node node) {
197
        for (ViolationSuppressor suppressor : suppressorList) {
1✔
198
            SuppressedViolation suppressed = suppressor.suppressOrNull(rv, node);
1✔
199
            if (suppressed != null) {
1✔
200
                return suppressed;
1✔
201
            }
202
        }
1✔
203
        return null;
1✔
204
    }
205

206

207
    /**
208
     * Represents an instance of a "suppressor" that didn't suppress anything.
209
     * This could be a suppression annotation, or part of an annotation, a
210
     * comment, etc.
211
     *
212
     * @since 7.14.0
213
     */
214
    interface UnusedSuppressorNode {
215

216
        Reportable getLocation();
217

218
        String unusedReason();
219
    }
220

221
    /**
222
     * Wrapper around a suppression comment.
223
     *
224
     * @experimental Since 7.14.0. See <a href="https://github.com/pmd/pmd/pull/5609">[core] Add rule to report unnecessary suppression comments/annotations #5609</a>
225
     */
226
    @Experimental
227
    interface SuppressionCommentWrapper {
228
        /** Message attached to the comment. */
229
        String getUserMessage();
230

231
        /** Location of the comment, maybe the location of the comment token for instance. */
232
        Reportable getLocation();
233

234
    }
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc