@cloudalgo/apex-lint
Zero-JVM Salesforce Apex linter. No Java, no Code Analyzer plugin — runs 47 built-in static analysis rules against your Apex source using the same ANTLR grammar as PMD 7.
npm install -g @cloudalgo/apex-lint
apex-lint force-app/Quick start
# Lint an sfdx project
apex-lint path/to/force-app
# Run only security rules
apex-lint force-app --categories security
# Fail CI on high severity and above
apex-lint force-app --fail-on high
# SARIF output for GitHub code scanning
apex-lint force-app --format sarif --output results.sarif
# List all 47 rules grouped by category
apex-lint --list-rulesCLI reference
apex-lint <path...> [options]
Options:
-f, --format <fmt> pretty | json | sarif (default: pretty)
-o, --output <file> write results to file instead of stdout
--fail-on <sev> fail at this severity+ (default: moderate)
critical | high | moderate | low | info
-c, --config <file> path to config json
--rules <ids> comma-separated rule IDs to run (default: all 47)
--exclude-rules <ids> comma-separated rule IDs to skip
--categories <cats> comma-separated categories to run
security | performance | error-prone | design
best-practices | code-style
--metadata-root <dir> sfdx project root for SObject metadata (repeatable)
--list-rules print all rules grouped by category
-h, --help show this help
Summary and progress always go to stderr; violation output goes to --output or stdout — safe to pipe in CI without mixing logs.
Performance. Runs of ≥ 64 files parse and lint in parallel across a worker-thread pool (~2.9× faster on large codebases); smaller runs stay serial. Set APEX_LINT_NO_PARALLEL=1 to force serial execution.
Environment. The npm update check is skipped when CI or NO_UPDATE_NOTIFIER is set.
Configuration
Place apexlint.config.json (or .apexlintrc.json) in your project root — auto-discovered from the current directory and each scanned path. Pass an explicit path with --config.
{
"rules": ["SoqlInLoop", "DmlInLoop", "ApexSOQLInjection"],
"excludeRules": ["MethodNamingConventions", "AvoidGlobalModifier"],
"categories": ["security", "performance"],
"severityOverrides": {
"EmptyCatchBlock": "critical",
"AvoidNonRestrictiveQueries": "info"
},
"excludePaths": ["**/test/**", "**/*Test.cls", "**/legacy/**"],
"maxViolationsPerFile": 50,
"metadataRoots": ["./force-app/main/default"],
"failOn": "high"
}| Field | CLI equivalent | Description |
|---|---|---|
rules |
--rules |
Run ONLY these rule IDs — all others are skipped |
excludeRules |
--exclude-rules |
Skip these rule IDs (merged with CLI flag) |
categories |
--categories |
Run only rules in these categories |
severityOverrides |
— | Override per-rule severity |
excludePaths |
— | Glob patterns for files to skip (*, **, ? supported) |
maxViolationsPerFile |
— | Cap violations per file (useful on large legacy codebases) |
metadataRoots |
--metadata-root |
sfdx project roots for SObject metadata |
failOn |
--fail-on |
Minimum severity for non-zero exit |
Precedence: CLI flags override config. rules takes priority over excludeRules and categories.
Suppression
Compatible with PMD suppression syntax:
doSomething(); // NOPMD
[SELECT Id FROM Account]; // NOPMD: SoqlInLoop
@SuppressWarnings('PMD.DmlInLoop')
public void myMethod() { ... }
@SuppressWarnings('PMD')
public class LegacyHelper { ... }
Rules (47)
Security (10)
| Rule | Severity |
|---|---|
ApexSOQLInjection |
critical |
ApexOpenRedirect |
high |
ApexSSRF |
high |
ApexXSSFromURLParam |
high |
ApexXSSFromEscapeFalse |
high |
ApexBadCrypto |
high |
ApexSharingViolations |
high |
DatabaseQueryWithVariable |
high |
UnguardedCrudOperation ★ |
high |
ApexCSRF |
moderate |
Performance (6)
| Rule | Severity |
|---|---|
SoqlInLoop |
high |
DmlInLoop |
high |
HttpCalloutInLoop |
high |
SoqlInBatchExecute |
moderate |
AvoidNonRestrictiveQueries |
low |
SystemDebugInLoop |
low |
Error-Prone (12)
| Rule | Severity |
|---|---|
InaccessibleAuraEnabledGetter |
high |
TestMethodsMustBeInTestClasses |
high |
FutureMethodChaining |
high |
EmptyCatchBlock |
moderate |
OverrideBothEqualsAndHashcode |
moderate |
AvoidHardcodedId |
moderate |
MapGetWithoutNullCheck |
moderate |
SoqlResultIndexWithoutCheck |
moderate |
TriggerContextNullAccess |
moderate |
ChainedRelationshipAccess |
info |
SoqlResultNotNullChecked |
moderate |
MapGetResultNotNullChecked |
info (opt-in) |
Design (8)
| Rule | Severity |
|---|---|
TriggerInlineLogic |
moderate |
CyclomaticComplexity |
moderate |
CognitiveComplexity |
moderate |
AvoidDeeplyNestedIfStmts |
moderate |
ExcessiveParameterList |
low |
ExcessivePublicCount |
low |
TooManyFields |
low |
UnusedPrivateMethod |
low |
Best Practices (10)
| Rule | Severity |
|---|---|
TestWithoutAsserts |
moderate |
SeeAllDataTrue |
moderate |
HardcodedUrl |
moderate |
QueueableWithoutFinalizer |
low |
AvoidGlobalModifier |
low |
AvoidFutureAnnotation |
low |
DebugsShouldUseLoggingLevel |
low |
ApexAssertionsShouldIncludeMessage |
low |
ApexUnitTestMethodShouldHaveIsTestAnnotation |
low |
ApexUnitTestClassShouldHaveRunAs |
low |
Code Style (1)
| Rule | Severity |
|---|---|
MethodNamingConventions |
low |
★ type-aware — needs --metadata-root for SObject context
Full descriptions, examples, and fix guidance: docs/rules.md
GitHub Actions example
- name: Apex lint
run: |
npx @cloudalgo/apex-lint force-app \
--format sarif \
--output apex-lint.sarif \
--fail-on high
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: apex-lint.sarifEmbedding the engine
To run rules programmatically (no CLI), use @cloudalgo/apex-core.
Repository
github.com/cloudalgo/apex-lint · License: BSD-3-Clause