• 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

0.0
/pmd-core/src/main/java/net/sourceforge/pmd/reporting/AbstractAnnotationSuppressor.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.ArrayList;
8
import java.util.Collections;
9
import java.util.HashSet;
10
import java.util.List;
11
import java.util.Set;
12
import java.util.stream.Collectors;
13

14
import org.apache.commons.lang3.mutable.MutableBoolean;
15
import org.checkerframework.checker.nullness.qual.NonNull;
16
import org.checkerframework.checker.nullness.qual.Nullable;
17

18
import net.sourceforge.pmd.lang.ast.Node;
19
import net.sourceforge.pmd.lang.ast.NodeStream;
20
import net.sourceforge.pmd.lang.ast.RootNode;
21
import net.sourceforge.pmd.lang.rule.Rule;
22
import net.sourceforge.pmd.lang.rule.impl.UnnecessaryPmdSuppressionRule;
23
import net.sourceforge.pmd.reporting.Report.SuppressedViolation;
24
import net.sourceforge.pmd.util.AssertionUtil;
25
import net.sourceforge.pmd.util.DataMap;
26
import net.sourceforge.pmd.util.DataMap.SimpleDataKey;
27
import net.sourceforge.pmd.util.OptionalBool;
28

29
/**
30
 * Base class for a {@link ViolationSuppressor} that uses annotations
31
 * of the source language to suppress some warnings.
32
 *
33
 * @param <A> Class of the node type that models annotations in the AST
34
 *           of the language
35
 * @since 7.14.0
36
 */
37
public abstract class AbstractAnnotationSuppressor<A extends Node> implements ViolationSuppressor {
38

39
    private final Class<A> annotationNodeType;
40

NEW
41
    protected AbstractAnnotationSuppressor(Class<A> annotationClass) {
×
NEW
42
        this.annotationNodeType = annotationClass;
×
NEW
43
    }
×
44

45
    @Override
46
    public String getId() {
NEW
47
        return "@SuppressWarnings";
×
48
    }
49

50
    @Override
51
    public Report.SuppressedViolation suppressOrNull(RuleViolation rv, @NonNull Node node) {
NEW
52
        if (contextSuppresses(node, rv.getRule())) {
×
NEW
53
            return new SuppressedViolation(rv, this, null);
×
54
        }
NEW
55
        return null;
×
56
    }
57

58
    @Override
59
    public Set<UnusedSuppressorNode> getUnusedSuppressors(RootNode tree) {
NEW
60
        return tree.descendants(annotationNodeType).crossFindBoundaries().toStream().map(this::getUnusedSuppressorNodes).flatMap(Set::stream).collect(Collectors.toSet());
×
61
    }
62

63

64
    private boolean contextSuppresses(Node node, Rule rule) {
NEW
65
        if (suppresses(node, rule)) {
×
NEW
66
            return true;
×
67
        }
68

NEW
69
        if (node instanceof RootNode) {
×
70
            // This logic is here to suppress violations on the root node
71
            // based on an annotation of its child. In Java for instance
72
            // you cannot annotate the root node, because you can only annotate
73
            // declarations. But an annotation on a toplevel class, or on
74
            // the package declaration, would suppress violations on the root
75
            // node as well.
NEW
76
            for (int i = 0; i < node.getNumChildren(); i++) {
×
NEW
77
                if (suppresses(node.getChild(i), rule)) {
×
NEW
78
                    return true;
×
79
                }
80
            }
81
        }
82

NEW
83
        Node parent = node.getParent();
×
NEW
84
        while (parent != null) {
×
NEW
85
            if (suppresses(parent, rule)) {
×
NEW
86
                return true;
×
87
            }
NEW
88
            parent = parent.getParent();
×
89
        }
NEW
90
        return false;
×
91
    }
92

93

94
    /**
95
     * Returns true if the node has an annotation that suppresses the
96
     * given rule.
97
     */
98
    private boolean suppresses(final Node node, Rule rule) {
NEW
99
        return getAnnotations(node).any(it -> annotationSuppresses(it, rule));
×
100
    }
101

102
    private boolean annotationSuppresses(A annotation, Rule rule) {
NEW
103
        List<AnnotationPartWrapper> applicableParts = new ArrayList<>();
×
NEW
104
        walkAnnotation(annotation, (parm, stringValue) -> {
×
NEW
105
            if (annotationParamSuppresses(stringValue, rule)) {
×
NEW
106
                applicableParts.add(new AnnotationPartWrapper(parm, stringValue));
×
107
            }
NEW
108
            return false;
×
109
        });
110

NEW
111
        AnnotationPartWrapper mostSpecific = getMostSpecific(applicableParts);
×
NEW
112
        if (mostSpecific != null) {
×
NEW
113
            mostSpecific.node.getUserMap().compute(KEY_SUPPRESSED_ANY_VIOLATION, a -> Boolean.TRUE);
×
NEW
114
            return true;
×
115
        }
NEW
116
        return false;
×
117
    }
118

119
    /**
120
     * If several parts match (eg "PMD.RuleName" and "PMD") then we take the most specific and mark it as used.
121
     */
122
    private static @Nullable AnnotationPartWrapper getMostSpecific(List<AnnotationPartWrapper> parts) {
NEW
123
        if (parts.isEmpty()) {
×
NEW
124
            return null;
×
NEW
125
        } else if (parts.size() == 1) {
×
NEW
126
            return parts.get(0);
×
127
        }
NEW
128
        parts.sort(AbstractAnnotationSuppressor::compareSpecificity);
×
NEW
129
        if (parts.stream().allMatch(p -> isPmdSuppressor(p.stringValue))) {
×
130
            // If they are all pmd suppressors then we can take the most specific and assume that
131
            // the more generic
NEW
132
            return parts.get(parts.size() - 1);
×
133
        } else {
134
            // Otherwise the non-pmd suppressors are found at the start of the list as they are
135
            // classified as less-specific than any PMD suppressor.
NEW
136
            return parts.get(0);
×
137
        }
138
    }
139

140
    /**
141
     * Walk the individual suppression specifications of an annotation (usually strings within the annotation).
142
     * For each of those, call the callback. If the callback returns true, interrupt the walk and return true.
143
     * Otherwise, continue the walk.
144
     *
145
     * @param annotation An annotation
146
     * @param callbacks Callback object
147
     *
148
     * @return True if the callback returned true once
149
     */
150
    protected abstract boolean walkAnnotation(A annotation, AnnotationWalkCallbacks callbacks);
151

152

153
    /** Return the annotations attached to the given node. */
154
    protected abstract NodeStream<A> getAnnotations(Node n);
155

156
    /** Return a nice toString for the given annotation. */
157
    protected String getAnnotationName(A annotation) {
NEW
158
        return "@SuppressWarnings annotation";
×
159
    }
160

161

162
    /**
163
     * Return whether one of the annotation params suppresses the given rule.
164
     * The default implementation uses sensible values, so call super.
165
     */
166
    protected boolean annotationParamSuppresses(String stringVal, Rule rule) {
NEW
167
        return "PMD".equals(stringVal) || ("PMD." + rule.getName()).equals(stringVal) || "all".equals(stringVal);
×
168
    }
169

170
    /**
171
     * Return whether the annotation param may be suppressing warnings from other tools.
172
     * If this returns NO, then the parameter may be marked as unused and reported by the
173
     * rule {@link UnnecessaryPmdSuppressionRule}.
174
     */
175
    protected OptionalBool isSuppressingNonPmdWarnings(String stringVal, A annotation) {
NEW
176
        if (isPmdSuppressor(stringVal)) {
×
NEW
177
            return OptionalBool.NO;
×
178
        }
NEW
179
        return OptionalBool.UNKNOWN;
×
180
    }
181

182
    /** Callbacks for a walk over an annotation. */
183
    protected interface AnnotationWalkCallbacks {
184

185
        /**
186
         * Process one parameter of the annotation being walked.
187
         *
188
         * @param annotationParam The node corresponding to the parameter
189
         * @param stringValue The string extracted from the node
190
         */
191
        boolean processNode(Node annotationParam, @NonNull String stringValue);
192

193
    }
194

NEW
195
    private static final SimpleDataKey<Boolean> KEY_SUPPRESSED_ANY_VIOLATION = DataMap.simpleDataKey("pmd.core.suppressed.any");
×
196

197
    /**
198
     * Return the set of rule names for which the given annotation has suppressed at least one violation.
199
     *
200
     * @param annotation An annotation
201
     */
202
    private Set<UnusedSuppressorNode> getUnusedSuppressorNodes(A annotation) {
NEW
203
        Set<UnusedSuppressorNode> unusedParts = new HashSet<>();
×
NEW
204
        MutableBoolean entireAnnotationIsUnused = new MutableBoolean(true);
×
NEW
205
        MutableBoolean anySuppressor = new MutableBoolean(false);
×
NEW
206
        walkAnnotation(annotation, (annotationParam, stringValue) -> {
×
NEW
207
            anySuppressor.setTrue();
×
208

NEW
209
            boolean suppressedAny = annotationParam.getUserMap().getOrDefault(KEY_SUPPRESSED_ANY_VIOLATION, Boolean.FALSE);
×
NEW
210
            if (suppressedAny) {
×
NEW
211
                entireAnnotationIsUnused.setFalse();
×
212
            } else {
NEW
213
                if (isSuppressingNonPmdWarnings(stringValue, annotation) == OptionalBool.NO) {
×
NEW
214
                    unusedParts.add(makeAnnotationPartSuppressor(annotation, annotationParam, stringValue));
×
215
                } else {
NEW
216
                    entireAnnotationIsUnused.setFalse();
×
217
                }
218
            }
NEW
219
            return false;
×
220
        });
221

NEW
222
        if (anySuppressor.isTrue() && entireAnnotationIsUnused.isTrue()) {
×
NEW
223
            return Collections.singleton(makeFullAnnotationSuppressor(annotation));
×
224
        } else {
NEW
225
            return unusedParts;
×
226
        }
227
    }
228

229
    private static boolean isPmdSuppressor(String stringValue) {
NEW
230
        return "PMD".equals(stringValue) || stringValue.startsWith("PMD.");
×
231
    }
232

233
    private SuppressorNodeImpl makeAnnotationPartSuppressor(A annotation, Node annotationPart, String stringValue) {
NEW
234
        String message = "Unnecessary suppression \"" + stringValue + "\" in " + getAnnotationName(annotation);
×
NEW
235
        return new SuppressorNodeImpl(annotationPart, message);
×
236
    }
237

238
    private SuppressorNodeImpl makeFullAnnotationSuppressor(A annotation) {
NEW
239
        String message = "Unnecessary " + getAnnotationName(annotation);
×
NEW
240
        return new SuppressorNodeImpl(annotation, message);
×
241
    }
242

243
    private static final class SuppressorNodeImpl implements UnusedSuppressorNode {
244
        private final Node location;
245
        private final String message;
246

NEW
247
        SuppressorNodeImpl(Node node, String message) {
×
NEW
248
            this.location = node;
×
NEW
249
            this.message = message;
×
NEW
250
        }
×
251

252
        @Override
253
        public Reportable getLocation() {
NEW
254
            return location;
×
255
        }
256

257
        @Override
258
        public String unusedReason() {
NEW
259
            return message;
×
260
        }
261
    }
262

263

264
    private static int compareSpecificity(AnnotationPartWrapper fstPart, AnnotationPartWrapper sndPart) {
NEW
265
        String fst = fstPart.stringValue;
×
NEW
266
        String snd = sndPart.stringValue;
×
267

NEW
268
        if (fst.equals(snd)) {
×
NEW
269
            return 0;
×
270
        }
NEW
271
        if ("all".equals(snd)) {
×
NEW
272
            return 1;
×
NEW
273
        } else if ("all".equals(fst)) {
×
NEW
274
            return -1;
×
275
        }
276

277
        // this is the case for "fallthrough" and such, they may suppress warnings that PMD did not cause
NEW
278
        if (!isPmdSuppressor(snd)) {
×
NEW
279
            return 1;
×
NEW
280
        } else if (!isPmdSuppressor(fst)) {
×
NEW
281
            return -1;
×
282
        }
283

NEW
284
        if ("PMD".equals(snd)) {
×
NEW
285
            return 1;
×
NEW
286
        } else if ("PMD".equals(fst)) {
×
NEW
287
            return -1;
×
288
        } else {
NEW
289
            throw AssertionUtil.shouldNotReachHere("Logically if we are here then both strings are of the form PMD.RuleName and should therefore be equal!");
×
290
        }
291
    }
292

293

294
    private static final class AnnotationPartWrapper {
295
        private final Node node;
296
        private final String stringValue;
297

NEW
298
        private AnnotationPartWrapper(Node node, String stringValue) {
×
NEW
299
            this.node = node;
×
NEW
300
            this.stringValue = stringValue;
×
NEW
301
        }
×
302
    }
303
}
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