• 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

80.0
/pmd-core/src/main/java/net/sourceforge/pmd/reporting/RuleContext.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 static net.sourceforge.pmd.util.CollectionUtil.listOf;
8

9
import java.text.MessageFormat;
10
import java.util.List;
11
import java.util.Map;
12
import java.util.Objects;
13
import java.util.Optional;
14

15
import org.apache.commons.lang3.StringUtils;
16
import org.checkerframework.checker.nullness.qual.NonNull;
17
import org.checkerframework.checker.nullness.qual.Nullable;
18

19
import net.sourceforge.pmd.annotation.Experimental;
20
import net.sourceforge.pmd.lang.LanguageVersionHandler;
21
import net.sourceforge.pmd.lang.ast.AstInfo;
22
import net.sourceforge.pmd.lang.ast.Node;
23
import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken;
24
import net.sourceforge.pmd.lang.ast.internal.NodeFindingUtil;
25
import net.sourceforge.pmd.lang.document.FileLocation;
26
import net.sourceforge.pmd.lang.document.TextRange2d;
27
import net.sourceforge.pmd.lang.rule.AbstractRule;
28
import net.sourceforge.pmd.lang.rule.Rule;
29
import net.sourceforge.pmd.properties.PropertyDescriptor;
30
import net.sourceforge.pmd.reporting.Report.SuppressedViolation;
31

32
/**
33
 * The API for rules to report violations or errors during analysis.
34
 * This forwards events to a {@link FileAnalysisListener}. It implements
35
 * violation suppression by filtering some violations out, according to
36
 * the {@link ViolationSuppressor}s for the language.
37
 * <p>
38
 * A RuleContext contains a Rule instance and violation reporting methods
39
 * implicitly report only for that rule. Contrary to PMD 6, RuleContext is
40
 * not unique throughout the analysis, a separate one is used per file and rule.
41
 */
42
public final class RuleContext {
43
    // Rule contexts do not need to be thread-safe, within PmdRunnable
44
    // they are stack-local
45

46
    private static final Object[] NO_ARGS = new Object[0];
1✔
47
    static final List<ViolationSuppressor> DEFAULT_SUPPRESSORS = listOf(ViolationSuppressor.NOPMD_COMMENT_SUPPRESSOR,
1✔
48
                                                                        ViolationSuppressor.REGEX_SUPPRESSOR,
49
                                                                        ViolationSuppressor.XPATH_SUPPRESSOR);
50

51
    private final FileAnalysisListener listener;
52
    private final Rule rule;
53

54
    /**
55
     * @apiNote Internal API
56
     */
57
    RuleContext(FileAnalysisListener listener, Rule rule) {
1✔
58
        Objects.requireNonNull(listener, "Listener was null");
1✔
59
        Objects.requireNonNull(rule, "Rule was null");
1✔
60
        this.listener = listener;
1✔
61
        this.rule = rule;
1✔
62
    }
1✔
63

64
    /**
65
     * @apiNote Internal API. Used in {@link AbstractRule} in {@code asCtx(Object)},
66
     * through {@link InternalApiBridge}.
67
     */
68
    Rule getRule() {
69
        return rule;
×
70
    }
71

72
    private String getDefaultMessage() {
73
        return rule.getMessage();
1✔
74
    }
75

76
    /**
77
     * Record a new violation of the contextual rule, at the given node.
78
     *
79
     * @param location Location of the violation
80
     */
81
    public void addViolation(Node location) {
82
        addViolationWithMessage(location, getDefaultMessage(), NO_ARGS);
1✔
83
    }
1✔
84

85
    /**
86
     * Record a new violation of the contextual rule, at the given node.
87
     * The default violation message ({@link Rule#getMessage()}) is formatted
88
     * using the given format arguments.
89
     *
90
     * @param location   Location of the violation
91
     * @param formatArgs Format arguments for the message
92
     *
93
     * @see MessageFormat
94
     */
95
    public void addViolation(Node location, Object... formatArgs) {
96
        addViolationWithMessage(location, getDefaultMessage(), formatArgs);
1✔
97
    }
1✔
98

99
    /**
100
     * Record a new violation of the contextual rule, at the given node.
101
     * The given violation message ({@link Rule#getMessage()}) is treated
102
     * as a format string for a {@link MessageFormat} and should hence use
103
     * appropriate escapes. No formatting arguments are provided.
104
     *
105
     * @param location Location of the violation
106
     * @param message  Violation message
107
     */
108
    public void addViolationWithMessage(Node location, String message) {
109
        addViolationWithPosition(location, -1, -1, message, NO_ARGS);
1✔
110
    }
1✔
111

112
    /**
113
     * Record a new violation of the contextual rule, at the given node.
114
     * The given violation message ({@link Rule#getMessage()}) is treated
115
     * as a format string for a {@link MessageFormat} and should hence use
116
     * appropriate escapes. The given formatting arguments are used.
117
     *
118
     * @param location   Location of the violation
119
     * @param message    Violation message
120
     * @param formatArgs Format arguments for the message
121
     */
122
    public void addViolationWithMessage(Node location, String message, Object... formatArgs) {
123
        addViolationWithPosition(location, -1, -1, message, formatArgs);
1✔
124
    }
1✔
125

126
    /**
127
     * Record a new violation of the contextual rule, at the given node.
128
     * The position is refined using the given begin and end line numbers.
129
     * The given violation message ({@link Rule#getMessage()}) is treated
130
     * as a format string for a {@link MessageFormat} and should hence use
131
     * appropriate escapes. The given formatting arguments are used.
132
     *
133
     * @param node       Location of the violation
134
     * @param message    Violation message
135
     * @param formatArgs Format arguments for the message
136
     */
137
    public void addViolationWithPosition(Node node, int beginLine, int endLine, String message, Object... formatArgs) {
138
        FileLocation location;
139
        if (beginLine != -1 && endLine != -1) {
1!
140
            location = FileLocation.range(node.getTextDocument().getFileId(),
1✔
141
                                          TextRange2d.range2d(beginLine, 1, endLine, 1));
1✔
142
        } else {
143
            location = node.getReportLocation();
1✔
144
        }
145
        addViolationWithPosition(node, node.getAstInfo(), location, message, formatArgs);
1✔
146
    }
1✔
147

148
    /**
149
     * Record a new violation of the contextual rule, at the given location (node or token).
150
     * The position is refined using the given begin and end line numbers.
151
     * The given violation message ({@link Rule#getMessage()}) is treated
152
     * as a format string for a {@link MessageFormat} and should hence use
153
     * appropriate escapes. The given formatting arguments are used.
154
     *
155
     * @param reportable Location of the violation (node or token) - only used to determine suppression
156
     * @param astInfo    Info about the root of the tree ({@link Node#getAstInfo()})
157
     * @param location   Report location of the violation
158
     * @param message    Violation message
159
     * @param formatArgs Format arguments for the message
160
     * @experimental This will probably never be stabilized, will instead be
161
     *      replaced by a fluent API or something to report violations. Do not use
162
     *      this outside of the PMD codebase. See <a href="https://github.com/pmd/pmd/issues/5039">[core] Add fluent API to report violations #5039</a>.
163
     */
164
    @Experimental
165
    public void addViolationWithPosition(Reportable reportable, AstInfo<?> astInfo, FileLocation location,
166
                                         String message, Object... formatArgs) {
167
        Objects.requireNonNull(reportable, "Node was null");
1✔
168
        Objects.requireNonNull(message, "Message was null");
1✔
169
        Objects.requireNonNull(formatArgs, "Format arguments were null, use an empty array");
1✔
170

171
        Node suppressionNode = getNearestNode(reportable, astInfo);
1✔
172
        RuleViolation violation = createViolation(() -> location, astInfo, suppressionNode, message, formatArgs);
1✔
173
        SuppressedViolation suppressed = suppressOrNull(suppressionNode, violation, astInfo);
1✔
174

175
        if (suppressed != null) {
1✔
176
            listener.onSuppressedRuleViolation(suppressed);
1✔
177
        } else {
178
            listener.onRuleViolation(violation);
1✔
179
        }
180
    }
1✔
181

182
    /**
183
     * @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>
184
     */
185
    @Experimental
186
    public void addViolationNoSuppress(Reportable reportable, AstInfo<?> astInfo,
187
                                String message, Object... formatArgs) {
NEW
188
        Objects.requireNonNull(reportable, "Node was null");
×
NEW
189
        Objects.requireNonNull(message, "Message was null");
×
NEW
190
        Objects.requireNonNull(formatArgs, "Format arguments were null, use an empty array");
×
191

NEW
192
        Node nearestNode = getNearestNode(reportable, astInfo);
×
NEW
193
        RuleViolation violation = createViolation(reportable, astInfo, nearestNode, message, formatArgs);
×
NEW
194
        listener.onRuleViolation(violation);
×
NEW
195
    }
×
196

197
    private RuleViolation createViolation(Reportable reportable, AstInfo<?> astInfo, Node nearestNode, String message, Object... formatArgs) {
198
        LanguageVersionHandler handler = astInfo.getLanguageProcessor().services();
1✔
199
        Map<String, String> extraVariables = ViolationDecorator.apply(handler.getViolationDecorator(), nearestNode);
1✔
200
        String description = makeMessage(message, formatArgs, extraVariables);
1✔
201
        FileLocation location = reportable.getReportLocation();
1✔
202
        return new ParametricRuleViolation(rule, location, description, extraVariables);
1✔
203
    }
204

205
    private Node getNearestNode(Reportable reportable, AstInfo<?> astInfo) {
206
        if (reportable instanceof Node) {
1!
207
            return (Node) reportable;
1✔
208
        }
209
        int startOffset = getStartOffset(reportable, astInfo);
×
210
        Optional<Node> foundNode = NodeFindingUtil.findNodeAt(astInfo.getRootNode(), startOffset);
×
211
        // default to the root node
212
        return foundNode.orElse(astInfo.getRootNode());
×
213
    }
214

215
    private static int getStartOffset(Reportable reportable, AstInfo<?> astInfo) {
216
        if (reportable instanceof JavaccToken) {
×
217
            return ((JavaccToken) reportable).getRegion().getStartOffset();
×
218
        }
219
        FileLocation loc = reportable.getReportLocation();
×
220
        return astInfo.getTextDocument().offsetAtLineColumn(loc.getStartPos());
×
221
    }
222

223
    private static @Nullable SuppressedViolation suppressOrNull(Node location, RuleViolation rv, AstInfo<?> astInfo) {
224
        LanguageVersionHandler handler = astInfo.getLanguageProcessor().services();
1✔
225
        SuppressedViolation suppressed = ViolationSuppressor.suppressOrNull(handler.getExtraViolationSuppressors(), rv, location);
1✔
226
        if (suppressed == null) {
1!
227
            suppressed = ViolationSuppressor.suppressOrNull(DEFAULT_SUPPRESSORS, rv, location);
1✔
228
        }
229
        return suppressed;
1✔
230
    }
231

232
    private String makeMessage(@NonNull String message, Object[] args, Map<String, String> extraVars) {
233
        // Escape PMD specific variable message format, specifically the {
234
        // in the ${, so MessageFormat doesn't bitch.
235
        final String escapedMessage = StringUtils.replace(message, "${", "$'{'");
1✔
236
        String formatted = MessageFormat.format(escapedMessage, args);
1✔
237
        return expandVariables(formatted, extraVars);
1✔
238
    }
239

240

241
    private String expandVariables(String message, Map<String, String> extraVars) {
242

243
        if (!message.contains("${")) {
1✔
244
            return message;
1✔
245
        }
246

247
        StringBuilder buf = new StringBuilder(message);
1✔
248
        int startIndex = -1;
1✔
249
        while ((startIndex = buf.indexOf("${", startIndex + 1)) >= 0) {
1✔
250
            final int endIndex = buf.indexOf("}", startIndex);
1✔
251
            if (endIndex >= 0) {
1!
252
                final String name = buf.substring(startIndex + 2, endIndex);
1✔
253
                String variableValue = getVariableValue(name, extraVars);
1✔
254
                if (variableValue != null) {
1✔
255
                    buf.replace(startIndex, endIndex + 1, variableValue);
1✔
256
                }
257
            }
258
        }
1✔
259
        return buf.toString();
1✔
260
    }
261

262
    private String getVariableValue(String name, Map<String, String> extraVars) {
263
        String value = extraVars.get(name);
1✔
264
        if (value != null) {
1✔
265
            return value;
1✔
266
        }
267
        final PropertyDescriptor<?> propertyDescriptor = rule.getPropertyDescriptor(name);
1✔
268
        return propertyDescriptor == null ? null : String.valueOf(rule.getProperty(propertyDescriptor));
1✔
269
    }
270
}
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