Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Deployment

ApexUnitTestClassShouldHaveAsserts / ApexBadCrypto / Code Analyzer violations blocking deploy

Salesforce's open-source code scanners (PMD-Apex, Salesforce Code Analyzer) found rule violations in your codebase. They don't block deploys by themselves but most CI pipelines configure them as gates. Each violation has a specific fix.

Also seen asApexUnitTestClassShouldHaveAsserts·ApexBadCrypto·PMD violations·Code Analyzer violations

A team adopts Salesforce Code Analyzer in their CI pipeline to catch quality issues before they reach production. The first run on the codebase returns 312 warnings. The deploy is blocked. Half the team thinks the rules are too strict; the other half wants to fix every warning. The architect needs to decide what to ship.

What the platform is checking

Salesforce Code Analyzer is a wrapper around several open-source linters (PMD, ESLint, RetireJS) plus Salesforce-specific rules. When integrated into a CI pipeline or deploy gate, Code Analyzer runs against the source code and reports violations of style, security, and correctness rules. The deploy can be configured to fail if any violation above a chosen severity is detected.

Three categories of rules dominate the warning list. Style violations (ApexAssertionsShouldIncludeMessage, MethodNamingConventions) are cosmetic and rarely block deploys. Correctness violations (ApexUnitTestClassShouldHaveAsserts, ApexDoc) flag code that probably has bugs or is missing documentation. Security violations (ApexCRUDViolation, ApexSOQLInjection, ApexBadCrypto) flag patterns that could be exploited.

Salesforce's official Code Analyzer ships with PMD's Apex ruleset by default. The ruleset is opinionated; some rules are unambiguously valuable, some are debatable. Teams typically customize the ruleset to disable noise and focus on what matters. The configuration lives in pmd-rules.xml or in a .code-analyzer.yaml file at the repo root.

The rules with the highest action value are the security rules. A flagged SOQL-injection vector is a real vulnerability. A flagged missing CRUD check is a real bypass risk. These need fixing or explicit justification.

The broken examples

A test method without assertions:

@isTest
private class OpportunityServiceTest {
    @isTest
    static void testCreate() {
        Opportunity opp = new Opportunity(Name='Test', StageName='Prospecting', CloseDate=Date.today());
        insert opp;
        // No System.assertEquals or System.assert
    }
}

PMD flags this with ApexUnitTestClassShouldHaveAsserts. The test increases coverage but provides no real protection. It would pass even if insert opp did nothing or returned a record with the wrong fields.

A dynamic SOQL query built by string concatenation:

public List<Account> findByName(String name) {
    String soql = 'SELECT Id, Name FROM Account WHERE Name = \'' + name + '\'';
    return Database.query(soql);
}

PMD flags this with ApexSOQLInjection. The name parameter is concatenated directly into the SOQL string. A caller passing ' OR Name LIKE '% constructs a query that returns more rows than intended.

A class using weak crypto:

public class TokenSigner {
    public static String sign(String payload, String secret) {
        Blob hash = Crypto.generateDigest('MD5', Blob.valueOf(payload + secret));
        return EncodingUtil.base64Encode(hash);
    }
}

PMD flags this with ApexBadCrypto. MD5 is broken for security use; the code should be using SHA-256 or HMAC-SHA-256.

A SOQL query without explicit CRUD/FLS checks:

public List<Contact> getContactsForAccount(Id accountId) {
    return [SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId];
}

PMD's ApexCRUDViolation rule flags this in some configurations. The code assumes the running user has read access to Contact and to the Email field; if they don't, the query throws at runtime.

The fix, three paths

Fix the underlying issue. For each warning, decide whether the rule is correct. If yes, fix the code. The test method gets real assertions. The SOQL query uses bind variables. The crypto upgrades to SHA-256. The CRUD-violation gets an explicit FLS check or a with sharing declaration.

Suppress the warning with justification. Some rules don't apply to every situation. PMD allows suppression at the line, method, or class level via comments:

@SuppressWarnings('PMD.ApexCRUDViolation')
public List<Account> getAllAccounts() {
    // System-mode query intended for an internal service
    return [SELECT Id, Name FROM Account];
}

The suppression is fine when the rule genuinely doesn't apply, but every suppression should carry a comment explaining why. A reviewer should be able to look at the suppression and agree with it.

Customize the ruleset. If a rule produces 50 warnings across the codebase and none of them are actionable, consider disabling the rule in your config. The trade-off is that any future genuine violation also goes unflagged. Disable with care.

The fixed examples

The test with real assertions:

@isTest
private class OpportunityServiceTest {
    @isTest
    static void testCreate() {
        Opportunity opp = new Opportunity(Name='Test', StageName='Prospecting', CloseDate=Date.today());
        insert opp;
        Opportunity stored = [SELECT Id, Name, StageName FROM Opportunity WHERE Id = :opp.Id];
        System.assertEquals('Test', stored.Name);
        System.assertEquals('Prospecting', stored.StageName);
    }
}

The dynamic SOQL with bind variables:

public List<Account> findByName(String name) {
    return [SELECT Id, Name FROM Account WHERE Name = :name];
}

If dynamic SOQL is genuinely needed, use Database.queryWithBinds:

public List<Account> findByName(String name) {
    String soql = 'SELECT Id, Name FROM Account WHERE Name = :nameParam';
    return Database.queryWithBinds(soql, new Map<String, Object>{'nameParam' => name}, AccessLevel.USER_MODE);
}

The crypto upgraded to SHA-256:

public class TokenSigner {
    public static String sign(String payload, String secret) {
        Blob mac = Crypto.generateMac('HmacSHA256', Blob.valueOf(payload), Blob.valueOf(secret));
        return EncodingUtil.base64Encode(mac);
    }
}

The query with explicit FLS handling via USER_MODE:

public List<Contact> getContactsForAccount(Id accountId) {
    return [
        SELECT Id, Name, Email
        FROM Contact
        WHERE AccountId = :accountId
        WITH USER_MODE
    ];
}

WITH USER_MODE causes Salesforce to enforce the running user's CRUD and FLS automatically. The query throws or returns nothing if access is denied, but the code stays clean.

Severity tiers and what to block on

A practical CI setup blocks the deploy on specific severities and lets others through as informational. A common tier:

  • Block on severity 1 (security): ApexSOQLInjection, ApexBadCrypto, ApexInsecureEndpoint, ApexCSRF. These are unambiguous security issues. A new violation should fail the build.
  • Warn on severity 2 (correctness): ApexCRUDViolation, ApexSharingViolations, ApexUnitTestClassShouldHaveAsserts. Worth fixing but not always blocking.
  • Suppress severity 3 (style): ApexDoc, MethodNamingConventions, OverrideBothEqualsAndHashcode. Often noise; configure thresholds based on team conventions.

The tiering reduces alert fatigue. Developers learn that a blocked deploy is genuinely actionable. A noisy CI that blocks on style violations leads to widespread @SuppressWarnings annotations and erodes the value of the analyzer entirely.

Running Code Analyzer locally

Before integrating into CI, every developer should be able to run the analyzer locally. The CLI command:

sf code-analyzer run --workspace . --rule-selector security

The output lists each violation with file path, line number, rule name, and severity. Developers fix violations on their feature branch before opening a pull request. CI catches the few that slip through.

For VS Code users, the Salesforce Code Analyzer extension annotates the editor with inline warnings. The feedback loop tightens; violations are caught at the moment they're written, not at PR review time.

Customizing the ruleset

The default ruleset is a starting point. Teams should curate based on what matters for their codebase. The configuration file:

# .code-analyzer.yaml
engines:
  pmd:
    rule_selectors:
      - security
      - performance
    disable_rules:
      - ApexDoc
      - MethodNamingConventions

Reviewing the ruleset is itself an exercise in defining team standards. The conversation around "do we care about this rule?" surfaces implicit assumptions about code quality. Document the decisions; future team members benefit from understanding why specific rules were enabled or disabled.

Integrating with the build pipeline

The standard integration runs the analyzer as a pre-deploy gate:

# GitHub Actions example
- name: Run Code Analyzer
  run: |
    sf code-analyzer run --workspace . --rule-selector security --severity-threshold 1
    if [ $? -ne 0 ]; then exit 1; fi

The build fails if any severity-1 violation is detected. Developers fix on their branch. The main branch stays clean.

Some teams configure the analyzer to fail only on new violations, not pre-existing ones. The diff is calculated by comparing the violation count on the feature branch against the main branch. New violations fail the build; existing violations are tolerated until they're touched. This pragmatic approach lets teams adopt the analyzer without immediately fixing every legacy violation.

Edge case: false positives

Some violations are genuine false positives. A rule that flags a SOQL-injection pattern might not understand that the code already validates the input. A rule that flags missing CRUD might be confused by a method that's called only from a trusted system context.

For each false positive, decide whether to suppress at the line level (cleanest) or disable the rule entirely (only if the rule produces many false positives in your codebase). Document the suppression with a comment explaining why; a future reviewer should be able to verify the reasoning.

Edge case: third-party code

Code copied from managed packages or AppExchange listings often violates rules your team would never accept in internal code. Decide whether to scan third-party code or exclude it from the analyzer. Exclusion patterns belong in the configuration file:

exclude:
  - "force-app/main/default/classes/3rdparty/**"
  - "node_modules/**"

Excluding third-party code from analyzer scans is reasonable; your team didn't write it and can't always fix it. Just be explicit about the exclusion so reviewers understand what's being skipped.

A subtle case: rule version drift

Salesforce Code Analyzer and its underlying engines (PMD, ESLint) release new rule versions periodically. A rule that didn't exist last quarter may flag patterns that have been in your codebase for years. The upgrade can suddenly turn a clean build into a failing one.

The fix is to pin the analyzer version in CI. The repo's package.json or pipeline configuration locks the analyzer to a specific release. Upgrades are then deliberate: someone opens a PR that bumps the version, the team reviews the new rules together, and the codebase is updated to comply with any genuine new findings.

- name: Run Code Analyzer (pinned)
  run: |
    sf plugins install @salesforce/sfdx-scanner@4.6.0
    sf code-analyzer run --workspace . --rule-selector security

Pinning the version prevents surprise build breakage. The trade-off is that you have to manually upgrade to get new rules, which can lead to stagnation if nobody owns the upgrades.

A subtle case: legacy code exclusions

A new team adopting Code Analyzer on a five-year-old codebase often finds hundreds or thousands of violations. Fixing every one before enabling the gate is impractical. The pragmatic path is to enable the analyzer with a baseline that tolerates existing violations.

Some teams maintain a baseline file that lists every current violation. New PRs are compared against the baseline; only new violations fail the build. Existing violations are tolerated until they're touched.

- name: Run Code Analyzer (diff mode)
  run: |
    sf code-analyzer run --workspace . > current.json
    diff <(jq '.violations | length' baseline.json) <(jq '.violations | length' current.json) || exit 1

The baseline approach lets teams ship the analyzer immediately without waiting for a cleanup sprint. The baseline shrinks naturally over time as developers touch code with violations and fix them as a byproduct.

Defensive habits

Run Code Analyzer locally before pushing. The friction of fixing violations at write time is much lower than fixing them at PR review or in CI. Treat local execution as part of the workflow, not an optional polish step. Most editors have an integration that shows violations inline; install it and trust it.

Document team standards. A README in the repo, or a section in the engineering wiki, explaining the team's rule selections and the rationale behind each one. New team members onboard faster when the standards are explicit. Without documentation, suppression decisions become tribal knowledge that erodes when people leave.

Treat security violations as severity 1, always. The cost of suppressing a real SOQL-injection finding is much higher than the cost of writing a parameterized query. Don't relax security thresholds to reduce build failures; fix the code instead. Security suppressions should require a second reviewer with security expertise.

Pair the analyzer with manual code review. The analyzer catches mechanical issues; humans catch design issues, missing edge cases, and unclear naming. Both layers contribute; neither replaces the other. The analyzer also frees the human reviewer to focus on the higher-value parts of the review.

Quick recovery checklist

When a deploy is blocked by Code Analyzer warnings:

  1. Get the report. Run sf code-analyzer run and examine the output.
  2. Sort by severity. Severity-1 violations are the priority.
  3. For each violation, decide: fix, suppress with justification, or disable the rule.
  4. Re-run the analyzer to confirm the remaining violations are below the build threshold.
  5. Document any rule changes or new suppressions in the commit message.

Most analyzer-blocked deploys resolve within a few hours once the team agrees on the rule tiers. Long incidents tend to reveal codebase-wide issues (consistent missing FLS checks, scattered weak crypto) that deserve a focused effort rather than band-aid suppressions.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Deployment errors