Appendix F.2: Custom PMD XPath Rules¶
This page describes the reference custom rulesets: the custom PMD rules written in XPath for these standards, in five files, for use as the starting point for a Salesforce project's PMD configuration.
- Apex Rules:
pmd-custom-rules-apex.xml - HTML, VF, Aura, LWC Rules:
pmd-custom-rules-markup.xml - Flow Rules:
pmd-custom-rules-flow.xml - Metadata Rules:
pmd-custom-rules-metadata.xml - JavaScript Rules:
pmd-custom-rules-javascript.xml
The reference ships as {N} ruleset files, all listed under engines.pmd.custom_rulesets in the sample code-analyzer.yml, and one more, pmd-all-rules.xml, that imports the other {N} for a direct PMD run. The stock rules file and the stock rules file for JavaScript hold the stock PMD rules this reference adopts ({N} rules across Apex, JavaScript, HTML, and Visualforce: every rule PMD ships for those languages except the ones listed under Stock PMD Rules Not Adopted), each with an explicit priority and, where a rule's behaviour is changed, the property override with PMD's default named in a comment (Appendix F.1 lists them, and says why there are {N} files). The custom rules on this page are the other {N} files, pmd-custom-rules-apex.xml, pmd-custom-rules-markup.xml, pmd-custom-rules-metadata.xml, pmd-custom-rules-flow.xml, and pmd-custom-rules-javascript.xml, the org-authored ones. The rules that complete the set but are not written in XPath are published elsewhere and listed under Companion Rules.
What the XPath Custom Ruleset Covers¶
The rules fall into two families, each serving a different governance purpose. Within the file, comment headings divide each family into groups.
Code Rules¶
The code rules cover Apex, Visualforce, and Lightning markup. Combined with the rules in code-analyzer.yml, they are the code quality layer, governing how code is written and how Lightning Web Component and Visualforce source is structured.
| Group | What its rules check |
|---|---|
| Visualforce, LWC, and Aura markup | An alt attribute on every image, and an empty alt only on a decorative one. Accessibility checks on tables, form groups, frames, lists, and media, each as a pair of rules, one for Visualforce and one for Lightning markup. In Visualforce: the names of a page's controller and extensions, a hard-coded /resource/ path or /apex/ link, an inline style, a javascript: URL, and a script tag inside a script block. In Lightning markup and plain HTML, a javascript: URL as well. In both markups, the absolute URL of a Salesforce org |
| Apex access, sharing, and access mode | An access modifier on every method and outer class, a sharing declaration on every outer class, the same declaration recommended on every inner class, any use of without sharing, tests that run as the current user, and WITH SECURITY_ENFORCED, which is not accepted |
| Apex error prone | A switch without when else, an if / else if chain that a switch could replace, a null check placed after an isEmpty() call on the same collection, a record type looked up by its label, a null check that reads the value it checks, a null check that protects the wrong object of equals(), equals() called on a variable with a string literal as its argument, return null from a method that returns a collection, a condition repeated in one if / else if chain, an if condition that is always the same, an assignment of a value to itself, an abstract class with no method, a class that no code can use, an empty instance initializer, and a switch inside a switch |
| Apex readability | A default written with a ternary in place of ??, a null check in place of ?., a trivial property getter, nested if statements that one if could hold, a variable kept only to be returned, two statements on one line, a negated comparison, an if that returns true or false, a ternary with true or false as a result, a null check next to instanceof, a blank-string test through trim(), and size() compared with zero |
| Apex naming | A field or property with the same name as a method of its class, or as the class, the name of a class with @AuraEnabled methods, and a Boolean method named with get |
| Apex hard-coded values | A string that is an IP address, or a URL whose host is one, a string that holds the absolute URL of a Salesforce org, and a string that holds an email address |
| Apex cookies and cryptography | A cookie created with isSecure set to false, a call to the Crypto class that names MD5 or SHA-1, and every call that encrypts or decrypts, for a security review |
| Apex untrusted input | Code that reads the HTTP Referer header, and every place that reads JSON, for a security review |
| Apex performance | A describe call in the body of a loop that uses no variable of the loop, size() on a query, and a getter that queries without lazy loading |
| Apex triggers | Methods declared inside a trigger |
| Apex testing | Test classes that do not say which code they test or whether they can run in parallel, test classes with no test method, test methods without Test.startTest() and Test.stopTest(), Test.isRunningTest() and assertions outside test code, assertions whose result is fixed by the values written into them, and assertions that a clearer method of the Assert class would express |
Metadata Rules¶
The metadata rules operate on XML metadata files rather than on Apex, Visualforce, HTML, or JavaScript source. They enforce permission governance, the sharing model, metadata documentation, field classification, and metadata quality, as the table sets out. These rules are the pipeline enforcement mechanism for the Tier 1 metadata types classified in Appendix B. They belong in the pipeline architecture regardless of whether Apex code quality is in scope for a given engagement.
| Group | What its rules check |
|---|---|
| Permissions | View All Data and Modify All Data, View All and Modify All Records on standard and custom objects, Manage Users, and View Setup, in Profiles and Permission Sets |
| Sharing model | The organisation-wide default of every custom and standard object, internal and external: Private, or a platform-controlled model |
| Metadata documentation | A description on every field, custom object, Flow, Permission Set, Profile, Custom Permission, and Permission Set Group |
| Field classification and design | A Compliance Categorization and a Data Sensitivity Level on every field, a minimum sensitivity for PII and HIPAA fields, and whether a master-detail field allows reparenting |
| Metadata quality | Hardcoded instance URLs in weblinks, metadata below the API version floor, and the Tier 0 types, which must not be committed |
| Integration endpoints | An IP address in the metadata that says where the org connects to |
| Org settings | Lightning Web Security switched off in the Security settings file |
| Flow, Process Builder, and Workflow Rules | Process Builder automations, Flows that run in system mode, scheduled Flows below API version 61.0, Workflow Rules, a Get Records element with no filter, a loop inside a loop, a Flow that is not in auto-layout, and an inactive Flow |
{N} of the permission rules carry a violation suppression that skips standard Profiles (those whose custom element is false), so that committing a standard Profile to version control does not raise false positives.
How the Rules Are Written¶
The Apex, Visualforce, and markup rules on this page are written in XPath against PMD's syntax tree, with PMD's XPathRule class. The metadata rules are written against the XML document itself, with PMD's DomXPathRule class, which PMD recommends for XML: the query is standard XPath (enabled = 'true', not(description)), the defaultNsUri property names the Salesforce metadata namespace so element names match without prefixes, and pmd:fileName() is available for checks that depend on the file name. Two things stay in PMD's older dialect: the violationSuppressXPath property, which PMD evaluates on its own wrapped tree whichever class the rule uses, and PMD's Rule Designer, which does not support DomXPathRule, so metadata queries are tested with an ordinary XPath tool or a PMD run.
Some checks cannot be written in XPath at all and need custom PMD rules written in Java: a check that depends on another file (an Apex class's API version is stored in its -meta.xml, not in the .cls file), or on detail the parser records but does not expose to XPath (the access mode of a DML statement). Those rules have their own page, Appendix F.3.
Using and Changing the Ruleset¶
This ruleset is a starting point, not a prescription. Rule selection, priority assignments, and suppression policies should be reviewed and adjusted to reflect each organisation's risk posture, coding standards, and compliance requirements, as discussed in Section 5. Changes to the ruleset should follow the formal change process: PR, review, sign-off, and changelog entry. Be sure to communicate all changes and timelines to developers.
The custom ruleset is stored in config/pmd/ of the project repository, where code-analyzer.yml names it (The PMD Folder), version-controlled alongside source code, and protected by CODEOWNERS so that only the governance team (@<my-org>/admin-team) can modify it.
Every rule carries a priority. The Priority Reference shows how a rule's priority becomes its severity, and Severity Levels says what each severity means for the pipeline.
The Code Analyzer configuration file can change a rule's severity, disable the rule, or tag it, for the stock PMD rules and for these custom rules alike. A rule's property is different: a property override in a ruleset does apply under Salesforce Code Analyzer, and a ruleset is the only place a property can be set, because code-analyzer.yml has no syntax for one (Appendix E).
{N} of the custom rules hold a value that each organisation is expected to set for itself. Each keeps it in a named property of its own, so the value is changed without touching the XPath:
| Rule | Property | Default value | What it holds |
|---|---|---|---|
ProhibitedMetadataType |
metadataTypes |
FlowDefinition, Certificate, ExtlClntAppGlobalOauthSettings, Scontrol |
The Tier 0 metadata types, which are never committed |
AvoidHardcodedIpAddressMetadata |
metadataTypes |
NamedCredential, RemoteSiteSetting, CspTrustedSite, CorsWhitelistOrigin, ExternalDataSource, AuthProvider, Workflow, WorkflowOutboundMessage |
The metadata types whose values are read for an IP address |
BumpApiVersion |
minimumApiVersion |
60 |
The lowest API version a metadata file may have |
RequireExplicitSharing |
reportInTestClasses |
true |
Whether a test class without a sharing declaration is reported. It is true here, because the standard holds a test class to the same rule as any other class (Apex: Be Explicit Everywhere) |
AuraEnabledControllerNaming |
controllerPattern |
.*Controller |
The pattern that the name of a class with @AuraEnabled methods must match |
AvoidAbsoluteSalesforceUrl |
allowedSubdomains |
www, help, developer, trailhead, status, trust, appexchange |
The subdomains of salesforce.com whose URLs may be written into Apex |
AvoidAbsoluteSalesforceUrlHtml |
allowedSubdomains |
www, help, developer, trailhead, status, trust, appexchange |
The subdomains of salesforce.com whose URLs may be written into Apex |
AvoidAbsoluteSalesforceUrlVf |
allowedSubdomains |
www, help, developer, trailhead, status, trust, appexchange |
The subdomains of salesforce.com whose URLs may be written into Apex |
AvoidJavaScriptUrlHtmlLwcAura |
allowedPattern |
^\s*javascript:\s*void\s*\(\s*0\s*\)\s*;?\s*$ |
The javascript: values that may be written in Lightning markup and plain HTML, case ignored; by default javascript:void(0), with or without a semicolon |
AvoidJavaScriptUrlVf |
allowedPattern |
^\s*javascript:\s*void\s*\(\s*0\s*\)\s*;?\s*$ |
The javascript: values that may be written in Visualforce, case ignored; by default javascript:void(0), with or without a semicolon |
NoHardcodedWeblinkURLs |
allowedSubdomains |
www, help, developer, trailhead, status, trust, appexchange |
The subdomains of salesforce.com whose URLs a weblink may hold |
RequireLazyLoadingInGetters |
controllerPattern |
.*Controller|.*(Extension|Ext) |
The pattern for the names of the Visualforce controllers and extensions whose get methods the rule reads; it should match the patterns of VisualforceControllerNaming |
VisualforceControllerNaming |
controllerPattern, extensionPattern |
.*Controller, .*(Extension|Ext) |
The patterns that the names of a Visualforce page's custom controller and extensions must match |
A metadata type is named in these lists by its API name, which is the name of the root element of its file. In ProhibitedMetadataType the list is how a group of metadata types gets a rule: a type is added to the list, and every file of that type is reported. In AvoidHardcodedIpAddressMetadata the list says which files are read. A naming pattern is a regular expression that the whole class name must match.
Aura Markup and PMD Alone¶
The rules for HTML, Lightning Web Component, and Aura markup read Aura's .cmp and .app files as HTML. Code Analyzer gives those files to PMD as HTML, because the sample code-analyzer.yml lists them among the HTML file types (Appendix E). PMD on its own chooses a language by the file extension, and it has no Aura language, so it skips .cmp and .app files. Its command line has no option to add an extension to a language.
To check Aura markup with PMD alone, run PMD a second time with --force-language html, which reads every file of the run as HTML. Give that run a list of the Aura markup files only, because the controller, helper, and style files of an Aura bundle are not HTML:
find force-app -path '*/aura/*' \( -name '*.cmp' -o -name '*.app' \) > aura-markup.txt
pmd check --force-language html --file-list aura-markup.txt --rulesets config/pmd/pmd-custom-rules-markup.xml,config/pmd/pmd-stock-rules.xml
An organisation with no Aura components does not need this second run.
Notes on Individual Rules¶
A rule's description says what the rule requires. These notes say how some of the rules behave: what a rule reports, what it leaves alone, and how it differs from a stock PMD rule. The rule tables on the standards pages, and the catalogue on the Appendix F title page, link each of these rules to its note.
ApexDocDescriptionLength¶
An ordinary comment is invisible to XPath, but PMD exposes an ApexDoc block to it as a FormalComment node whose text is the whole comment. The rule takes the description of the comment: the text after the @description tag when there is one, and otherwise the text at the top of the comment, before any tag, which is how Salesforce writes it. It cuts that text at the next ApexDoc block tag (@param, @return, @throws, @exception, @deprecated, @since, @see, @author, @version, @group, or @example) or at the end of the comment, so an annotation such as @future in the text does not end it. It turns each line's leading asterisk into a space, collapses the whitespace, and reports a description of fewer than 10 characters, counted from its first letter and including spaces. A description that wraps onto a second line is counted as one. A comment with no description at all is the stock ApexDoc rule's to report. The standard is in Apex: Documentation.
AvoidAbsoluteSalesforceUrl¶
The rule reports a My Domain, Lightning, Visualforce, Sites, or Experience Cloud host, an instance host such as na1.salesforce.com, and the two login hosts, wherever the URL is in the string. It does not report a link to one of the public Salesforce websites, such as help.salesforce.com or developer.salesforce.com, which are the same for every org. Those subdomains are the rule's allowedSubdomains property, and an organisation adds any other subdomain of salesforce.com that its code may write out (Using and Changing the Ruleset). The standard is in Apex: Hard-Coded Salesforce URLs.
AvoidHardcodedIpAddressApex¶
The rule reports an address that is the whole string, with or without a port, and an address that is the host of a URL. It does not report an address inside other text, so a version number in a sentence or in a path is not taken for one. A string that is nothing but a four-part version number, such as 1.0.0.0, cannot be told from an address and is reported. The standard is in Apex: Hard-Coded IP Addresses.
AvoidHardcodedIpAddressMetadata¶
The rule reads Named Credentials, Remote Site Settings, CSP Trusted Sites, CORS allowlist origins, External Data Sources, Auth Providers, and workflow outbound messages. Those types are its metadataTypes property, so an organisation adds the others in which it keeps endpoints, such as the custom metadata type that holds them (Using and Changing the Ruleset). In a file of a listed type it reports a value that is an address, with or without a port, or the host of a URL, and not an address inside other text, such as a version number in a path. The standard is in Integration: Endpoints.
AvoidHardcodedResourcePathVf and AvoidHardcodedPageLinkVf¶
The {N} rules read attribute values, script blocks, and quoted strings inside an expression. A sentence that only mentions /apex/ is not reported. The standards are in HTML and Visualforce: Resources and Links.
AvoidInsecureCookies and AvoidWeakCryptoAlgorithms¶
Both rules read a value written as a literal in the call, and do not follow a variable or a constant. AvoidWeakCryptoAlgorithms also reports the algorithm name RSA, which the Crypto class documents as an RSA signature of a SHA-1 hash. The standards are in Apex: Cookies and Cryptography.
AvoidNonSelectiveSoqlFilter¶
The rule reads a SOQL query and reports a WHERE clause with a filter that cannot use an index: !=, <>, EXCLUDES, NOT as a logical modifier (NOT IN, NOT (...), NOT Field LIKE), or a LIKE whose pattern starts with % or _. A filter on null, != null, is reported too, because it reads the whole table as any other negative filter does. It reads inline SOQL and a query written as a string, wherever the string sits: any string literal that starts with SELECT and holds FROM, so the string assigned to a variable before a Database.query call is read as well as one passed to the call. Of a query built from several strings, only the piece that starts with SELECT is read. The ApexGuru rules SoqlWithNegativeExpressions and SoqlWithWildcardFilter report the same things through a connected org; this rule reports them on every pull request with no org required. The standard is in Apex: Queries.
BumpApiVersion¶
This rule and Code Analyzer's own AvoidOldSalesforceApiVersions check the same thing and set the floor differently. AvoidOldSalesforceApiVersions ships with Salesforce Code Analyzer and reports any version three or more years old. The three years is fixed, so its floor cannot be set, and it rises with each Salesforce release. BumpApiVersion is the PMD rule in this ruleset. Its floor is its minimumApiVersion property, so it is the rule to change for a floor of an organisation's own. The standard is in Metadata Standards: API Version.
The Code Analyzer rule is switched off with disabled: true under rules.regex in code-analyzer.yml (Customize the Configuration), and the reason its floor moves by itself is set out in Appendix F.4. The sample configuration runs both rules, so that either can be kept. In Code Analyzer 5.16.0 the rule that ships also has a bug in the calculation of its floor, reported to Salesforce as code-analyzer issue #2102: each January the floor is three versions too high, and it drops back in February (Appendix F.4). An organisation may choose to use the standard rule once that bug is fixed.
Both rules read the apiVersion element of a metadata file, which the files for Apex classes and triggers, Visualforce pages and components, Lightning components, and Flows all carry. Neither reads sourceApiVersion in sfdx-project.json, or the version in a package.xml manifest.
The Capital Letter Rules¶
{N} generic rules report a component whose name or label starts with a lower-case letter. Each takes the metadata types it reads from a types property, by the root element of the file: Flow, CustomObject, and CustomField by default, and an organisation adds a type to the list to cover it. MetadataNameStartsWithCapital reads the fullName element where the file has one, as a field's file does, and the file's name otherwise, up to the type suffix, which is how a Flow and an object are named in source format; it reads the file's name with pmd:fileName(). MetadataLabelStartsWithCapital reads the label element, and allows a label to start with a digit. The names block (priority 2), because a name is hard to change once it is referenced; the labels are reported (priority 3), because a label changes at any time. The standard is in Objects and Fields: Naming.
DeclareAccessOnMethods¶
A test method needs no modifier and is not reported. Three things cannot carry a modifier and are not reported either: a static initialiser block, the accessor methods that PMD creates for a property, and the body of a trigger, which PMD models as a method. A method written inside a trigger is still checked. The standard is in Apex: Access Modifiers.
The Description Rules¶
The {N} rules that require a description, on a custom field, a standard field, a custom object, a Flow, and the permission components, report a description of 10 characters or fewer as well as a missing one. The length is measured after normalize-space(), so a description of spaces, or of a placeholder such as TBD, counts as none. The standard is in Objects and Fields: Descriptions.
DoNotUseProcessBuilder and DoNotUseWorkflow¶
DoNotUseProcessBuilder reports every Process Builder process: a Flow file whose process type is Workflow (a process that starts when a record changes), InvocableProcess (one that another process calls), or CustomEvent (one that a platform event starts). DoNotUseWorkflow reports every Workflow Rule. Each one must be converted to a Flow. Neither rule reports a process or a Workflow Rule from a managed package, whose name has a namespace prefix, because the organisation cannot change it: DoNotUseProcessBuilder reads the prefix in the name of the Flow file, and DoNotUseWorkflow in the name of the rule. The standards are in Automation: Workflow & Process Builder.
FlowNotAutoLayout¶
A Process Builder process has no canvas and is not reported. The standard is in Automation: Flow.
LayoutTable and LayoutTableWithSemanticMarkup¶
Applies to LayoutTableVf, LayoutTableHtml, LayoutTableWithSemanticMarkupVf, and LayoutTableWithSemanticMarkupHtml.
A layout table that carries data table markup is reported by both rules: once as a layout table, and once for the markup. The standards are in HTML and Visualforce: Tables.
The Permission Rules¶
Applies to ModifyOrViewAllData, ViewSetupProfile, ViewSetupPermissionSet, ManageUsers, ModifyOrViewAllRecordsStandard, and ModifyOrViewAllRecordsCustom.
The rules read Profile and Permission Set metadata files. Each applies a violationSuppressXPath that excludes a standard profile, one where custom is false, because the platform manages it. The standards are in Permissions and Security: High-Risk Permission Elements Within Profiles and Permission Sets.
RequireExplicitSharing¶
The rule has a reportInTestClasses property, true by default. An organisation that does not want its test classes checked sets the property to false in the ruleset. The standard is in Apex: The Standards.
RequireExplicitSharing is more restrictive than ApexSharingViolations, because it does not check for DML. The stock rule reports a class with no sharing declaration only where the class holds a query, a DML statement, or a Database method call, and it does not report a test class. RequireExplicitSharing reports every outer class with no declaration, whatever the class does. ApexSharingViolations also reports an inner class, where the sharing standard recommends the declaration and does not require it. These standards do not enforce the stock rule (Stock PMD Rules Not Adopted).
RequireTestForOrCritical¶
The rule checks that one of the two parameters, testFor or critical=true, is in the @IsTest annotation of a test class. It cannot check that testFor names the right class or trigger, or that critical=true is deserved. The standard is in Apex: Test Selection.
Both parameters compile only in a class at API 66.0 or later, so the rule also brings a test class forward: the next change to an older test class has to raise its API version. That is deliberate, and it is work for an organisation with many old test classes. An organisation that is not ready can switch the rule off, or lower its severity, in its own ruleset. A test class with no test methods, such as a test data factory, is not reported.
PMD 7.28.0 has a stock rule for the same check, ApexUnitTestClassShouldHaveRunRelevantTestsAnnotation, which does not report a class below API 66.0. This rule deliberately takes no account of the API version: it reports every test class that is edited, not only those at API 66.0 and later, which is what brings the older ones forward. Salesforce Code Analyzer 5.16.0 ships PMD 7.26, so current standards use a custom rule of their own. When Salesforce Code Analyzer ships PMD 7.28 or later, the standards and rules will be updated.
TableCellHeaderNotFound¶
Applies to TableCellHeaderNotFoundVf and TableCellHeaderNotFoundHtml.
A headers value that is bound to an expression is not checked, because what it names is not known until the page runs. The standard is in HTML and Visualforce: Tables.
TriggerNameSuffix¶
The rule reads the trigger's name and reports one that does not end with a suffix in its suffixes property, compared without regard to case. The default is Trigger, so AccountTrigger passes and Accounts is reported. An organisation that names its triggers after the plural of the object sets the property to its own list (Using and Changing the Ruleset); a bare s passes any name that ends in that letter, so a list of the plural names is the safer setting. The standard is in Apex: Naming.
VisualforceControllerNaming¶
The rule reports on the page, where the class is named, and not on the class. A page that has a standard controller and no extension is not reported. The two endings are the rule's controllerPattern and extensionPattern properties, so an organisation with other endings sets its own (Using and Changing the Ruleset). The standard is in HTML and Visualforce: Controller Names.
Custom PMD Rulesets (Sample)¶
Each custom ruleset in full is on a page of its own: pmd-custom-rules-apex.xml, pmd-custom-rules-markup.xml, pmd-custom-rules-metadata.xml, pmd-custom-rules-flow.xml, and pmd-custom-rules-javascript.xml. Its rules are listed by group, beside the Java rules, in The Custom Rules on the Appendix F landing page. Stock PMD rules are not in these files: the {N} stock rules files adopt them (Appendix F.1 links to both), and code-analyzer.yml sets their severities.
Companion Rules¶
Two kinds of rule complement this ruleset and are published elsewhere. The access mode rules, RequireAccessMode and FlagSystemMode, are written in Java and published in Appendix F.3: both read the DML access mode, which PMD does not yet expose to XPath (pmd/pmd#7108). The suppression rule NoUnapprovedSuppression, the {N} comment rules (NoUnresolvedTodosApex, NoUnresolvedTodosJavaScript, NoUnresolvedTodosMarkup, and NoEmptyBlockComments), NoPolyfillIo, and NoConsoleInVisualforce are Code Analyzer regex rules: they are defined in the sample code-analyzer.yml and described in Appendix F.4.
Enforcing a Trigger Framework (Sample Rule)¶
This section is a note, not part of the reference ruleset. These standards name the Trigger Actions Framework as the trigger framework for every object (Automation Framework), and under it the body of every trigger is exactly one line:
trigger AccountTrigger on Account(before insert, before update, after insert, after update) {
new MetadataTriggerHandler().run();
}
A standard that exact can be checked exactly. The rule below reports any trigger whose body is anything else. It is published as an example of how a ruleset can enforce an architectural choice, for an organisation that wants to do so. It is not in the custom ruleset and has its own file, because an organisation that uses a different framework would fail it on every trigger.
The Rule¶
Expand to show the rule that allows only the one-line framework trigger
<?xml version="1.0" encoding="UTF-8" ?>
<ruleset
name="Trigger Framework Sample Rule"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
A sample, not part of the reference ruleset: one rule that enforces the
Trigger Actions Framework's one-line trigger.
</description>
<!--
Reports a trigger whose body is anything other than the one line the
Trigger Actions Framework requires:
new MetadataTriggerHandler().run();
PMD models a trigger's body as a method that is not in the source
(@RealLoc is false). The body conforms when it holds exactly one
statement, and that statement calls run() with no arguments on a new
MetadataTriggerHandler built with no arguments. A method declared in
the trigger is a second way to fail. Names are compared in lower case
because Apex is case-insensitive; comments are not code and do not
count.
-->
<rule
name="RequireFrameworkTriggerBody"
language="apex"
message="The body of the trigger must be exactly: new MetadataTriggerHandler().run();"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
<description>Under the Trigger Actions Framework a trigger holds one line, new MetadataTriggerHandler().run(); and nothing else: no logic, no context checks, no variables and no methods. This rule reports any trigger that holds anything else.</description>
<priority>1</priority>
<properties>
<property name="xpath">
<value><![CDATA[
//UserTrigger[
Method[@RealLoc = true()]
or count(Method[@RealLoc = false()]/*[not(self::ModifierNode)]) != 1
or not(
Method[@RealLoc = false()]/ExpressionStatement/MethodCallExpression
[lower-case(@FullMethodName) = 'run'][@InputParametersSize = 0]
/ReferenceExpression/NewObjectExpression
[lower-case(@Type) = 'metadatatriggerhandler'][not(*)]
)
]
]]></value>
</property>
</properties>
</rule>
</ruleset>
The file is pmd-trigger-framework-rule.xml. To use it, list it under engines.pmd.custom_rulesets in code-analyzer.yml beside the other rulesets, and override its severity to 1 as Appendix E describes. Conforming to the framework's one-line trigger is a priority 1 standard, so the rule blocks the pull request.
What It Reports¶
A trigger that does not conform is reported as a whole, at its first line.
| Trigger body | Reported |
|---|---|
new MetadataTriggerHandler().run(); |
No |
| The same line in another letter case, or with comments around it | No |
| Empty | Yes |
| The line followed by a second statement | Yes |
The line wrapped in a context check such as if (Trigger.isInsert) |
Yes |
A different handler, such as new AccountTriggerHandler().run(); |
Yes |
An argument passed to the constructor or to run() |
Yes |
The handler assigned to a variable first, then handler.run(); |
Yes |
| A method declared in the trigger, with the line beside it | Yes |
Verified on PMD 7.26.0 and on Salesforce Code Analyzer 5.16.0 against one trigger for each row.
How It Works¶
PMD models the body of a trigger as a method that does not appear in the source, which the @RealLoc attribute marks as false. The rule tests three things about each trigger:
- It declares no methods of its own, which are the
Methodnodes whose@RealLocistrue. - Its body holds exactly one statement.
- That statement calls
run()with no arguments on a newMetadataTriggerHandlerthat is built with no arguments.
Names are compared in lower case because Apex is case-insensitive. Comments are not part of the syntax tree, so they never count as a statement.
Adapting to Another Framework¶
The same technique enforces any framework whose triggers have a fixed shape. Under a framework where each object has its own handler class, for example, the trigger body is new AccountTriggerHandler().run();. Changing one predicate makes the rule accept any class whose name ends in TriggerHandler:
That line replaces [lower-case(@Type) = 'metadatatriggerhandler'][not(*)] in the rule above, and the message changes to match.
Additional Trigger Rules¶
NoMethodsInTriggersin the custom ruleset reports each method declared in a trigger, at the method's own line, and not the trigger's own body. This sample rule reports the same trigger at its first line. Run both to get the method's line as well.DeclareAccessOnMethodsalso applies inside a trigger, so a method there with no access modifier is reported by both rules. An organisation can run either rule, or both.- PMD's stock rule
AvoidLogicInTriggeris a general check that knows nothing about a framework. The reference ruleset adopts it at priority 3. - The Java rules
RequireAccessModeandFlagSystemModein Appendix F.3 check a database operation written in a trigger body.