eslint-plugin-apex
v0.1.1
Published
ESLint plugin for Salesforce Apex — ports PMD Apex rules to the ESLint ecosystem
Maintainers
Readme
eslint-plugin-apex
ESLint plugin for Salesforce Apex — inspired by apex-pmd, this plugin ports 57 ESLint rules that align with the PMD Apex rule set into the ESLint ecosystem so that teams working with Salesforce can use a single linting tool for both Apex and Lightning Web Components (LWC) side by side.
If you already use eslint-plugin-lwc for your LWC code, adding eslint-plugin-apex brings your Apex codebase into the same workflow — one eslint.config.js, one npm run lint command, one CI step.
The plugin is backed by the ANTLR4-based @apexdevtools/apex-parser for accurate, whitespace-aware parsing rather than regex heuristics.
Status: Early release. Parser coverage is solid for classes and triggers; advanced patterns (anonymous Apex, complex generics) are progressively improved.
Features
- 57 rules across 7 categories (Best Practices, Code Style, Design, Documentation, Error Prone, Performance, Security), each with a Markdown page under
docs/rules/ - Custom ANTLR4-backed parser — no regex heuristics
- ESLint flat config API (v9+)
- Four ready-made shared configs:
recommended,strict,security,performance - PMD converter — paste a PMD XML rule file and get an ESLint config snippet in the browser
Installation
npm install --save-dev eslint-plugin-apexRequires ESLint v9+ and Node.js v18+.
Quick Start
// eslint.config.js
import apex from 'eslint-plugin-apex';
export default [apex.configs.recommended];Available Configs
| Config | Severity | Description |
| -------------------------- | ----------------------------------------------------- | ------------------------------ |
| apex.configs.recommended | errors for problem rules, warnings for suggestion | Recommended set of rules |
| apex.configs.strict | error for all | Every rule enabled as an error |
| apex.configs.security | error | Security rules only |
| apex.configs.performance | error | Performance rules only |
All configs automatically target **/*.cls, **/*.trigger, and **/*.apex files.
Manual Rule Configuration
// eslint.config.js
import apex from 'eslint-plugin-apex';
export default [
{
files: ['**/*.cls', '**/*.trigger', '**/*.apex'],
plugins: { apex },
languageOptions: apex.configs.recommended.languageOptions,
rules: {
'apex/security-no-soql-injection': 'error',
'apex/perf-no-dml-in-loop': 'error',
'apex/best-test-has-asserts': 'warn',
// disable a rule
'apex/doc-require-apexdoc': 'off',
},
},
];Rule Reference
Default reflects apex.configs.recommended: problem + recommended: true → error; suggestion + recommended: true → warn; recommended: false → off.
Best Practices
| Rule | Description | PMD equivalent | Default |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------- |
| apex/best-debug-use-logging-level | System.debug() calls should specify a LoggingLevel argument | DebugsShouldUseLoggingLevel | warn |
| apex/best-no-future-annotation | Prefer Queueable over @Future — the future annotation has significant limitations | AvoidFutureAnnotation | off |
| apex/best-no-global-modifier | Avoid 'global' class modifier — it permanently locks the public API in managed packages | AvoidGlobalModifier | warn |
| apex/best-no-logic-in-trigger | Avoid placing business logic directly in triggers — delegate to handler classes | AvoidLogicInTrigger | warn |
| apex/best-no-unused-local-variable | Detects local variables that are declared but never read | UnusedLocalVariable | off |
| apex/best-queueable-needs-finalizer | Queueable classes should attach a Finalizer for error recovery | QueueableWithoutFinalizer (+ legacy QueueableShouldAttachFinalizer) | off |
| apex/best-test-assertions-have-message | System.assert() calls should include a message parameter for clarity | ApexAssertionsShouldIncludeMessage | warn |
| apex/best-test-has-asserts | Apex unit test classes should include at least one assertion | ApexUnitTestClassShouldHaveAsserts | warn |
| apex/best-test-has-run-as | Test classes should include at least one System.runAs() call | ApexUnitTestClassShouldHaveRunAs | warn |
| apex/best-test-method-annotation | Use @IsTest annotation instead of the deprecated 'testMethod' keyword | ApexUnitTestMethodShouldHaveIsTestAnnotation | warn |
| apex/best-test-no-see-all-data | Avoid @isTest(seeAllData=true) as it exposes real org data to test modifications | ApexUnitTestShouldNotUseSeeAllDataTrue | error |
Code Style
| Rule | Description | PMD equivalent | Default |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| apex/style-annotation-naming | Annotation names should use PascalCase | AnnotationsNamingConventions | warn |
| apex/style-braces-for-for | Require braces around for loop bodies | ForLoopsMustUseBraces | warn |
| apex/style-braces-for-if | Require braces around if/else statement bodies | IfStmtsMustUseBraces / IfElseStmtsMustUseBraces | warn |
| apex/style-braces-for-while | Require braces around while loop bodies | WhileLoopsMustUseBraces | warn |
| apex/style-fields-at-start | Field declarations should appear before method declarations | FieldDeclarationsShouldBeAtStart | warn |
| apex/style-naming-conventions | Enforce configurable naming conventions for Apex declarations | ClassNamingConventions / MethodNamingConventions / FieldNamingConventions / LocalVariableNamingConventions / FormalParameterNamingConventions / PropertyNamingConventions | warn |
| apex/style-one-declaration-per-line | Declare only one variable per statement | OneDeclarationPerLine | warn |
Design
| Rule | Description | PMD equivalent | Default |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------- | ------- |
| apex/design-cognitive-complexity | Limit cognitive complexity of methods and classes | CognitiveComplexity | warn |
| apex/design-cyclomatic-complexity | Limit cyclomatic complexity of methods and classes | CyclomaticComplexity / StdCyclomaticComplexity | warn |
| apex/design-excessive-parameters | Flag methods with too many parameters | ExcessiveParameterList | warn |
| apex/design-excessive-public-count | Flag classes with too many public methods or attributes | ExcessivePublicCount | warn |
| apex/design-ncss-method-count | Limit the number of non-commenting source statements per method and class | NcssMethodCount / NcssCount / NcssTypeCount | warn |
| apex/design-no-boolean-parameters | Avoid boolean parameters in public and global methods | AvoidBooleanMethodParameters | warn |
| apex/design-no-deep-nesting | Avoid deeply nested if statements | AvoidDeeplyNestedIfStmts | warn |
| apex/design-no-unused-method | Detect private methods that are never called within the class | UnusedMethod | off |
| apex/design-too-many-fields | Flag classes with too many fields | TooManyFields | warn |
Documentation
| Rule | Description | PMD equivalent | Default |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------- | ------- |
| apex/doc-require-apexdoc | Require ApexDoc comments on public and global classes, methods, and properties | ApexDoc | off |
Error Prone
| Rule | Description | PMD equivalent | Default |
| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------- |
| apex/error-aura-enabled-getter-public | @AuraEnabled property getters must be public or global | InaccessibleAuraEnabledGetter (older docs sometimes mis-labeled this as AuraEnabledWithoutCatchBlock) | error |
| apex/error-no-csrf-in-constructor | Disallow DML operations in constructors or class initializers | ApexCSRF | error |
| apex/error-no-direct-trigger-map-access | Avoid direct index access to Trigger.new or Trigger.old — iterate instead | AvoidDirectAccessTriggerMap | error |
| apex/error-no-empty-catch | Disallow empty catch blocks | EmptyCatchBlock | error |
| apex/error-no-empty-if | Disallow empty if statement bodies | EmptyIfStmt | error |
| apex/error-no-empty-try | Disallow empty try or finally blocks | EmptyTryOrFinallyBlock | error |
| apex/error-no-empty-while | Disallow empty while loop bodies | EmptyWhileStmt | error |
| apex/error-no-hardcoded-id | Avoid hardcoding Salesforce record IDs — they differ between environments | AvoidHardcodingId | error |
| apex/error-no-method-name-as-class | Non-constructor methods should not share the name of the enclosing class | MethodWithSameNameAsEnclosingClass | error |
| apex/error-no-nonexistent-annotation | Avoid annotations that do not exist in Apex | AvoidNonExistentAnnotations (+ legacy NonExistentCustomSettingOrMetadata) | error |
| apex/error-no-stateful-db-result | Avoid storing Database result types as instance variables in Database.Stateful batch classes | AvoidStatefulDatabaseResult | off |
| apex/error-no-type-shadow-namespace | Avoid declaring types with the same name as System or Schema namespace types | TypeShadowsBuiltInNamespace (+ legacy AvoidShadowingField) | off |
| apex/error-override-both-equals-hashcode | If overriding equals(), also override hashCode(), and vice versa | OverrideBothEqualsAndHashcode | error |
| apex/error-test-methods-in-test-class | @IsTest methods must reside in @IsTest annotated classes | TestMethodsMustBeInTestClasses | error |
Performance
| Rule | Description | PMD equivalent | Default |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ------- |
| apex/perf-no-debug-statements | Remove or gate System.debug() calls in production code | AvoidDebugStatements | off |
| apex/perf-no-dml-in-loop | Avoid DML operations, SOQL queries, and governor-limited calls inside loops | OperationWithLimitsInLoop (+ legacy AvoidDmlStatementsInLoops) | error |
| apex/perf-no-eager-describe | Avoid calling Schema.describe*() methods inside loops — cache results instead | AvoidEagerDescribes (distinct from EagerlyLoadedDescribeSObjectResult) | error |
| apex/perf-no-high-cost-in-loop | Avoid high-cost Apex calls inside loops | OperationWithHighCostInLoop (+ legacy AvoidSoqlInLoops / AvoidHighCostInLoopWithoutBulkification) | error |
| apex/perf-no-non-restrictive-query | SOQL queries should include a WHERE clause to limit results | AvoidNonRestrictiveQueries (+ legacy WherelessSOQLQuery) | error |
Security
| Rule | Description | PMD equivalent | Default |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------- | ------- |
| apex/security-crud-violation | DML operations and SOQL queries should include CRUD/FLS permission checks | ApexCRUDViolation | warn |
| apex/security-no-dangerous-methods | Flag potentially dangerous Apex method calls | ApexDangerousMethods | warn |
| apex/security-no-hardcoded-crypto | Avoid hardcoded cryptographic keys or IVs | ApexBadCrypto | error |
| apex/security-no-insecure-endpoint | HTTP callout endpoints must use HTTPS | ApexInsecureEndpoint | error |
| apex/security-no-open-redirect | Avoid constructing PageReference from user-controlled input | ApexOpenRedirect | error |
| apex/security-no-soql-injection | Avoid SOQL injection — use bind variables or String.escapeSingleQuotes() | ApexSOQLInjection | error |
| apex/security-no-xss-false-escape | Disabling HTML escaping (escape=false) can introduce XSS vulnerabilities | ApexXSSFromEscapeFalse | error |
| apex/security-no-xss-from-url | URL parameter values must be sanitized before output to prevent XSS | ApexXSSFromURLParam | error |
| apex/security-sharing-violations | Classes that access data should explicitly declare 'with sharing' or 'without sharing' | ApexSharingViolations | warn |
| apex/security-use-named-credentials | Use Named Credentials instead of hardcoding authentication details in HTTP requests | ApexSuggestUsingNamedCred | warn |
PMD Converter
The PMD Converter is a browser-based tool that converts PMD Apex XML rule files into:
- An ESLint flat-config snippet with the correct rule IDs and severities
- A rule implementation skeleton (when the PMD rule is not yet in this plugin)
No data is sent anywhere — conversion happens entirely in the browser.
PMD rules in the reference mirror without an ESLint twin (yet)
The offline PMD rule list this project tracks includes ExcessiveClassLength, EmptyStatementBlock, EagerlyLoadedDescribeSObjectResult, and NcssConstructorCount. They are intentionally not mapped to an apex/* rule today; the converter emits a custom-… skeleton for them. See the per-rule notes for perf-no-eager-describe versus EagerlyLoadedDescribeSObjectResult, and design-ncss-method-count versus per-constructor NCSS.
Architecture
eslint-plugin-apex/
├── src/
│ ├── node-types.js ← custom AST node type constants + VISITOR_KEYS
│ ├── ast-builder.js ← ANTLR4 parse tree → custom ESLint AST
│ ├── apex-parser.js ← parseForESLint() adapter
│ ├── index.js ← plugin entry point, rule registry, shared configs
│ └── rules/
│ ├── best-practices/ ← 11 rules
│ ├── code-style/ ← 7 rules
│ ├── design/ ← 9 rules
│ ├── documentation/ ← 1 rule
│ ├── error-prone/ ← 14 rules
│ ├── performance/ ← 5 rules
│ └── security/ ← 10 rules
├── tests/
│ ├── parser.test.js ← custom parser tests (node:test)
│ ├── rules.test.js ← RuleTester tests for all rules
│ └── docs-coverage.test.js
└── docs/
├── index.html ← GitHub Pages PMD converter
└── rules/ ← one Markdown page per registered rule
├── best-practices/
├── code-style/
├── design/
├── documentation/
├── error-prone/
├── performance/
└── security/The parser uses @apexdevtools/apex-parser, an ANTLR4-based Apex parser, and wraps its concrete syntax tree into a flat, traversable AST with custom node types.
Contributing
Contributions welcome — especially:
- Improved AST coverage for edge-case Apex constructs
- More comprehensive rule implementations
- Additional test cases
License
MIT — see LICENSE.
