Skip to content

starch-uk/sca-extra

sca-extra

Additional PMD and Regex rules for testing Salesforce Apex code using Salesforce Code Analyzer. Most rules are implemented as XPath 3.1 expressions that operate on the PMD Apex AST. Some rules use the Regex engine for pattern-based matching.

Repository: https://github.com/starch-uk/sca-extra

Note: This entire project has been vibe coded. The plan for the project can be found in PLAN.md.

Overview

This project provides a comprehensive set of PMD and Regex rules for Salesforce Apex code analysis. Most rules are implemented using XPath 3.1 expressions only (no custom Java classes), making them easy to understand, modify, and maintain. Some rules use the Regex engine for pattern-based matching (e.g., NoConsecutiveBlankLines, ProhibitPrettierIgnore, ProhibitSuppressWarnings, NoLongLines).

Configuring code-analyzer.yml

The code-analyzer.yml file is where you configure which rules to enable, customize rule properties, and disable default rules. This section explains how to customize your configuration.

Where the rulesets live:

  • Most rules are defined as XML rulesets under the rulesets/ directory, grouped by category (see Rule Categories below for an overview).
  • Some rules (like NoConsecutiveBlankLines, ProhibitPrettierIgnore, ProhibitSuppressWarnings, and NoLongLines) are defined as Regex rules in the repository's code-analyzer.yml under engines.regex.custom_rules.
  • You can copy the entire rulesets/ folder into your Salesforce project, or just the specific category folders you want to enable.

Add the rulesets to code-analyzer.yml:

  • Create or update a code-analyzer.yml file in the root of your Salesforce project.
  • Reference the ruleset XML files you want to enable under engines.pmd.custom_rulesets, using paths relative to your project root (for full examples, see Installation and Usage below).
  • Important: Do NOT copy the repository's code-analyzer.yml file in its entirety, as it has rulesets: [] (disabled). Instead, copy only the Regex rules configuration from the engines.regex.custom_rules section into your own code-analyzer.yml (see Regex Rules section below).

Comprehensive Example (All Rules)

Here's a complete code-analyzer.yml configuration that includes all 48 PMD rules and 4 Regex rules from this repository:

engines:
    pmd:
        disable_engine: false
        file_extensions:
            apex:
                - .apex
                - .cls
                - .trigger
        custom_rulesets:
            - rulesets/bestpractices/FinalVariablesMustBeFinal.xml
            - rulesets/bestpractices/RegexPatternsMustBeStaticFinal.xml
            - rulesets/bestpractices/StaticMethodsMustBeStatic.xml
            - rulesets/bestpractices/StaticVariablesMustBeFinalAndScreamingSnakeCase.xml
            - rulesets/bestpractices/TestClassIsParallel.xml
            - rulesets/codestyle/AvoidMagicNumbers.xml
            - rulesets/codestyle/AvoidOneLinerMethods.xml
            - rulesets/codestyle/InnerClassesMustBeOneWord.xml
            - rulesets/codestyle/MapShouldBeInitializedWithValues.xml
            - rulesets/codestyle/MultipleStringContainsCalls.xml
            - rulesets/codestyle/NoMethodCallsAsArguments.xml
            - rulesets/codestyle/NoMethodCallsInConditionals.xml
            - rulesets/codestyle/NoSingleLetterVariableNames.xml
            - rulesets/codestyle/PreferBuilderPatternChaining.xml
            - rulesets/codestyle/PreferConcatenationOverStringJoinWithEmpty.xml
            - rulesets/codestyle/PreferMethodCallsInLoopConditions.xml
            - rulesets/codestyle/PreferNullCoalescingOverTernary.xml
            - rulesets/codestyle/PreferSafeNavigationOperator.xml
            - rulesets/codestyle/PreferStringJoinOverConcatenation.xml
            - rulesets/codestyle/PreferStringJoinOverMultipleNewlines.xml
            - rulesets/codestyle/PreferStringJoinWithSeparatorOverEmpty.xml
            - rulesets/codestyle/SingleArgumentMustBeSingleLine.xml
            - rulesets/codestyle/VariablesMustNotShareNamesWithClasses.xml
            - rulesets/design/AvoidLowValueWrapperMethods.xml
            - rulesets/design/AvoidTrivialPropertyGetters.xml
            - rulesets/design/ClassesMustHaveMethods.xml
            - rulesets/design/CombineNestedIfStatements.xml
            - rulesets/design/EnumMinimumValues.xml
            - rulesets/design/InnerClassesCannotBeStatic.xml
            - rulesets/design/InnerClassesCannotHaveStaticMembers.xml
            - rulesets/design/NoCustomParameterObjects.xml
            - rulesets/design/NoInterfacesEndingWithCallback.xml
            - rulesets/design/NoParameterClasses.xml
            - rulesets/design/NoThisOutsideConstructors.xml
            - rulesets/design/NoUnnecessaryAttributeVariables.xml
            - rulesets/design/NoUnnecessaryReturnVariables.xml
            - rulesets/design/UnusedVariableNotReturnedOrPassed.xml
            - rulesets/design/PreferPropertySyntaxOverGetterMethods.xml
            - rulesets/design/PreferSwitchOverIfElseChains.xml
            - rulesets/design/SingleParameterMustBeSingleLine.xml
            - rulesets/design/DmlInLoopAcrossMethods.xml
            - rulesets/documentation/ExceptionDocumentationRequired.xml
            - rulesets/documentation/MethodsRequireExampleTag.xml
            - rulesets/documentation/ProhibitAuthorSinceVersionTags.xml
            - rulesets/documentation/SingleLineDocumentationFormat.xml
            - rulesets/documentation/ValidGroupTagValues.xml
    regex:
        custom_rules:
            NoConsecutiveBlankLines:
                regex: /\n\s*\n\s*\n/g
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prevents two or more consecutive blank lines in Apex code.
                    Code should have at most one blank line between statements,
                    methods, or other code elements.'
                violation_message:
                    'Two or more consecutive blank lines are not allowed. Use at
                    most one blank line between statements.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']
            ProhibitSuppressWarnings:
                regex: /@SuppressWarnings\([^)]*\)|\/\/\s*NOPMD/gi
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prohibits the use of @SuppressWarnings annotations and
                    NOPMD comments in Apex code. Suppressions hide code quality
                    issues; prefer fixing the underlying problems or improving
                    rules instead.'
                violation_message:
                    'Suppression of warnings is not allowed. Fix the underlying
                    issue or improve the rule instead of suppressing violations.'
                severity: 'High'
                tags: ['CodeStyle', 'Recommended']
            NoLongLines:
                regex: /.{81,}/gm
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Enforces a maximum line length of 80 characters. Lines
                    longer than 80 characters reduce readability and make code
                    harder to review. Use shorter class, attribute, method, and
                    variable names to improve readability.'
                violation_message:
                    'Line exceeds 80 characters. Use shorter class, attribute,
                    method, and variable names to improve readability.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']
            ProhibitPrettierIgnore:
                regex: /\/\/\s*prettier-ignore/gi
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prohibits the use of prettier-ignore comments in Apex code.
                    Code should be formatted consistently without exceptions.
                    Using prettier-ignore comments undermines code formatting
                    standards and makes the codebase less maintainable.'
                violation_message:
                    'Prettier-ignore comments are not allowed. Code should be
                    formatted consistently without exceptions.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']

Note: You can copy this entire configuration into your project's code-analyzer.yml file, or select only the rules you want to enable. See the sections below for information on customizing rules and selecting specific categories.

Basic Structure

The code-analyzer.yml file uses YAML syntax and typically includes:

engines:
    pmd:
        custom_rulesets:
            # List of ruleset files to include
            - rulesets/design/InnerClassesCannotBeStatic.xml

rules:
    # Rule-specific configuration (severity and tags only)
    # Note: Property overrides are NOT supported via code-analyzer.yml
    # Use custom ruleset XML files with ref= syntax instead (see "Overriding Rule Properties" below)
    RuleName:
        severity: 'High'
        tags: ['Recommended']

Adding Custom Rules

To add rules from this repository, reference the ruleset XML files under engines.pmd.custom_rulesets using paths relative to your project root:

engines:
    pmd:
        custom_rulesets:
            # Design rules
            - rulesets/design/InnerClassesCannotBeStatic.xml
            - rulesets/design/InnerClassesCannotHaveStaticMembers.xml

            # Best practices rules
            - rulesets/bestpractices/FinalVariablesMustBeFinal.xml
            - rulesets/bestpractices/StaticMethodsMustBeStatic.xml

            # Code style rules (including naming)
            - rulesets/codestyle/NoSingleLetterVariableNames.xml

            # More code style rules
            - rulesets/codestyle/NoMethodCallsInConditionals.xml
            - rulesets/codestyle/PreferSafeNavigationOperator.xml

Important:

  • Copy the rulesets/ directory (or specific category folders) to your Salesforce project root first
  • Paths in code-analyzer.yml are relative to your project root
  • You can include as many or as few rules as needed
  • Use engines.pmd.custom_rulesets (not rulesets:) to reference PMD rulesets

Customizing Rules

Important: PMD 7+ does not support dynamic properties for XPath rules via code-analyzer.yml. However, rules are designed with configurable variables at the top of their XPath expressions, making customization straightforward.

How to Customize a Rule:

  1. Locate the rule file in the rulesets/ directory (e.g., rulesets/design/EnumMinimumValues.xml)

  2. Edit the configurable variables at the top of the XPath expression (usually in a let statement). You don't need to edit the entire XPath - just change the variable values.

  3. Update the rule description to reflect your changes (optional but recommended)

Example 1 - Change EnumMinimumValues threshold from 3 to 4:

Open rulesets/design/EnumMinimumValues.xml and find the variable at the top of the XPath expression:

<property name="xpath">
    <value>
        <![CDATA[
        let $minValues := 3  <!-- Change this value -->
        return //UserEnum[
            count(Field) < $minValues
        ]
        ]]>
    </value>
</property>

Change 3 to 4:

<property name="xpath">
    <value>
        <![CDATA[
        let $minValues := 4  <!-- Changed from 3 to 4 -->
        return //UserEnum[
            count(Field) < $minValues
        ]
        ]]>
    </value>
</property>

Best Practices:

  • Look for variables at the top - Most rules have configurable variables in let statements at the beginning of the XPath expression
  • Only change variable values - You typically don't need to modify the rest of the XPath expression
  • Keep a backup of the original rule file before making changes
  • Document your changes in comments or commit messages
  • Test your changes by running sf code-analyzer run on your codebase
  • Consider version control - if you customize rules, you may want to maintain your own fork or keep custom rules in a separate directory
  • Update rule descriptions to reflect your customizations (optional)

Note: If you need different behavior for different parts of your codebase, you can:

  1. Create multiple copies of the rule with different names (e.g., EnumMinimumValues4.xml, EnumMinimumValues5.xml)
  2. Reference both in your code-analyzer.yml
  3. Use PMD's exclusion patterns if needed

Rule Examples: All rules include <example> sections in their XML files showing violations and valid code patterns. These examples help clarify the rule's intent. Rules with configurable thresholds use easy-to-edit variables at the top of the XPath expression, making customization straightforward - you only need to change the variable values, not the entire XPath logic.

Disabling Default Rules

To disable default PMD rules provided by Salesforce Code Analyzer, you can exclude them from standard PMD category rulesets. Create a custom ruleset XML file that references the category but excludes specific rules:

<?xml version="1.0" ?>
<ruleset name="CustomPMDRules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
    <description>Custom PMD rules with some defaults disabled</description>

    <!-- Include standard security rules, but exclude ApexCRUDViolation -->
    <rule ref="category/apex/security.xml">
        <exclude name="ApexCRUDViolation" />
    </rule>

    <!-- Include best practices, but exclude specific rules -->
    <rule ref="category/apex/bestpractices.xml">
        <exclude name="DebugsShouldUseLoggingLevel" />
        <exclude name="ApexUnitTestClassShouldHaveRunAs" />
    </rule>
</ruleset>

Save this as rulesets/custom-disabled-defaults.xml and reference it in code-analyzer.yml:

engines:
    pmd:
        custom_rulesets:
            - rulesets/custom-disabled-defaults.xml # Custom ruleset with disabled defaults
            - rulesets/design/InnerClassesCannotBeStatic.xml
            - rulesets/codestyle/NoSingleLetterVariableNames.xml

Alternatively, if you're creating a comprehensive custom ruleset, you can combine standard PMD rules with exclusions in a single XML file. Note that custom rulesets from this repository are typically referenced separately in code-analyzer.yml (as shown in the example below):

<?xml version="1.0" ?>
<ruleset
    name="ComprehensiveRules"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
>
    <description>Standard PMD rules with custom exclusions</description>

    <!-- Standard PMD rules with some disabled -->
    <rule ref="category/apex/security.xml">
        <priority>1</priority>
        <exclude name="ApexCRUDViolation" />
    </rule>

    <rule ref="category/apex/bestpractices.xml">
        <priority>2</priority>
        <exclude name="DebugsShouldUseLoggingLevel" />
    </rule>

    <!-- Standard PMD category rulesets -->
    <rule ref="category/apex/design.xml">
        <priority>2</priority>
    </rule>
</ruleset>

Then reference both this comprehensive ruleset and your custom rulesets in code-analyzer.yml:

engines:
    pmd:
        custom_rulesets:
            - rulesets/custom-comprehensive-rules.xml # Standard rules with exclusions
            - rulesets/design/InnerClassesCannotBeStatic.xml # Custom rules
            - rulesets/codestyle/NoSingleLetterVariableNames.xml

Standard PMD category rulesets available:

  • category/apex/security.xml - Security-related rules
  • category/apex/bestpractices.xml - Best practice rules
  • category/apex/design.xml - Design pattern rules
  • category/apex/performance.xml - Performance rules
  • category/apex/codestyle.xml - Code style rules
  • category/apex/errorprone.xml - Errorprone patterns

Regex Rules

Some rules are implemented using the Regex engine for pattern-based matching. These rules are defined in the repository's code-analyzer.yml under engines.regex.custom_rules.

To use Regex rules in your project:

Copy the engines.regex.custom_rules section from the repository's code-analyzer.yml into your own code-analyzer.yml file. Do NOT copy the entire code-analyzer.yml file, as the repository version has rulesets: [] (disabled) and is only meant as a reference for Regex rules.

Example - Add to your code-analyzer.yml:

engines:
    pmd:
        custom_rulesets:
            - rulesets/design/InnerClassesCannotBeStatic.xml
            # ... other rulesets ...
    regex:
        custom_rules:
            NoConsecutiveBlankLines:
                regex: /\n\s*\n\s*\n/g
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prevents two or more consecutive blank lines in Apex code.
                    Code should have at most one blank line between statements,
                    methods, or other code elements.'
                violation_message:
                    'Two or more consecutive blank lines are not allowed. Use at
                    most one blank line between statements.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']
            ProhibitSuppressWarnings:
                regex: /@SuppressWarnings\([^)]*\)|\/\/\s*NOPMD/gi
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prohibits the use of @SuppressWarnings annotations and
                    NOPMD comments in Apex code. Suppressions hide code quality
                    issues; prefer fixing the underlying problems or improving
                    rules instead.'
                violation_message:
                    'Suppression of warnings is not allowed. Fix the underlying
                    issue or improve the rule instead of suppressing violations.'
                severity: 'High'
                tags: ['CodeStyle', 'Recommended']
            NoLongLines:
                regex: /.{81,}/gm
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Enforces a maximum line length of 80 characters. Lines
                    longer than 80 characters reduce readability and make code
                    harder to review. Use shorter class, attribute, method, and
                    variable names to improve readability.'
                violation_message:
                    'Line exceeds 80 characters. Use shorter class, attribute,
                    method, and variable names to improve readability.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']
            ProhibitPrettierIgnore:
                regex: /\/\/\s*prettier-ignore/gi
                file_extensions: ['.apex', '.cls', '.trigger']
                description:
                    'Prohibits the use of prettier-ignore comments in Apex code.
                    Code should be formatted consistently without exceptions.
                    Using prettier-ignore comments undermines code formatting
                    standards and makes the codebase less maintainable.'
                violation_message:
                    'Prettier-ignore comments are not allowed. Code should be
                    formatted consistently without exceptions.'
                severity: 'Moderate'
                tags: ['CodeStyle', 'Recommended']

Regex rules are useful for:

  • Pattern-based text matching (e.g., consecutive blank lines)
  • Finding patterns in comments (PMD ignores comments)
  • Simple string/pattern detection

For more information on creating Regex rules, see the Code Analyzer Configuration Regex Engine section.

Minimal Example

Here's a minimal code-analyzer.yml example showing how to enable a few rules and customize their severity:

engines:
    pmd:
        custom_rulesets:
            # Design rules
            - rulesets/design/InnerClassesCannotBeStatic.xml
            - rulesets/design/InnerClassesCannotHaveStaticMembers.xml

            # Best practices rules
            - rulesets/bestpractices/FinalVariablesMustBeFinal.xml

            # Code style rules
            - rulesets/codestyle/NoSingleLetterVariableNames.xml
            - rulesets/codestyle/NoMethodCallsInConditionals.xml

            # Documentation rules
            - rulesets/documentation/ExceptionDocumentationRequired.xml
            - rulesets/documentation/ValidGroupTagValues.xml
            - rulesets/documentation/ProhibitAuthorSinceVersionTags.xml
            - rulesets/documentation/SingleLineDocumentationFormat.xml
            - rulesets/documentation/MethodsRequireExampleTag.xml

rules:
    # Override severity and tags
    NoSingleLetterVariableNames:
        severity: 'High'
        tags: ['Recommended', 'Naming']

For a comprehensive example with all 48 PMD rules and 4 Regex rules, see the Comprehensive Example (All Rules) section above.

For more examples: See the unhappy-soup ruleset for a comprehensive example of combining standard PMD rules with custom rules and configuration.

To customize rule behavior: See the Customizing Rules section above for instructions on customizing rules by editing configurable variables at the top of XPath expressions.

Quick Start

  1. Copy rulesets to your project

    • Copy the rulesets/ directory (or specific category folders) to your Salesforce project root
    • Rules are organized by category (see Rule Categories below)
  2. Configure code-analyzer.yml

    • Create or update code-analyzer.yml in your project root
    • Add rulesets and disable default rules as needed
    • See Configuring code-analyzer.yml for detailed instructions
  3. Run the analyzer

    • Use the command line: sf code-analyzer run
    • Or use the VS Code extension for real-time analysis
    • See Usage for details

What Makes a Good Rule vs. What Prettier Handles

Important: PMD rules should focus on code quality, logic, and best practices, not formatting. Formatting concerns should be handled by Prettier or similar formatters.

Good PMD Rules Focus On:

  1. Code Quality & Logic

    • Detecting anti-patterns and code smells
    • Enforcing best practices and design patterns
    • Identifying potential bugs or logic errors
    • Examples:
      • NoMethodCallsInConditionals - Prevents side effects in conditionals
      • InnerClassesCannotBeStatic - Enforces Apex language constraints
  2. Structural Issues

    • Code organization and architecture
    • Class and method structure
    • Inheritance and composition patterns
    • Examples:
      • ClassesMustHaveMethods - Ensures classes have purpose
      • AvoidTrivialPropertyGetters - Prevents unnecessary wrappers
      • NoParameterClasses - Enforces proper class design
  3. Naming & Conventions

    • Meaningful names that affect code readability and maintainability
    • Naming patterns that indicate intent
    • Examples:
      • NoSingleLetterVariableNames - Ensures descriptive variable names
      • VariablesMustNotShareNamesWithClasses - Prevents confusion
  4. Modifiers & Access Control

    • Appropriate use of modifiers (final, static, etc.)
    • Access control best practices
    • Examples:
      • FinalVariablesMustBeFinal - Detects method-local variables that are never reassigned and should be declared as final
      • StaticMethodsMustBeStatic - Prevents unnecessary instance methods
      • RegexPatternsMustBeStaticFinal - Ensures regex patterns are static final constants
      • TestClassIsParallel - Enforces test best practices
  5. Documentation Quality

    • Meaningful documentation, not formatting
    • Documentation completeness
    • Examples:
      • ExceptionDocumentationRequired - Ensures exceptions are documented
      • ValidGroupTagValues - Ensures @group tags use valid values
      • ProhibitAuthorSinceVersionTags - Prohibits @author, @since, and @version tags
      • MethodsRequireExampleTag - Requires methods to have @example tags in ApexDoc

What Prettier Handles (Not PMD Rules):

  1. Formatting & Style

    • Indentation and spacing
    • Line breaks and line length
    • Brace placement
    • Quote style (single vs. double)
    • Trailing commas
    • Examples of what NOT to create rules for:
      • ❌ "Methods must be on separate lines" (formatting)
      • ❌ "Indentation must be 4 spaces" (formatting)
      • ❌ "No trailing whitespace" (formatting)
      • ❌ "Line length must be < 120 characters" (formatting)
  2. Whitespace & Spacing

    • Spaces around operators
    • Spaces in function calls
    • Blank lines between methods
    • Examples:
      • ❌ "No consecutive blank lines" (formatting - though this could be a code quality rule if it's about readability)
      • ❌ "Spaces around operators" (formatting)
  3. Syntax Formatting

    • Semicolon placement
    • Comma placement
    • Parentheses spacing
    • Examples:
      • ❌ "Always use semicolons" (formatting)
      • ❌ "No space before opening parenthesis" (formatting)

When to Create a Rule vs. Use Prettier:

Create a PMD Rule When:

  • The issue affects code quality, maintainability, or correctness
  • The check requires understanding code semantics (not just syntax)
  • The rule helps prevent bugs or enforces architectural decisions
  • The rule is about "what" the code does, not "how" it looks

Use Prettier When:

  • The issue is purely about code appearance
  • The check is about spacing, indentation, or line breaks
  • The rule would just reformat code without changing meaning
  • The rule is about "how" the code looks, not "what" it does

Examples:

Good Rule (PMD):

// Rule: NoMethodCallsInConditionals
// Why: Prevents side effects and makes code more predictable
if (getValue() > 0) {  // ❌ Method call in conditional
    // ...
}

Formatting (Prettier):

// Prettier handles: Indentation, spacing, line breaks
if(getValue()>0){  // Prettier formats to:
if (getValue() > 0) {  // Proper spacing

Good Rule (PMD):

// Rule: NoSingleLetterVariableNames
// Why: Ensures code is readable and maintainable
Integer x = 5;  // ❌ Unclear what 'x' represents

Formatting (Prettier):

// Prettier handles: Spacing around operators
Integer x=5;  // Prettier formats to:
Integer x = 5;  // Proper spacing

Note: Some rules may appear to overlap with formatting (e.g., NoConsecutiveBlankLines), but if they're about code readability and structure rather than pure formatting, they can be appropriate rules. NoConsecutiveBlankLines is implemented as a Regex rule for efficient pattern matching. The key distinction is: Does this affect code quality and understanding, or just appearance?

Installation

  1. Clone or download the repository

    git clone https://github.com/starch-uk/sca-extra.git
  2. Copy rulesets to your Salesforce project

    • Copy the rulesets/ directory to your Salesforce project root
    • Or copy specific category folders (e.g., rulesets/design/) as needed
    • Maintain the directory structure for organization
  3. Reference rulesets in your project

    • Rulesets can be referenced by relative path from your project root
    • Example: rulesets/design/InnerClassesCannotBeStatic.xml

Usage

Running Code Analyzer

Once you've configured your code-analyzer.yml file (see Configuring code-analyzer.yml above), you can run Salesforce Code Analyzer in two ways:

1. Command Line:

# Install the plugin (once per environment)
sf plugins install code-analyzer

# Run the analyzer from your project root
sf code-analyzer run

2. VS Code Extension:

  • Install the Salesforce Code Analyzer extension in VS Code
  • Open your Salesforce project with code-analyzer.yml and the rulesets/ folder present
  • The extension will automatically analyze your code and surface issues in the editor

For detailed configuration instructions, including how to add rules, customize rules, disable default rules, and use Regex rules, see the Configuring code-analyzer.yml section above.

Rules Documentation

Design Rules

InnerClassesCannotBeStatic

Priority: P1 (Critical)
Description: Inner classes in Apex cannot be static. Remove the static modifier from inner class declarations.

Violations:

public class Outer {
    public static class Inner {  // ❌ Static inner class not allowed
    }
}

Valid Code:

public class Outer {
    public class Inner {  // ✅ Non-static inner class
    }
}

InnerClassesCannotHaveStaticMembers

Priority: P1 (Critical)
Description: Inner classes in Apex cannot have static attributes or methods. Remove static modifiers from inner class members.

Violations:

public class Outer {
    public class Inner {
        public static Integer value;  // ❌ Static field not allowed
        public static void method() { }  // ❌ Static method not allowed
    }
}

Valid Code:

public class Outer {
    public class Inner {
        public Integer value;  // ✅ Non-static field
        public void method() { }  // ✅ Non-static method
    }
}

NoInterfacesEndingWithCallback

Priority: P3 (Medium)
Description: Interfaces should not be named with the suffix 'Callback'. Callbacks are an anti-pattern that should be avoided in favor of better design patterns such as event-driven architectures or observer patterns. Callbacks are often used to hide circular dependencies without fixing them, create tight coupling, make code harder to test, and reduce maintainability.

Violations:

public interface PaymentCallback {  // ❌ Interface ending with 'Callback'
    void onSuccess();
    void onError(String error);
}

Valid Code:

public interface PaymentHandler {  // ✅ Descriptive name without 'Callback'
    void onSuccess();
    void onError(String error);
}

public interface EventListener {  // ✅ Alternative pattern
    void handleEvent(String event);
}

DmlInLoopAcrossMethods (thanks to @SamanAttar for the idea on this)

Priority: P1 (Critical)
Description: DML operations should not be performed inside loops, even when the DML operation is in a separate method called from the loop. This pattern leads to governor limit issues and poor performance. Collect records in a list and perform bulk DML operations outside the loop instead.

This rule detects when a for loop or for-each loop calls a method that performs DML operations (insert, update, delete, upsert, merge, or Database class methods). The rule flags the loop that calls the method, not the method that performs the DML.

Violations:

// Violation: Loop calls method that performs DML
public void processAccounts(List<Account> accounts) {
    for (Account acc : accounts) {
        saveAccount(acc);  // ❌ Method performs insert
    }
}

private void saveAccount(Account acc) {
    insert acc;  // DML operation
}

// Violation: Loop calls method that performs DML via Database class
public void updateContacts(List<Contact> contacts) {
    for (Contact con : contacts) {
        updateContact(con);  // ❌ Method performs Database.update
    }
}

private void updateContact(Contact con) {
    Database.update(con);  // DML via Database class
}

Valid Code:

// Valid: Collect records and perform bulk DML outside loop
public void processAccounts(List<Account> accounts) {
    List<Account> accountsToInsert = new List<Account>();
    for (Account acc : accounts) {
        // Process account without DML
        acc.Name = 'Processed';
        accountsToInsert.add(acc);
    }
    insert accountsToInsert;  // ✅ Bulk DML outside loop
}

// Valid: Method called from loop does not perform DML
public void processAccounts(List<Account> accounts) {
    for (Account acc : accounts) {
        validateAccount(acc);  // ✅ No DML in this method
    }
}

private void validateAccount(Account acc) {
    if (acc.Name == null) {
        acc.Name = 'Unknown';
    }
}

UnusedVariableNotReturnedOrPassed (thanks to Marlene Guerra-Reeve for the idea on this)

Priority: P3 (Medium)
Description: Detects variables that are assigned but never returned or passed to a method or constructor. Variables should either be used in return statements or passed as arguments to methods or constructors. This helps identify dead code and unused variables that can be removed.

This rule flags variables that are assigned but not used in:

  • Return statements
  • Method call arguments
  • Constructor arguments (both positional and named parameters)

Variables that are reassigned, incremented, or decremented after being used are still flagged if the final value is not returned or passed.

Violations:

// Violation: Variable assigned but never used
public void method1() {
    String unused = 'test';  // ❌ Variable never returned or passed
}

// Violation: Variable assigned after being used but not returned or passed
public void method2() {
    String value = 'test';
    process(value);
    value = 'test2';  // ❌ Variable reassigned but not returned or passed
}

// Violation: Variable incremented but not returned or passed
public void method3() {
    Integer value = 0;
    process(value);
    value++;  // ❌ Variable incremented but not returned or passed
}

// Violation: Variable used in constructor then reassigned but not returned or passed
public void method4() {
    String name = 'test';
    Account acc = new Account(Name = name);
    name = 'test2';  // ❌ Variable reassigned but not returned or passed
}

Valid Code:

// Valid: Variable returned
public String method1() {
    String result = 'test';
    return result;  // ✅ Variable is returned
}

// Valid: Variable passed to method
public void method2() {
    String value = 'test';
    process(value);  // ✅ Variable is passed to method
}

// Valid: Variable passed to constructor with named parameters
public void method3() {
    String name = 'Test';
    Account acc = new Account(Name = name);  // ✅ Variable is passed to constructor
    process(acc);
}

// Valid: Variable passed to constructor with positional parameters
public Account method4() {
    String name = 'Test';
    Account acc = new Account(name);  // ✅ Variable is passed to constructor
    return acc;  // ✅ Result is returned
}

// Valid: Variable used in multiple ways
public void method5() {
    String value = 'test';
    process(value);
    log(value);  // ✅ Variable is passed to multiple methods
}

// Valid: Variable returned after being used
public String method6() {
    String result = 'test';
    process(result);
    return result;  // ✅ Variable is returned after being used
}

Code Style Rules (Naming)

NoSingleLetterVariableNames

Priority: P2 (High)
Description: Single-letter variable names are not allowed except for loop counters (i, c) or exception variables (e).

Violations:

Integer x = 5;  // ❌ Single-letter variable name
String s = 'test';  // ❌ Single-letter variable name

Valid Code:

Integer index = 5;  // ✅ Descriptive name
for (Integer i = 0; i < 10; i++) { }  // ✅ Loop counter allowed
catch (Exception e) { }  // ✅ Exception variable allowed

Code Style Rules

NoMethodCallsInConditionals

Priority: P2 (High)
Description: Method calls should not be used in conditional expressions to prevent side effects and make code more predictable.

Violations:

if (getValue() > 0) {  // ❌ Method call in conditional
    // ...
}

Valid Code:

Integer value = getValue();  // ✅ Extract to variable
if (value > 0) {
    // ...
}

PreferMethodCallsInLoopConditions

Priority: P3 (Medium)
Description: Method calls should be used directly in loop conditionals rather than testing inside the loop with a break statement. This makes the loop condition explicit and improves readability. Applies to both while and do-while loops.

Violations:

// while loop with break inside
while (true) {  // ❌ Should use method call in condition
    if (!hasMore()) {
        break;
    }
    processItem();
}

// do-while loop with break inside
do {  // ❌ Should use method call in condition
    if (!hasMore()) {
        break;
    }
    processItem();
} while (true);

Valid Code:

// Method call in while loop condition
while (hasMore()) {  // ✅ Method call in condition
    processItem();
}

// Method call in do-while loop condition
do {  // ✅ Method call in condition
    processItem();
} while (hasMore());

// Break without method call (not flagged)
while (true) {
    if (flag) {  // ✅ No method call, so not flagged
        break;
    }
    process();
}

PreferNullCoalescingOverTernary

Priority: P3 (Medium)
Description: Prefer the null coalescing operator (??) over ternary operators when checking for null and providing a default value. The null coalescing operator is more concise and clearly expresses the intent of providing a default value when a variable is null.

The null coalescing operator (??) returns its left-hand operand if it's not null, otherwise returns the right-hand operand. It's a binary operator that's left-associative. The left-hand operand is evaluated only once, and the right-hand operand is only evaluated if the left-hand operand is null.

This rule flags ternary expressions that check for null using == or != operators and follow patterns that can be converted to the null coalescing operator, where the same variable or expression appears in both the condition and one of the branches.

Patterns where the branches contain different variables than the one being checked (e.g., x != null ? y : z where y differs from x) cannot be converted to null coalescing, as they represent different logic. This rule also does not flag ternary expressions that check conditions other than null.

When converting ternary operators to null coalescing, you may also need to use the safe navigation operator (?.) to safely access properties or methods when the left operand might be null.

Both operands must be of compatible types. The null coalescing operator cannot be used as the left-hand side of an assignment, and SOQL bind expressions don't support the null coalescing operator.

Violations:

// Basic variable null check
String result = data != null ? data : 'default';  // ❌ Should use ??
Integer value = num == null ? 0 : num;            // ❌ Should use ??

// Method call with null check
String value = obj != null ? obj.getName() : 'default';  // ❌ Should use ?. and ??

// Property access with null check
String name = account == null ? 'Unknown' : account.Name;  // ❌ Should use ?. and ??

// Null on left side of comparison
String result = null == data ? 'default' : data;  // ❌ Should use ??

// SOQL query with null check
Account defaultAccount = new Account(Name = 'Default');
Account a = [SELECT Id FROM Account WHERE Id = :accountId] != null
    ? [SELECT Id FROM Account WHERE Id = :accountId]
    : defaultAccount;  // ❌ Should use ??

Valid Code:

// Basic variable null coalescing
String result = data ?? 'default';  // ✅ Uses null coalescing operator
Integer value = num ?? 0;           // ✅ Uses null coalescing operator

// Method call with safe navigation and null coalescing
String value = obj?.getName() ?? 'default';  // ✅ Uses ?. and ??

// Property access with safe navigation and null coalescing
String name = account?.Name ?? 'Unknown';  // ✅ Uses ?. and ??

// Chained null coalescing operators (left-associative)
String city = account?.BillingCity ?? account?.ShippingCity ?? 'N/A';  // ✅ Chained ??

// SOQL query with null coalescing
Account defaultAccount = new Account(Name = 'Default');
Account a = [SELECT Id FROM Account WHERE Id = :accountId] ?? defaultAccount;  // ✅ Uses ??

// Ternary for non-null checks (not flagged by this rule)
String result = status == 'active' ? 'yes' : 'no';  // ✅ Not a null check
Integer value = count > 0 ? count : 1;              // ✅ Not a null check

AvoidMagicNumbers (thanks to @SamanAttar for the idea on this)

Priority: P3 (Medium)
Description: Magic numbers are hardcoded numeric literals that should be replaced with named constants for better readability and maintainability. This rule flags numeric literals that are not common "safe" numbers (like 0, 1, -1) and are not used in array indices or loop conditions.

Test classes annotated with @IsTest are exempt from this rule when SeeAllData is false (the default) or explicitly set to false, as they often need to use hardcoded values for test data setup and assertions.

Violations:

// Magic numbers in calculations
Integer total = price * 1.15;  // ❌ What is 1.15?
Integer timeout = 30000;  // ❌ What is 30000?
Decimal discount = amount * 0.10;  // ❌ What is 0.10?

// Magic numbers in method calls
setMaxRetries(3);  // ❌ What is 3?
setBatchSize(200);  // ❌ What is 200?

// Magic numbers in assignments
Integer maxRetries = 3;  // ❌ Should use a constant
Integer batchSize = 200;  // ❌ Should use a constant

// Magic numbers in return statements
public Integer getDefaultTimeout() {
    return 5000;  // ❌ What is 5000?
}

// Magic numbers in conditions (not safe numbers)
if (count > 5) { }  // ❌ 5 is not a safe number
if (size >= 10) { }  // ❌ 10 is not a safe number

Valid Code:

// Use named constants
private static final Decimal TAX_RATE = 1.15;
private static final Integer TIMEOUT_MS = 30000;
private static final Decimal DISCOUNT_PERCENTAGE = 0.10;
private static final Integer MAX_RETRIES = 3;
private static final Integer BATCH_SIZE = 200;
private static final Integer DEFAULT_TIMEOUT = 5000;

Integer total = price * TAX_RATE;  // ✅ Uses constant
Integer timeout = TIMEOUT_MS;  // ✅ Uses constant
Decimal discount = amount * DISCOUNT_PERCENTAGE;  // ✅ Uses constant

// Safe numbers (0, 1, -1) are allowed
Integer count = 0;  // ✅ Safe number
Integer index = 1;  // ✅ Safe number
Integer offset = -1;  // ✅ Safe number

// Array/list indices are allowed
List<String> items = new List<String>();
String first = items[0];  // ✅ Array index
String second = items[1];  // ✅ Array index

// Loop conditions are allowed
for (Integer i = 0; i < 10; i++) { }  // ✅ Loop condition
while (count < 100) { }  // ✅ Loop condition

// Comparisons with safe numbers (0, 1, -1) are allowed
if (count == 0) { }  // ✅ Safe number comparison
if (size > 1) { }  // ✅ Safe number comparison
if (result != -1) { }  // ✅ Safe number comparison

// Test classes with @IsTest are exempt (SeeAllData defaults to false)
@IsTest
private class TestClassDefault {
    static void testSomeMethod() {
        Integer batchSize = 200;  // ✅ Allowed in test classes
        Decimal taxRate = 0.08;   // ✅ Allowed in test classes
        Account acc = new Account(AnnualRevenue = 50000); // ✅ Allowed
        System.assertEquals(3, records.size()); // ✅ Allowed
    }
}

// Test classes with @IsTest(SeeAllData=false) are exempt
@IsTest(SeeAllData=false)
private class TestClassExplicitFalse {
    static void testSomeMethod() {
        Integer timeout = 30000;  // ✅ Allowed when SeeAllData=false
        Decimal discount = amount * 0.10;  // ✅ Allowed when SeeAllData=false
    }
}

Best Practices Rules

RegexPatternsMustBeStaticFinal

Priority: P2 (High)
Description: Regular expression patterns must be declared as static final constants instead of inline. This improves performance by avoiding repeated pattern compilation, makes patterns reusable, and improves maintainability by centralizing pattern definitions.

This rule applies to:

  • Pattern.compile() and Pattern.matches() static methods
  • String.split(), String.matches(), String.replaceAll(), and String.replaceFirst() instance methods

For String replace operations, consider using the Matcher class with a static final Pattern instead of String.replaceAll() or String.replaceFirst() for better performance when the same pattern is used multiple times.

Violations:

// Pattern.compile() with inline regex pattern
Pattern pattern = Pattern.compile('^[A-Z]+$');  // ❌ Inline pattern

// Pattern.matches() with inline regex pattern
Boolean isValid = Pattern.matches('^[A-Z]+$', input);  // ❌ Inline pattern

// String.replaceAll() with inline regex pattern
String replaced = input.replaceAll('\\d', 'X');  // ❌ Inline pattern

// String.replaceFirst() with inline regex pattern
String replaced = input.replaceFirst('\\d', 'X');  // ❌ Inline pattern

// String.split() with inline regex pattern
List<String> parts = input.split(',');  // ❌ Inline pattern

// String.matches() with inline regex pattern
Boolean matches = input.matches('test.*');  // ❌ Inline pattern

Valid Code:

// Static final regex pattern with Pattern.matches()
private static final String PATTERN = '^[A-Z]+$';
public Boolean isValid(String input) {
    return Pattern.matches(PATTERN, input);  // ✅ Uses static final constant
}

// Static final regex pattern with Pattern.compile()
private static final String EMAIL_PATTERN = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
public Boolean isValidEmail(String input) {
    Pattern pattern = Pattern.compile(EMAIL_PATTERN);  // ✅ Uses static final constant
    return pattern.matcher(input).matches();
}

// Using Matcher class with static final Pattern for replace operations
private static final Pattern DIGIT_PATTERN = Pattern.compile('\\d');
public String replaceDigits(String input) {
    Matcher m = DIGIT_PATTERN.matcher(input);  // ✅ Uses static final Pattern
    return m.replaceAll('X');
}

// Pattern from method/constructor argument (exception)
public void processPattern(String pattern) {
    Pattern p = Pattern.compile(pattern);  // ✅ Pattern from parameter - allowed
}

// Pattern from method output (exception)
public void useDynamicPattern() {
    Pattern p = Pattern.compile(getPattern());  // ✅ Pattern from method - allowed
}

Modifier Rules

FinalVariablesMustBeFinal

Priority: P3 (Moderate)
Description: Method-local variables that are never reassigned must be declared as final to enforce immutability and improve code clarity.

Violations:

public void method() {
    Integer value = 5;  // ❌ Should be final (never reassigned)
    String name = 'test';  // ❌ Should be final (never reassigned)
    Boolean isValid = true;  // ❌ Should be final (never reassigned)
    // value, name, and isValid are never reassigned
}

Valid Code:

public void method() {
    final Integer value = 5;  // ✅ Declared as final
    final String name = 'test';  // ✅ Declared as final

    Integer counter = 0;
    counter++;  // ✅ Not final (incremented)

    Integer result = 10;
    result = result + 5;  // ✅ Not final (reassigned)

    Integer sum = 0;
    sum += 5;  // ✅ Not final (compound assignment)
}

// Loop variables and parameters are excluded
public void method(Integer param) {
    for (Integer i = 0; i < 10; i++) {
        // Loop variable 'i' is excluded
    }
    // Parameter 'param' is excluded
}

StaticMethodsMustBeStatic

Priority: P2 (High)
Description: Methods that don't use instance state should be declared as static.

Violations:

public class Utils {
    public Integer add(Integer a, Integer b) {  // ❌ Should be static
        return a + b;
    }
}

Valid Code:

public class Utils {
    public static Integer add(Integer a, Integer b) {  // ✅ Static method
        return a + b;
    }
}

Documentation Rules

MethodsRequireExampleTag

Priority: P3 (Medium)
Version: 1.0.2
Source: rulesets/documentation/MethodsRequireExampleTag.xml
Description: Methods must have at least one @example tag in their ApexDoc comments containing a properly formatted {@code} block (with both opening { and closing }), unless they have annotations, are override methods, are interface method declarations, are constructors, or return void and take no arguments. This helps developers understand how to use methods correctly. According to the Salesforce ApexDoc format specification, @example tags must contain {@code} blocks to properly format code examples.

Violations:

/**
 * Processes the account.
 * @param acc The account to process
 * @return The processed account
 */
public Account processAccount(Account acc) {  // ❌ Missing @example tag
    return acc;
}

/**
 * Processes the account.
 * @param acc The account to process
 * @example
 * Account acc = new Account(Name = 'Test');
 */
public Account processAccount(Account acc) {  // ❌ @example without {@code} block
    return acc;
}

/**
 * Processes the account.
 * @param acc The account to process
 * @example
 * @code
 * Account acc = new Account(Name = 'Test');
 */
public Account processAccount(Account acc) {  // ❌ @code without curly braces (should be {@code})
    return acc;
}

/**
 * Processes the account.
 * @param acc The account to process
 * @example
 * {@code
 * Account acc = new Account(Name = 'Test');
 */
public Account processAccount(Account acc) {  // ❌ {@code without closing }
    return acc;
}

Valid Code:

/**
 * Processes the account.
 * @param acc The account to process
 * @return The processed account
 * @example
 * {@code
 * Account acc = new Account(Name = 'Test');
 * Account result = processor.processAccount(acc);
 * }
 */
public Account processAccount(Account acc) {  // ✅ Has @example with {@code} block
    return acc;
}

// Valid: Method with annotation (exempt)
/**
 * Test method for account processing.
 */
@IsTest
public static void testProcessAccount() {  // ✅ Annotated method exempt
    // Test code
}

// Valid: Override method (exempt)
/**
 * String representation.
 */
@Override
public String toString() {  // ✅ Override method exempt
    return 'Example';
}

// Valid: Interface method declaration (exempt)
public interface MyInterface {
    /**
     * Interface method without @example.
     */
    void doSomething();  // ✅ Interface method exempt
}

// Valid: Constructor (exempt)
/**
 * Creates a new instance.
 */
public MyClass() {  // ✅ Constructor exempt
    // Constructor implementation
}

// Valid: Void method with no parameters (exempt)
/**
 * Initializes the instance.
 */
public void initialize() {  // ✅ Void method with no parameters exempt
    // Method implementation
}

Rule Categories

Rules are organized into PMD's 8 standard categories (consistent across languages):

  • bestpractices/ - Generally accepted best practices (5 PMD rules: modifier rules, test class rules)
  • codestyle/ - Coding style enforcement (21 PMD rules: formatting, naming conventions, code style patterns)
  • design/ - Design issue detection (18 PMD rules: code structure, method signatures, class organization)
  • documentation/ - Code documentation rules (5 PMD rules)
  • errorprone/ - Broken/confusing/runtime-error-prone constructs (currently empty)
  • multithreading/ - Multi-threaded execution issues (currently empty)
  • performance/ - Suboptimal code detection (currently empty)
  • security/ - Potential security flaws (currently empty)

Total: 48 PMD rules + 4 Regex rules = 52 rules

In addition to the PMD rules above, 4 Regex rules are provided:

  • NoConsecutiveBlankLines - Prevents consecutive blank lines
  • ProhibitPrettierIgnore - Prohibits prettier-ignore comments
  • ProhibitSuppressWarnings - Prohibits suppression annotations and comments
  • NoLongLines - Enforces maximum line length of 80 characters

For a complete list of all rules, see the rulesets/ directory. Each rule XML file contains detailed descriptions and XPath expressions. Regex rules are defined in code-analyzer.yml under engines.regex.custom_rules.

Development

Prerequisites

  • Node.js 18+
  • pnpm

Setup

pnpm install

Running Tests

pnpm test          # Run all tests
pnpm test:watch    # Run tests in watch mode
pnpm test:coverage # Run tests with coverage

For Jest API reference, see Jest Reference.

Formatting

pnpm format        # Format all files
pnpm format:check  # Check formatting without modifying

Linting

pnpm lint      # Lint JavaScript files
pnpm lint:fix  # Fix linting issues automatically

Validation

pnpm validate        # Validate all rulesets
pnpm check-xml-order # Check XML element order

XML Element Order

PMD ruleset XML files must follow the PMD Ruleset XML Schema, which requires elements within <rule> to be in a specific order:

  1. <description> (optional)
  2. <priority> (optional)
  3. <properties> (optional)
  4. <exclude> (optional, can appear multiple times)
  5. <example> (optional, can appear multiple times)

Scripts available:

  • pnpm check-xml-order - Check if all XML files have correct element order
  • pnpm fix-xml-order - Automatically fix element order in all XML files
  • pnpm add-version-info - Add version information to all rule descriptions

These scripts use XML libraries (@xmldom/xmldom) to properly parse and manipulate XML, ensuring all elements (including multiple examples) are preserved.

Benchmarking

pnpm benchmark                    # Run performance benchmarks
pnpm benchmark -- --baseline     # Generate baseline for regression detection
pnpm benchmark -- --json         # JSON output for CI/CD integration
pnpm benchmark -- --compare      # Compare mode (doesn't fail on regressions)
pnpm check-regressions           # Check for performance regressions

Results are saved to benchmarks/results/:

  • results-{timestamp}.json - Latest benchmark results
  • baseline.json - Baseline for regression comparison

Available Scripts

All scripts in the scripts/ directory have convenience commands in package.json:

Development:

  • pnpm test - Run all tests
  • pnpm test:watch - Run tests in watch mode
  • pnpm test:coverage - Run tests with coverage
  • pnpm validate - Validate all rulesets
  • pnpm format - Format all files with Prettier
  • pnpm format:check - Check formatting without modifying
  • pnpm lint - Lint JavaScript files
  • pnpm lint:fix - Fix linting issues automatically

XML Management:

  • pnpm check-xml-order - Check XML element order in all ruleset files
  • pnpm fix-xml-order - Automatically fix XML element order
  • pnpm add-version-info - Add version information to all rule descriptions

Testing & Utilities:

  • pnpm list-test-files - List all test files to verify Jest discovery
  • pnpm generate-test-ruleset - Generate test ruleset for validation
  • pnpm ast-dump - Dump PMD AST for Apex files (usage: pnpm ast-dump <file>)

Performance:

  • pnpm benchmark - Run performance benchmarks
  • pnpm check-regressions - Check for performance regressions

Project Management:

  • pnpm version:bump - Bump package.json version number
  • pnpm bump-rule-versions - Automatically bump rule versions based on changes:
    • Major bump: Rules with failing existing tests (tests that existed at HEAD)
    • Minor bump: Rules with new tests
    • Patch bump: Other changed rules
  • pnpm changelog - Generate changelog
  • pnpm clean - Clean build artifacts and temporary files

CI/CD:

  • pnpm ci - Run all CI checks (format, lint, test)

Script Security

All scripts in the scripts/ directory implement security best practices:

File System Operations

  • File Descriptors: Scripts use file descriptors (fs.openSync, fs.writeFileSync with file descriptor) instead of file paths to prevent time-of-check to time-of-use (TOCTOU) race conditions
  • No Existence Checks: Scripts never use fs.existsSync() before opening files - files are opened directly and errors are handled if they don't exist
  • Atomic Operations: File operations are atomic with respect to file descriptors, preventing race conditions

Shell Command Execution

  • execFileSync: Scripts use execFileSync instead of execSync when passing dynamic paths or arguments to prevent shell command injection vulnerabilities
  • Separate Arguments: Commands and arguments are passed separately (e.g., execFileSync('git', ['show', path], {...}))

Path Validation (for user input)

Scripts that accept file paths from user input implement additional security measures:

  • Path Sanitization: The sanitize-filename package is used to sanitize file paths, eliminating dangerous characters
  • Path Validation: Paths are validated to ensure they don't contain path traversal sequences (..) and are relative paths
  • Symbolic Link Resolution: fs.realpathSync() is used to resolve symbolic links and ensure files are within expected directories
  • Defense in Depth: Multiple validation layers ensure robust security

Scripts implementing these measures:

  • scripts/benchmark.js - File descriptor usage for read/write operations
  • scripts/bump-rule-versions.js - File descriptors and execFileSync for git operations
  • scripts/check-performance-regressions.js - Path validation for user input
  • scripts/ast-dump.sh - Path validation for user input

For more details, see SECURITY.md and CONTRIBUTING.md.

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines on:

  • Development setup
  • Adding new rules
  • Testing requirements
  • Pull request process
  • Code style guidelines

Please note that this project follows the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.

Security

For security vulnerabilities, please see SECURITY.md for reporting instructions.

Documentation

Core PMD & Rule Development

Documentation & Suppression

  • ApexDoc Reference - ApexDoc syntax, tags, and documentation format for Apex code
  • PMD Quick Reference - How to suppress PMD rule violations using annotations, comments, and rule properties (see Suppressing Warnings section)

Testing & Development

  • Jest Reference - Jest 30.0 API reference for writing and running tests
  • pnpm Reference - pnpm package manager reference for dependency management and workspace configuration
  • Husky Reference - Husky Git hooks manager reference for setting up pre-commit and other Git hooks

Additional Code Analyzer Engines

  • PMD Quick Reference - Copy/Paste Detector (CPD) engine for duplicate code detection across multiple languages (see CPD section)
  • ESLint Reference - ESLint engine configuration for JavaScript, TypeScript, and LWC static analysis

Graph Engine Documentation

AI Agent Configuration

The Code Analyzer Configuration and PMD Quick Reference provide machine-readable reference for AI coding assistants (like Cursor's Agent) to help developers configure PMD rules. This guide contains structured information about each rule including violations, valid code examples, and configuration properties.

Required Documentation Files

When setting up AI agent rules, you should reference these documentation files:

Core PMD & Rule Development:

Documentation & Suppression:

  • ApexDoc Reference - ApexDoc syntax, tags, and documentation format for Apex code
  • PMD Quick Reference - How to suppress PMD rule violations using annotations, comments, and rule properties (see Suppressing Warnings section)

Testing & Development:

  • Jest Reference - Jest 30.0 API reference for writing and running tests
  • pnpm Reference - pnpm package manager reference for dependency management and workspace configuration
  • Husky Reference - Husky Git hooks manager reference for setting up pre-commit and other Git hooks

Additional Engines:

  • PMD Quick Reference - Copy/Paste Detector (CPD) engine for duplicate code detection (see CPD section)
  • ESLint Reference - ESLint engine configuration for JavaScript, TypeScript, and LWC

Graph Engine Documentation:

Setting Up in Cursor

To configure Cursor's Agent to use this guide, create a Cursor Project Rule:

  1. Create a Project Rule:

    • Open Cursor Settings → Rules, Commands
    • Click + Add Rule next to Project Rules
    • Or use the New Cursor Rule command
    • This creates a new rule folder in .cursor/rules
  2. Rule Structure: Create a folder like .cursor/rules/salesforce-pmd-guide/ containing RULE.md:

    ---
    description:
        'Guide for configuring PMD rules for Salesforce Apex code analysis.
        Reference @docs/CODEANALYZER.md and @docs/PMD.md when helping with
        code-analyzer.yml configuration or creating new rules.'
    alwaysApply: false
    ---
    
    When helping with Salesforce Code Analyzer configuration or creating PMD
    rules:
    
    - Reference @docs/CODEANALYZER.md for `code-analyzer.yml` configuration and
      engine settings (includes Regex engine configuration)
    - Reference @docs/XPATH31.md for XPath 3.1 syntax when writing rule queries
    - Reference @docs/PMD.md for PMD Apex AST node types and patterns (see Apex
      AST Reference section)
    - Reference @docs/PMD.md for suppressing rule violations when needed (see
      Suppressing Warnings section)
    - Use the structured format to provide accurate rule information
    - Include code examples from the guides
    - When configuring code-analyzer.yml, use property examples from the guides
    - Suggest appropriate property values based on the rule's purpose
    - Provide complete YAML examples when needed
  3. Rule Types:

    • Always Apply: Apply to every chat session
    • Apply Intelligently: When Agent decides it's relevant based on description
    • Apply to Specific Files: When file matches a specified pattern (use globs in frontmatter)
    • Apply Manually: When @-mentioned in chat (e.g., @salesforce-pmd-guide)

Cursor Rules Overview

Cursor supports four types of rules:

  • Project Rules: Stored in .cursor/rules, version-controlled and scoped to your codebase
  • User Rules: Global to your Cursor environment, used by Agent (Chat)
  • Team Rules: Team-wide rules managed from the dashboard (Team/Enterprise plans)
  • AGENTS.md: Agent instructions in markdown format, simple alternative to .cursor/rules

Best Practices

  • Keep rules focused and under 500 lines
  • Split large rules into multiple, composable rules
  • Provide concrete examples or reference files using @filename
  • Write rules like clear internal documentation
  • Use @filename syntax to include relevant files in rule context

For more information about Cursor rules, see the Cursor Rules Documentation.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

Support

About

Extra Apex rules for Salesforce Code Analyzer

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors