Handling False Positives in Salesforce Static Analysis — Without Turning the Scanner Off
A Salesforce static-analysis report full of findings nobody plans to fix trains the team to ignore the report. The fix is to accept each of those findings explicitly — recording who accepted it, why, and for how long — so the numbers everyone reads reflect real open work. In mtdt.io you suppress a finding from the scan results with a category and a reason, one path pattern can cover an entire third-party library, security findings can only be accepted with an expiry date of at most 180 days, and every deploy gate, pull-request check, and report reads the same accepted-adjusted counts.
Why the report fills up with noise
Run PMD across an org that has never been scanned and the volume comes from a handful of predictable sources.
- Documentation rules.
ApexDocfires once for every undocumented class, method, and property. On a mature codebase that is thousands of findings about comments. - Style rules.
IfStmtsMustUseBracesandDebugsShouldUseLoggingLevelfire on ordinary code that works fine and follows a convention the team chose years ago. - Third-party libraries. Apex Enterprise Patterns (fflib), test frameworks, and managed-package extension code all sit in the same org and get scanned like anything else. On one org we measured, the
fflib_classes alone produced about 13,000 findings across 58 files — code nobody on that team is allowed to edit. - Genuine false positives. A rule matches a shape in the code without the problem the rule describes, which happens most often with security and sharing rules on code that does its checks somewhere the analyzer cannot see.
Those three noisy rules alone are 17–53% of raw findings on an unconfigured org, depending on the codebase. When they lead the report, the real SOQL-in-a-loop finding is on page four.
How teams handle this today
PMD has suppression built in, and it works. Understanding what it costs is the point of this section.
// NOPMD line comments. Add // NOPMD at the end of a line and PMD skips that line. You can add a note after it — // NOPMD: intentional, batch context — for the humans, though PMD itself treats everything after the marker as free text.
@SuppressWarnings annotations. Place @SuppressWarnings('PMD.ExcessiveClassLength') on a class or method and PMD skips that rule inside it. @SuppressWarnings('PMD') skips everything. Apex accepts several rules in one string, separated by commas, and Apex requires single quotes. Salesforce's own documentation notes that the annotation does nothing to the Apex runtime — it exists to inform third-party tools.
Ruleset XML. A PMD ruleset file lists which rules run, and <exclude-pattern> entries drop whole paths before any rule sees them — the standard way to keep a vendor library out of a report, with regex patterns such as .*/fflib_.*\.cls$.
These three cover a lot of ground. Their costs are the same three, every time:
| Where the decision lives | Reason recorded | Expires | Shows up in an audit trail | |
|---|---|---|---|---|
// NOPMD | In the Apex file | Free-text comment | No | Only in git history |
@SuppressWarnings('PMD.Rule') | In the Apex file | No field for it | No | Only in git history |
Ruleset <exclude-pattern> | In a config file in the repo | No field for it | No | Only in git history |
Three things follow from that table. Accepting a finding means editing the code the finding is about, which is impossible for a managed package and awkward for a vendor library you resync. A decision made once holds forever, so "we'll accept this until Q3" quietly becomes permanent. And the record of who decided lives in a commit, which means nobody reviews it — a security finding silenced by a one-line annotation in 2023 is still silenced today, and no screen will ever mention it.
The other common response is worse: turn the whole rule off across the org, or stop running the scan. Both make the report quiet by making it uninformative.
Suppress a finding in mtdt.io
mtdt.io keeps the decision in the tool. Open a scan result, go to the Findings tab, and each row has a Suppress action. The findings table filters by text, severity, rule, and security-only, so you can work through a noisy rule in one pass.
The suppression dialog asks for four things:
- What it covers — this exact finding, this rule in this file, or a path pattern over file paths. This rule in this file is the one that holds up over time: it survives line edits and Flow version changes, where an exact-finding suppression is released as soon as the line text changes.
- A category — False positive, Accepted risk, Won't fix, or Third-party code. The category drives the breakdown in the monthly org report, which is why "the scanner was wrong" and "we accept this risk" are not the same entry.
- A reason — required, free text, stored with the suppression and shown in the tooltip on the suppressed row.
- An expiry date — optional for most findings, mandatory for security findings.
The finding itself is never modified or deleted. mtdt.io stores the scan output as it came and applies suppression when results are read, which is what makes un-suppressing instant and keeps the raw history honest.
One pattern for a whole third-party library
For vendor code, the level to reach for is a path pattern — a glob over the file path, optionally narrowed to one rule. src/classes/fflib_* is a single suppression that covers all ~13,000 findings across those 58 fflib files, categorised as Third-party code, with the reason stating that the team does not maintain the library.
Two guardrails apply. The only wildcard is *, and underscores are matched literally, so src/classes/fflib_* covers the fflib_ prefix and nothing beyond it. And a pattern needs at least four literal characters — silencing most of an org should not be one keystroke away.
Security findings expire by design
A security finding can only be suppressed with an expiry date, and the maximum is 180 days. The form pre-fills 90. When the date passes, the finding comes back on its own.
mtdt.io works out whether a finding is security-relevant on the server, from the findings the suppression would actually cover, so the browser has no way to claim otherwise. A broad rule-level or path-pattern suppression created over ordinary findings also cannot later grow to cover a security finding. There is no combination of options that produces a permanently silenced security finding.
Every screen reads the same numbers
One database filter produces the accepted-adjusted counts, and every consumer calls it: the scan-results dialog, the Org Analyses list, the pre-deploy code-quality gate, pull-request check annotations, the scan delta, and the monthly org report. A finding your team accepted is accepted in all of them.
- Deploy gates compare thresholds against the adjusted counts, so an accepted finding does not block a deploy, and the gate's reason states how many findings were suppressed.
- The pull-request check filters the findings as well as the counts, so an accepted finding stops reappearing as an inline annotation on every PR.
- Scan delta — new, fixed, persisting — also runs on the adjusted counts, so accepting a finding today does not leave it flagged as new in next week's scan.
- Suppressed volume stays visible. Wherever counts appear, an N suppressed chip appears with them. A row reading "Clean" because of accepted risk does not get to look plainly clean.
Reviewable, revocable, audited
The Suppressed findings panel on the Static Analysis page, beside Scan profiles, is the registry of every decision your team has made: category, reason, scope, what it covers, expiry, and a Revoke action. Filter by state to see what is active, what expired, and what was revoked. Suppressions approaching their expiry date are flagged.
Suppressing and revoking sit behind their own team permission, separate from the permission that manages static analysis, because configuring scan profiles and accepting a security risk are different authorities. Every suppress and every revoke writes a team audit-log entry with the author, the reason, and how many findings were matched.
Nothing is deleted. A revoked suppression stays in the registry under the Revoked state, as the record that someone once accepted that finding.
When to exclude the rule instead
Some rules should never run for your team. That belongs on the scan profile as an excluded rule, and it happens while the scan runs, so no findings are produced at all. mtdt.io's product defaults exclude ApexDoc, IfStmtsMustUseBraces, and DebugsShouldUseLoggingLevel for exactly this reason — asking a person to accept 17–53% of an org's raw findings one at a time would be theatre.
The trade-off is reviewability. An excluded rule's findings were never collected, so you cannot look back at what it would have said. A suppressed finding is stored, audited, revocable in one click, and every historical number recalculates when you revoke it. Full mechanics for both are in Suppressing Static Analysis Findings.
FAQ
Should I suppress the finding or exclude the rule? Exclude the rule on the scan profile when the rule has no value for your team at all — the rule then does not run, and its findings are never collected. Suppress when the rule is worth running and this particular finding is accepted: the finding stays stored, carries a category and a reason, appears in the audit log, and can be revoked in one click with every historical count recalculating.
Can I suppress a Salesforce security finding permanently? No. In mtdt.io a security finding can only be suppressed together with an expiry date, capped at 180 days, and the form pre-fills 90 days. mtdt.io derives on the server whether a finding is security-relevant from the findings the suppression covers, so the setting cannot be bypassed from the browser, and a broad suppression created over ordinary findings cannot later expand to cover a security finding.
Do suppressed findings still block my deploy? No. The pre-deploy code-quality gate and the pull-request check compare your thresholds against the accepted-adjusted counts, and the gate's reason names how many findings were suppressed. The pull-request check filters the findings themselves too, so an accepted finding stops coming back as an inline annotation on every pull request.
What about third-party packages and libraries like fflib?
Use a path pattern with the Third-party code category. One suppression on src/classes/fflib_* covers every finding in those files — about 13,000 across 58 files on an org we measured — with no edits to code your team does not own. The only wildcard is *, underscores match literally, and a pattern needs at least four literal characters.
Does suppressing a finding delete it? No. mtdt.io never modifies or deletes stored scan findings; suppression is applied when results are read. That makes un-suppressing instant, keeps the raw scan history intact, and means a revoked or expired suppression restores the finding everywhere with nothing to re-run.
How is this different from // NOPMD or @SuppressWarnings('PMD.Rule')?
Those work, and they live in the Apex file itself — so accepting a finding means editing the code, which is impossible for a managed package, and the decision has no category, no expiry, and no record outside git history. Suppressing in mtdt.io keeps the code untouched, requires a category and a reason, expires security acceptances automatically, gates the action behind its own team permission, and writes every suppress and revoke to the team audit log.
Why did a Flow's suppressions disappear after I activated a new version? A finding is identified by its engine, rule, file path, line text, and occurrence index, so changing the underlying content releases the suppression and the finding returns for review. Activating a new Flow version does that to all of that Flow's suppressions. Choosing this rule in this file instead of the exact finding keeps the acceptance in place across those changes.
Keep the scanner on, and the report honest
Accept the findings you're never going to fix — with a category, a reason, an audit entry, and an expiry date on anything security-related — and let every gate, pull-request check, and report read the same numbers.
Log in to mtdt.io and clean up your static analysis report.
Related: Suppressing Static Analysis Findings · Salesforce Static Code Analysis: Catch Apex Problems Before They Ship
Was this page helpful?
Let us know how we did
