Apex¶
TODO(David)
Write this page. Stub created for structure only.
TODO
Class structure, error handling, asynchronous Apex. Readability and Governor Limits were drafted 2026-10-03 from David's rulings and the stock PMD rules the reference adopts; David to review.
Source (Step-Based Async Processing Framework; see standards-sources memory)
Async-first patterns: Queueable with Finalizers over ageing Batch, Apex Cursors for large result sets, step decomposition for retry and checkpointing, transaction-independent logging
Standards for Apex code: governor limits, readability, the API version, sharing and access mode, security, hard-coded values, testing, asynchronous Apex, exception handling, and architecture.
Most standards on this page end by naming the rule that enforces them, at a severity whose meaning for a pull request is set out in Severity Levels. Some standards cannot be checked by a static analysis rule. They are standards all the same: reviewers check them, and a violation of a blocking one is grounds for a reviewer to reject the pull request (Principles).
Governor Limits¶
Apex runs on shared infrastructure, and the platform caps what one transaction may use: the number of SOQL queries and DML statements, the rows they touch, CPU time, heap, and more (Execution Governors and Limits). The limits belong to the transaction, not to the class: a trigger, its handler classes, and the Flows on the same object all draw on one allowance, so code that wastes a limit takes it from everything that runs beside it.
Limits are design constraints from the start, not problems to fix after they are hit (Platform Limits). Every process is designed for the volumes it will meet in production, not the volumes it meets in testing.
- No code reaches a platform limit. A process is designed to stay well within every limit at production volumes, with room left for other automations that run in the same transaction or draw on the same allocation. When code exceeds a limit, the platform raises an exception that cannot be handled (Execution Governors and Limits), so the transaction fails and its work is rolled back. No rule can measure how close code comes to a limit. Reviewers check the design.
- Code that risks reaching a limit checks how close it is, and adjusts where it can. The methods of the
Limitsclass say how much of each limit the transaction has used and how much it is allowed, and theOrgLimitsclass says the same for an allocation of the whole org (Execution Governors and Limits). Where the code can adjust, it does so before the limit is reached: it works on fewer records, hands the rest of the work to an asynchronous job, or stops and logs what it left undone. Reviewers check this. - Automation avoids recursion, where the record performs DML on itself. Apex that fires a trigger, which runs Apex that fires the trigger again, is held to a far lower depth limit than ordinary recursion (Execution Governors and Limits). The standard is on the Automation page (Automation Framework).
- Work that does not fit within the limits of one transaction is moved to asynchronous Apex, not squeezed in. Several limits are higher for asynchronous Apex, and in Batch Apex the limits start again for each batch of records (Execution Governors and Limits).
ApexGuru¶
ApexGuru is a Salesforce feature. Through Code Analyzer it scans the Apex of a connected org and reports performance anti-patterns and, in an org where ApexGuru is activated, with runtime data from the org, the methods that use the most CPU time. Many standards on this page cite one of its rules beside the PMD rule for the same thing, as a second layer: an ApexGuru rule is named as such, and Appendix E says what the engine needs and where it runs. A finding of a rule that Salesforce adds later is reported at severity 5, for information, until these standards place it. Enforced by the ApexGuru rule apexguru-other.
The Limits Reached Most Often¶
Three limits are the ones that Apex code reaches most often. Each is a count for the whole transaction, and the reference gives its value for synchronous and for asynchronous Apex (Execution Governors and Limits).
- The number of SOQL queries issued.
- The number of DML statements issued.
- The number of callouts: HTTP requests and web service calls.
A query or a DML statement inside a loop reaches the first two (Bulkification), and a callout made once for each record reaches the third.
In an org with large data volumes, a fourth joins them: the number of records retrieved by SOQL queries, counted across every query of the transaction. A query with no filter, or with a filter that stops being selective as the object grows, reaches it (Queries). These standards treat every org as a large one (Large Data Volumes).
Running automation asynchronously often raises a limit. The number of SOQL queries, the heap size, and the CPU time allowed are all higher for asynchronous Apex. The number of DML statements, of callouts, and of records retrieved is the same (Asynchronous Apex).
An asynchronous context also has restrictions of its own: a batch job or a future method cannot call a future method, and an asynchronous job can add only one Queueable job to the queue. Asynchronous work that starts more asynchronous work also fills the org's daily allocation and delays other jobs (Future Methods), so code checks its context before it starts any (Execution Context).
Limits Counted for the Whole Org¶
Some limits are not counted for a transaction. They are allocations for the whole org, counted over a rolling period, and code that uses one up stops that kind of work for every user and every process in the org until the usage of the period falls back within the allocation.
- Asynchronous Apex executions: batch Apex, future methods, Queueable Apex, and scheduled Apex, counted over a rolling 24 hours. A job that needs more than is left throws an exception (Execution Governors and Limits).
- Platform event publishing, counted over the last hour. Once the allocation is used, every publish call fails (Platform Event Allocations).
- Platform event delivery to Pub/Sub API and CometD subscribers,
empApicomponents, and event relays, counted over the last 24 hours. Once the allocation is used, subscribers are disconnected (Platform Event Allocations). - API requests, an allocation for the org that spans 24 hours, which most
ConnectApicalls also count towards (Execution Governors and Limits). - Email sent from Apex to external addresses, counted for each day (Execution Governors and Limits).
A design that queues a job, publishes an event, or sends an email for each record is sized against the org's allocation at production volumes, together with everything else in the org that draws on the same allocation. Reviewers check this.
What is left of an allocation can be read while the org runs, with the OrgLimits class in Apex or the limits resource of the REST API. Batch Apex reads it before a job starts, and does not start the job unless there is room for all of it (Execution Governors and Limits).
Bulkification¶
- Code handles records in collections, never one at a time. A trigger receives up to 200 records in one invocation, and everything it calls is written to process all of them in a single pass (Bulk Trigger Idioms). No rule can check this. It is a blocking standard: a reviewer rejects a pull request that breaks it.
- No code reads one record of
Trigger.neworTrigger.oldby a fixed index, such asTrigger.new[0]. Such code handles the first record of the batch and ignores the others, and nothing fails to show it. A fixed index onTrigger.neworTrigger.old, in a trigger or in any class, blocks the pull request (severity 2). Enforced byAvoidDirectAccessTriggerMap. The rule does not report an index held in a variable, or an index on a copy of the list, so reviewers check those. - No operation that counts against a limit runs inside a loop: no SOQL query, SOSL search, DML statement,
Databasemethod, email send, or asynchronous call. Query once before the loop, collect the changes in a list, and run one DML statement after it (Running Apex within Governor Execution Limits). An operation inside a loop blocks the pull request (severity 2). Enforced byOperationWithLimitsInLoop. Also enforced by the Graph Engine ruleAvoidDatabaseOperationInLoop(Developer Preview), which follows the call path from an entry point, so it also finds a query or a DML statement in a method that a loop calls (severity 2). Also enforced byDmlInLoopAcrossMethods(severity 2), a fallback that runs with PMD alone: it finds a call, in a loop, to a method of the same file that runs DML. Also enforced by the ApexGuru rulesSoqlInALoop(severity 2),SoqlInALoopOneHop(severity 3), which looks one method call away from the loop, andDmlInALoop(severity 2). - No call to a future method runs inside a loop: each call counts against the transaction's limit on future calls. The rule above reports a job queued in a loop, and cannot tell that an ordinary method call is a call to a future method, so reviewers check this. It is a blocking standard: a reviewer rejects a pull request that breaks it.
- Some calls count as a DML statement without looking like one:
Database.setSavepoint,Database.rollback,Approval.process,Database.convertLead, a call toEventBus.publishfor a platform event that is published after commit, and every write through a class of theConnectApinamespace (Execution Governors and Limits). None of them runs inside a loop, and platform events are published as one list in one call.OperationWithLimitsInLoopreports the first four in a loop. It does not reportEventBus.publishor aConnectApicall, so reviewers check those two. - Email messages are collected in a list and sent in one call to
Messaging.sendEmail, because the limit counts the calls and not the messages (Execution Governors and Limits). A send inside a loop blocks the pull request (severity 2). Enforced byOperationWithLimitsInLoop. - No callout is made once for each record. The callouts of a transaction count against a limit, and so does the time that all of them take together (Execution Governors and Limits). The records are sent in one request where the other system accepts several, and otherwise the callouts are made from asynchronous Apex, in groups that fit within the limit. No rule reports a callout in a loop. It is a blocking standard: a reviewer rejects a pull request that breaks it.
Queries¶
- Every SOQL query and SOSL search limits what it can return, with a
WHEREclause or aLIMIT. An unfiltered query that works in a sandbox fails in production, where the table is large. An unrestricted query blocks the pull request (severity 2), and is not reported in a test class. Enforced byAvoidNonRestrictiveQueries. This is the Large Data Volumes principle applied to queries: every org is treated as a large one, however small it is today. Where a query filters, a filter on an indexed field, such as an Id, a lookup, or an External ID, keeps it selective as the object grows (Working with Very Large SOQL Queries). A query that runs for too long is cancelled, and its transaction with it (Execution Governors and Limits). Also enforced by the ApexGuru ruleSoqlWithoutAWhereClauseOrLimitStatement(severity 2). - A query is never assumed to return exactly one record. A query assigned straight to a single sObject variable throws an exception when it finds no record, and again when it finds more than one (Using SOQL Queries That Return One Record). Assign the result to a list and check that it is not empty. Where one record or one field is all that is needed, the safe navigation operator gives null when nothing matches (
[SELECT Name FROM Account WHERE Id = :accountId]?.Name), and the null coalescing operator gives a default ([SELECT Id FROM Account WHERE Id = :accountId] ?? defaultAccount). LIMIT 1does not make that assignment safe. It prevents only the exception for more than one record: a query that finds nothing still throws. It also hides the fact that several records matched, and withoutORDER BYthe record it returns is arbitrary.- Related records are read in one relationship query, not in a second query. A child-to-parent query reads the parent's fields through the relationship (
Contact.Account.Name), and a parent-to-child query reads the children in a subquery. A query for the records related to a trigger's records is made once for all of them, filtered on the set of their Ids, and never once for each record (Running Apex within Governor Execution Limits). A subquery is not free: each one counts against a limit of its own on relationship queries, and the rows it returns count towards the rows of the transaction (Execution Governors and Limits). - Counts and totals come from an aggregate query (
COUNT(),SUM()), not from querying every record and adding the values up in a loop. Records added up or counted in Apex are reported at severity 3. Enforced by the ApexGuru ruleSoql Aggregation. A count taken withsize()on a query, which loads every record only to count it, is also reported at severity 3. Enforced byAvoidSizeOfSoql. - A result that may be large is read in a SOQL for loop. The loop hands the code one batch of records at a time and never holds the whole result in memory, which keeps the transaction within the heap limit (Running Apex within Governor Execution Limits).
- Configuration is kept in custom metadata types. A query on custom metadata records does not count against the transaction's limit on SOQL queries (Execution Governors and Limits).
- A query selects the fields the code reads, and no more. Every field selected is fetched and held in the heap, used or not. A query that selects a field the code never reads is reported at severity 4. Enforced by the ApexGuru rule
SoqlWithUnusedFields. - Records are filtered by the query, in its
WHEREclause, and not in Apex after it. A query that returns more records than the code wants, so that anifin a loop can discard the rest, reads and holds the rest for nothing. Records filtered in Apex after the query block the pull request (severity 2). Enforced by the ApexGuru ruleSoqlWithApexFilter. - Records are sorted by the query, with
ORDER BY, and not in Apex after it. A sort in Apex costs CPU time that the database would not charge. Records sorted in Apex are reported at severity 4. Enforced by the ApexGuru ruleSortingInApex. - A filter is written so that its query can use an index.
NOT INand!=cannot, and neither can aLIKEwhose pattern starts with a wildcard, so each of them reads the whole table (Large Data Volumes). A negative expression in aWHEREclause is reported at severity 4, and so is a leading wildcard.
Enforced by the ApexGuru rules SoqlWithNegativeExpressions and SoqlWithWildcardFilter. Also enforced by AvoidNonSelectiveSoqlFilter (severity 4), which reports both, in inline SOQL and in a query written as a string, on every pull request with no org.
An org that holds large data volumes raises the severity of these rules to 2, so that a non-selective filter blocks the pull request: on a large object the platform refuses a non-selective query outright, and a filter that costs nothing on the few records of a developer sandbox shows its cost only at volume. This is why such orgs must test code in a full sandbox, with production's volume of data, before it reaches production (Large Data Volumes).
- The same query is not run twice in a transaction. Two queries on the same object with the same, or nearly the same, filter are one query, and the code keeps the result, in a map keyed on the field it looks records up by. A repeated query is reported at severity 3. Enforced by the ApexGuru rule
Redundant Soql. - A query that runs often, on data that rarely changes, serves its result from Platform Cache. A query that could be served from the cache is reported at severity 4, for information; an organisation whose code is mature raises the severity. Enforced by the ApexGuru rule
SoqlWithoutPlatformCache. - A dynamic query is a query.
Database.query,Database.countQuery, andDatabase.getQueryLocator, and theWithBindsform of each, count against the limit on SOQL queries as a query written in brackets does (Execution Governors and Limits).
No rule checks the single-record standard today, and no rule can tell that two different queries could have been one: an identical query run twice, and a loop that does an aggregate query's work, are what ApexGuru reports. Reviewers check the rest, and none of them blocks a pull request. Reviewers check the SOQL for loop and custom metadata standards in the same way.
CPU Time and Heap¶
No single statement uses up CPU time or heap. Both are spent a little at a time, by everything that runs in the transaction.
- Work that the database can do is left to the database. CPU time counts the Apex of the transaction and whatever that Apex sets off, such as package code and workflow, and it does not count the time a query or a DML statement spends in the database, or the time spent waiting for a callout (Execution Governors and Limits). So records are filtered, sorted, and added up by the query, not by a loop over its result (Queries, Efficient Code).
- The heap is kept small. The more heap a transaction holds, the more CPU time the platform spends checking it, so code that runs close to both limits is likely to reach the CPU limit first (Execution Governors and Limits). A large result is read in a SOQL for loop, and the body of a callout request or response is counted in, because it is held in the heap. A value that not every transaction needs is built only when it is read (Lazy Loading).
-
A process is designed with the installed packages on its objects counted in. CPU time, heap, and the running time of the transaction are counted once for the whole transaction, whichever code uses them. A certified managed package has counts of its own for most other limits, and shares these three with the org's code. The code of any other package counts against the org's limits throughout (Execution Governors and Limits).
-
Limits.getHeapSize()is not called inside a loop. The call walks the whole heap to measure it, so a loop that asks after every item spends the CPU time it set out to save, and the answer is for the moment of the call only. The size is measured before the loop, or once after it. A call toLimits.getHeapSize()inside a loop blocks the pull request (severity 2). Enforced byAvoidGetHeapSizeInLoop. Also enforced by the ApexGuru ruleLimitsGetHeapsizeMethods(severity 2), which reports the call wherever it is made often. -
No loop is written to pass time. An empty loop that spins until a clock has moved burns CPU time against the limit, and its length depends on the load of the server that runs it, so it is a workaround that these standards do not accept (Determinism). A delay is a queueable job enqueued with a delay, or a scheduled job. An empty loop written to wait is reported at severity 3. Enforced by the ApexGuru rule
BusyLoopDelay.
No rule measures CPU time or heap. Reviewers check the first three standards, and none of them blocks a pull request.
Efficient Code¶
- Code and automation use no more platform resources than the job requires: queries, DML statements, CPU time, and heap (Efficiency).
- Staying within limits comes before speed. Secure code that stays within limits and runs smoothly is preferred over fast code that runs badly (Platform Speed).
- No loop runs that the job does not need. A loop that adds up or counts records gives way to an aggregate query, and a loop that queries related records to a relationship query (Queries). A loop inside a loop that matches two collections gives way to a map keyed on the field that joins them. Two passes over one collection become one pass where one can do both jobs. No rule can tell that a loop is unnecessary. Reviewers check this, and it does not block a pull request.
- A call that is expensive every time it runs, such as
Schema.getGlobalDescribe()orSchema.describeSObjects(), is made once before a loop, never inside it. A call inside a loop is reported at severity 3. Enforced byOperationWithHighCostInLoop. Also enforced by the Graph Engine ruleAvoidMultipleMassSchemaLookups(Developer Preview), which follows the call path, so it also reports a second such call anywhere on one path, not only in a loop (severity 2).Schema.getGlobalDescribe()itself is avoided: it describes every object in the org, whereSchema.describeSObjects()with the names of the objects needed, or an object's own describe, returns only those. A call to it blocks the pull request (severity 2). Enforced by the ApexGuru ruleSchemaGetGlobalDescribeNotEfficient. - A collection is copied in one call, not element by element. A list or a set is copied with
addAll(), and a map of records by Id is built with the map constructor,new Map<Id, Account>(accounts), or withputAll(), never withaddorputin a loop over the source. A copy made in a loop is reported at severity 3. Enforced by the ApexGuru rulesCopyingListOrSetElementsUsingAForLoopandSObjectMapInAForLoop. - A string is compared with
equalsorequalsIgnoreCase, not with==. The==operator collates the two strings by locale, which costs more on every comparison and most in a loop, and it is right only where a locale-aware comparison is meant. A string compared with==is reported at severity 4. Enforced by the ApexGuru ruleExpensiveStringComparison. - A method that uses a large share of the org's CPU time is refactored. ApexGuru, from the org's runtime data, reports a method that takes more than 2% of the observed Apex CPU time as a hotspot, and such a method blocks the pull request (severity 2) until the team has looked at it. Enforced by the ApexGuru rule
ExpensiveMethods. - A describe call whose result does not change is made once, before the loop:
getDescribe(),fields.getMap(), andfieldSets.getMap(). A call that uses a variable of the loop, such asfield.getDescribe()in a loop over an object's fields, gives a different result on each pass and stays where it is. A describe call in the body of a loop that uses no variable of the loop is reported at severity 3. Enforced byAvoidDescribeInLoops. - A regular expression that runs more than once is compiled once, as a
static final Pattern, for efficient reuse.Pattern.compile()compiles the expression into aPatternobject, andPattern.matches()compiles it again on every call (Pattern Class). Either call with a pattern written in place is reported at severity 4.String.split()and the otherStringmethods that take a regular expression are not reported. Enforced byRegexPatternsMustBeStaticFinal. - Describe results are loaded lazily. A describe call with no options loads every child relationship of the object, whether or not the code needs them; pass
SObjectDescribeOptions.DEFERRED, and each attribute loads when it is first used. An eager describe blocks the pull request (severity 2). Enforced byEagerlyLoadedDescribeSObjectResult.
Lazy Loading¶
A value that is costly to build is loaded lazily: it is built when it is first read, not when its class is loaded or constructed, and it is then kept. A transaction that never reads the value never builds it, so it spends no heap, no CPU time, and no query on it. A transaction that reads it many times builds it once.
A property does this with a check for null in its getter. The property needs a set accessor, or the getter cannot assign the value (Apex Properties). A static property keeps its value for the whole transaction.
private static Map<String, Schema.RecordTypeInfo> accountRecordTypes {
get {
if (accountRecordTypes == null) {
accountRecordTypes = Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName();
}
return accountRecordTypes;
}
set;
}
A getter that queries without this check runs its query again on every read: a property getter on every read in any class, and the getter of a Visualforce controller as often as the page calls it, which Visualforce does not limit (Defining Getter Methods). Such a getter is reported at severity 4: a query in the get accessor of any property, and in a get method with no parameters of a class whose name matches the Visualforce controller and extension patterns. A get method of a selector or a service, which runs only when code calls it, is not reported. Enforced by RequireLazyLoadingInGetters.
Sources:
- Apex Properties - Apex Developer Guide
- Bulk Triggers - Apex Developer Guide
- Defining Getter Methods - Visualforce Developer Guide
- Execution Governors and Limits - Apex Developer Guide
- Future Methods - Apex Developer Guide
- Pattern Class - Apex Reference Guide
- Platform Event Allocations - Platform Events Developer Guide
- Running Apex within Governor Execution Limits - Apex Developer Guide
- Using SOQL Queries That Return One Record - Apex Developer Guide
- Working with Very Large SOQL Queries - Apex Developer Guide
Readability¶
All code is readable and documented: it is written for the next person to read it, who may not be the person who wrote it (Readability). A reviewer can approve only what they can follow, so readable code is also what makes every other standard on this page reviewable. A name is the first thing a reader meets, so it says what it names: a variable does not have a one-letter name, apart from the loop counters and the exception variable that every Apex developer knows (Naming).
Readable code is also explicit. Where Apex would fill in a default, the code states its choice, so that a reader never has to remember the default or look up the API version that decides it. Every method and every outer class declares its access modifier (Access Modifiers). Every outer class declares its sharing, and every database operation states its access mode, as Salesforce recommends (Be Explicit Everywhere).
Code Style¶
Formatting¶
- Formatting is applied by Prettier, from the repository's Prettier configuration file, on save and again when a commit is made (Section 4). It is not a matter for review.
- Apex is indented with tabs, not spaces, because tabs are more compatible with screen readers (Accessibility).
- The opening curly bracket sits on the same line as the declaration or statement that it opens, which is how Prettier formats Apex.
- There is at most one blank line in a row. Prettier reduces two or more to one in code, so a file that breaks this was committed without Prettier; Prettier keeps the blank lines inside a block comment, and they are removed by hand. Two or more blank lines in a row are reported at severity 3. Enforced by
NoConsecutiveBlankLines. - A line should not end in a space or a tab, and no line should hold only whitespace. Prettier removes both, so a report is a sign that Prettier did not format the file; trailing whitespace breaks nothing in Salesforce. Trailing whitespace is reported at severity 5, for the developer's information. Enforced by
NoTrailingWhitespace. - The Prettier configuration counts a tab as 2 columns when it measures a line. The width a tab is shown at is still each reader's own choice, set in the editor.
- A line is at most 180 characters wide. That is a recommendation: an organisation that prefers another number sets it in its Prettier configuration. A trigger may have 200 (Version Control: Prettier).
- SOQL and SOSL keywords are written in capitals (
SELECT,FROM,WHERE,FIND,RETURNING). Apex is case-insensitive, and so are SOQL and SOSL queries: the compiler readsSELECTandselectalike (Variables: Case Sensitivity). Prettier writes them in capitals in a query between square brackets. It does not change a query that is written in a string, forDatabase.queryorSearch.query, so the developer writes the capitals there. This convention lets a reader tell them at a glance from the names of objects and fields; this also allows IDE highlighters to colour the keywords differently, increasing readability. No rule is necessary if Prettier (prettier-plugin-apex) usage is enforced. - Each variable is declared on its own line. Two declarations on one line block the pull request (severity 1). Enforced by
OneDeclarationPerLine. - Each statement is on its own line, and so is the statement inside the block of an
if,else,for,while, ordo. Apex code coverage counts lines, so a line that holds two statements, or a one-line block such asif (x > 0) { a = 5; }, counts as covered when only part of it ran. Prettier never writes these forms, so a report usually means that aprettier-ignorecomment kept the line. Such a line blocks the pull request (severity 2). Enforced byStatementsShouldBeOnSeparateLines. - Fields are declared before the first method of a class; properties may sit among them. A field declared lower down is reported at severity 4. Enforced by
FieldDeclarationsShouldBeAtStart.
Expressions¶
- A number with a meaning is a named constant (
static final), whose name labels it:TAX_RATE, not1.15(Constants). Enforced byAvoidMagicNumbers. - A default for a value that may be null is written with the null coalescing operator:
name ?? 'none', notname != null ? name : 'none'(Null Coalescing Operator). The operator reads its left-hand side once. A ternary that only supplies a default for null is reported at severity 4.obj != null ? obj.getName() : 'none'becomesobj?.getName() ?? 'none', which also gives the default whengetName()itself returns null. Enforced byPreferNullCoalescingOverTernary. - A single call or read on an object that may be null uses the safe navigation operator:
account?.close(), notif (account != null) { account.close(); }(Safe Navigation Operator). Anifthat only checks for null before one such statement is reported at severity 4. Enforced byPreferSafeNavigationOperator. -
A value that is only read is a property, not a field with a getter method:
public String name { get; private set; }, not a privatenameand agetName()that returns it (Apex Properties). A Visualforce page binds to the property as it bound to the getter. A getter method that only returns a field of its class is reported at severity 4. Enforced byAvoidTrivialPropertyGetters.
// BAD: a field and a getter that only returns it
private String name;
public String getName() {
return name;
}
// GOOD: a property
public String label { get; private set; }
- A value should be returned directly:
return calculate();, not a variable that is set and then only returned. Such a variable is reported at severity 5, for the developer's information; one kept to name the value or to set a breakpoint costs nothing. Enforced byNoUnnecessaryReturnVariables.
// BAD: a variable set only to be returned
public Integer badTotal() {
Integer total = calculate();
return total;
}
// GOOD: the value returned directly
public Integer goodTotal() {
return calculate();
}
- A negated comparison should use the opposite operator:
x != y, not!(x == y), andx >= y, not!(x < y). A negated comparison is reported at severity 3. Enforced byLogicInversionApex. - A condition should be returned directly:
return x > 0;, not anifthat returnstruein one branch andfalsein the other. Such anifis reported at severity 4. Enforced bySimplifyBooleanReturnsApex. - A ternary expression should not have
trueorfalseas a result:flag || other, notflag ? true : other. It is reported at severity 4. Enforced bySimplifiedTernaryApex. - A null check next to
instanceofshould be removed:o instanceof Account, noto != null && o instanceof Account. From API version 32.0,instanceofreturnsfalsefor null (Using the instanceof Keyword). Such a check is reported at severity 4. Enforced bySimplifyConditionalApex. - A blank string should be tested with
String.isBlank(s), which returnstruefor white space, an empty string, or null (String Class), not withs.trim().length() == 0,s.trim().isEmpty(), ors.trim() == ''. Such a test is reported at severity 4. Enforced byInefficientEmptyStringCheckApex. - An empty collection should be tested with
isEmpty():xs.isEmpty(), notxs.size() == 0, and!xs.isEmpty(), notxs.size() > 0. A comparison ofsize()with zero is reported at severity 4. Enforced byUseCollectionIsEmptyApex.
Required Formatting¶
Apex has little to no required formatting. It is case-insensitive, and it is largely agnostic about line breaks, so the platform asks for almost nothing about how code is laid out. Every formatting standard for Apex is a choice, and the standards are in Formatting.
Documentation¶
Comments follow the General: Comments standards: a comment says why, an exception to a blocking rule is explained in the code, unfinished work is not kept in a comment, and no suppression goes unapproved. These are particular to Apex.
TODO,TO-DO,FIXME,FIX-ME, orHACKin a//or/* */comment of a class or a trigger, as the first word of a line (after any*in a block comment) or with a colon after it, is reported at severity 5. Enforced byNoUnresolvedTodosApex.- A block comment says something. An empty one, such as a comment template that was never filled in, is reported at severity 3. Enforced by
NoEmptyBlockComments. - A
@SuppressWarningsannotation, a// NOPMDcomment, or acode-analyzer-suppressmarker blocks the pull request (severity 2) until the violation is fixed or the owning team approves the suppression. Enforced byNoUnapprovedSuppression.
ApexDoc¶
- Every class, method, and property carries an ApexDoc comment, whatever its access modifier. The code is documented for the next developer, not only for the caller, so a private member is documented as a public one is. Overrides and test classes are exempt. A class, an interface, a constructor, or a method with no comment is reported at severity 4. A property, an enum, or a trigger is not reported, and documenting it is still encouraged. Enforced by
ApexDoc. - An ApexDoc comment starts with
/**, ends with*/, and immediately precedes the declaration that it documents, with no other code or comment between them. Each line after the first starts with an asterisk (ApexDoc Comment Structure and Tags). A/* */or//comment is not ApexDoc, so a class or a method with only that is reported as having no comment. Enforced byApexDoc. No rule checks for something between the comment and its declaration, or for the asterisks. Reviewers do. - Every tag in an ApexDoc comment is filled in, and what it says is true of the code. A tag with no text, a placeholder, and a default that a tool wrote look like documentation and tell the reader nothing. When the code changes, its comment changes with it. The Apex compiler does not check the syntax of an ApexDoc comment, or whether it matches the code (ApexDoc Comment Structure and Tags), so the rules check what they can: that a tag is there, that it matches the signature, and that a description has real text. Whether any other tag has text, and whether the text is true, a reviewer checks in the diff, where the changed code and its comment appear together.
- A comment has a description that says what the thing is for. The description is the text at the top of the comment, before any tag, as Salesforce writes it, or the text after a
@descriptiontag, which is not in Salesforce's list of tags, and both are accepted. A comment with no description is reported at severity 4. Enforced byApexDoc. The description says something: its text is at least 10 characters, counted from its first letter and including spaces, across the lines it wraps onto, so a placeholder such asTBDis reported at severity 4. Enforced byApexDocDescriptionLength. - A description should open with a one-sentence summary that ends with a full stop, because documentation generators often use that sentence in summary tables and indexes. Any further context follows it (ApexDoc Comment Structure and Tags). No rule checks this. Reviewers do.
- A block tag, such as
@param, follows the description and starts on a new line. An inline tag, such as{@link}, is in curly braces inside the description or the text of a block tag (ApexDoc Comment Structure and Tags). No rule checks this. Reviewers do. - No colon comes between a tag and its text:
@author Jackie Lee, never@author : Jackie Lee. Salesforce puts the text of a block tag after the name of the tag (ApexDoc Comment Structure and Tags), so a colon becomes the first character of the text, and generated documentation shows it. The header of the SFDoc extension writes a colon after every tag. A colon after any tag, block or inline, with or without whitespace before it, blocks the pull request (severity 2). PMD does not keep the comment of a top-level enum, or of an inner enum that no other member follows, so the rule does not read it. Enforced byNoColonAfterApexDocTag. - A class's ApexDoc comment has an
@authortag, so that a reader knows whom to ask about the class. Salesforce recommends@authoron a class (Document Apex Constructs and Features); these standards make it required. On an interface or an enum the tag is optional, and a comment may have more than one (ApexDoc Comment Structure and Tags). A test class is exempt, as it is from theApexDocrule. No rule checks for the@authortag: reviewers look for it. - The author is a person or a team, never a default that a tool wrote. The Salesforce Documenter (SFDoc) extension for VS Code, which is no longer maintained, fills in
ChangeMeIn@UserSettingsUnder.SFDocas the author and as the last user to modify the file, in Apex, Visualforce, Aura, and Lightning Web Component files, until its user name setting is changed. The default looks like an author and names nobody. That string in any of those files blocks the pull request (severity 2). Enforced byNoSfdocDefaultAuthor. - An ApexDoc comment has no
@last modifiedtag, such as@last modified onor@last modified by. No such tag is an ApexDoc tag (ApexDoc Comment Structure and Tags): the SFDoc extension writes both in its header. Git records who changed a file and when, and these tags become wrong after the next change that does not update them. Such a tag, in any case and with or without a colon, blocks the pull request (severity 2). PMD does not keep the comment of a top-level enum, or of an inner enum that no other member follows, so the rule does not read it. Enforced byNoLastModifiedApexDocTag. - A method's or a constructor's comment names each parameter with
@param, with the parameter's name, in the order of the signature. Salesforce says that each parameter must have this tag (Document Apex Constructs and Features). The text should say what the parameter is for and what it expects, such as "Cannot be null". A method or a constructor with a parameter that has no@param, or with a@paramthat is misnamed or out of order, is reported at severity 4. Enforced byApexDoc. - A method's comment describes the value that the method returns with
@return. A method that returns a value has one, and avoidmethod or a constructor has none. The text should say what is returned, including when the value is null or a list is empty (Document Apex Constructs and Features). A missing or an unexpected@returnis reported at severity 4. Enforced byApexDoc. - A method's or a constructor's ApexDoc comment names each exception that it throws with
@throws, so that a caller knows what to catch. The text should give the conditions that cause the exception (Document Apex Constructs and Features). A method or a constructor with an ApexDoc comment that throws an exception it does not name is reported at severity 4. Enforced byExceptionDocumentationRequired. - A class's comment should explain its sharing model when the reason is not obvious, as for a class that is
without sharing(Document Apex Constructs and Features). No rule checks this. Reviewers do. - An interface's comment should state the contract that the interface defines, and each of its methods should say what an implementing class is expected to do (Document Apex Constructs and Features). No rule checks this. Reviewers do.
- An enum's comment should say what the enum represents. A constant whose name does not explain itself is described in that comment, or in a block comment directly above the constant. The implicit methods, such as
values()andname(), need no documentation (Document Apex Constructs and Features). No rule checks this. Reviewers do. - A comment on a property, or on a member variable that is part of a class's public API, should say what it is for, its data type if that is unclear, and any usage note, such as its default value or that it is read-only after initialisation (Document Apex Constructs and Features). No rule checks this. Reviewers do.
- A trigger's comment is optional, because ApexDoc has no specification of its own for a trigger: the business logic belongs in a separate class, and that class is documented. A trigger's comment may carry tags such as
@sinceand@see(Document Apex Constructs and Features). - Every other tag is optional. The one exception is
@deprecatedon an element with the@Deprecatedannotation, below. - The
@sincetag, the version or the date that the thing was introduced, is optional. Salesforce recommends it on a class. - The
@versiontag, the version of a class, an interface, or an enum, is optional. Salesforce recommends it on a class. - The
@grouptag, the group that the thing belongs to in generated documentation, is optional. Salesforce recommends it on a class. - The
@seetag, a reference to another class, method, etc., is optional. Salesforce recommends it on a class. Multiple@seetags are allowed. The reference is a class or an interface with an optional member, a quoted text string, or an HTML link (ApexDoc Comment Structure and Tags). - The
@exampletag, a usage example, is optional. The example is formatted as code when it is in a{@code}tag (ApexDoc Comment Structure and Tags). - The
@deprecatedtag marks a thing as deprecated and gives the reason and the alternative. Every element with the@Deprecatedannotation has one, as Salesforce says (Document Apex Constructs and Features), so that a developer who sees the element knows what to use instead. On any other element the tag is optional. A deprecated element with no comment, with no@deprecatedtag, or with an empty tag is reported at severity 4. PMD's syntax tree has no comment for an enum, so an enum is not reported. Enforced byDeprecatedDocumentationRequired. - The
{@code}inline tag, which formats text as inline code, is optional (ApexDoc Comment Structure and Tags). - The
{@link}inline tag, an inline link to another element, is optional. Its reference takes the same forms as that of the@seetag (ApexDoc Comment Structure and Tags). - The
{@literal}inline tag, which shows text as written with no HTML interpreted, such as{@literal <script>}, is optional (ApexDoc Comment Structure and Tags). - The
{@hidden}inline tag, which holds what should stay out of generated documentation, is optional. A developer can use it for a note to maintainers, as in Salesforce's example of the tag, although Salesforce's table of tags says that it prevents the element from appearing in generated documentation (ApexDoc Comment Structure and Tags). Generators differ: ApexDocs leaves out the whole element, and IcApexDoc leaves out only the tag's text. So a note on an element that should stay in the documentation is tested with the organisation's generator first. The note should not contain@paramor@return, becauseApexDocreads either one in the tag's text as a real tag and reports it. - A comment on an element with an annotation should say what the annotation means for the element's behaviour or use, as this table sets out (Document Apex Constructs and Features). No rule checks this. Reviewers do.
| Annotation | The Comment Should Say |
|---|---|
@AuraEnabled |
Whether it has cacheable=true, and what that setting implies. |
@Future |
What running asynchronously implies, such as a separate transaction, governor limits, and callouts. |
@InvocableMethod |
What the element does as an invocable action, with @param and @return for its input and output. |
@InvocableVariable |
The variable's role, its data type, and any constraints or expectations for the action. |
@IsTest |
The scenario or the functionality tested, and the reason for seeAllData or onInstall when either is true. |
@NamespaceAccessible |
Whether the annotation restricts exposure compared with global access, or the reason that this level of access is right. |
@ReadOnly |
Whether the element is used for performance with large query sets or for specific APIs. |
@RestResource, and the @Http annotations on its methods |
The resource as a whole, and the role of each method. |
@SuppressWarnings |
The warning that the third-party tool suppresses, and the reason when it is not self-evident. |
@TestSetup |
The common test data that the method creates. |
@TestVisible |
The reason for the annotation, such as "Visibility modified for testing purposes." |
Standards Not Adopted¶
Why Only Some Are Listed
Not every standard that was left out is listed here, only those that merit an explanation. Standards Not Adopted on the Code Standards page says why.
Standards that were researched and left out on purpose, each with the reason.
- A builder's calls do not have to be chained on one variable. The pattern is rare in Apex, and whether to chain calls is a matter of style; Prettier's line breaks keep either form readable.
- These standards take no position on whether strings are joined with
String.join()or with the+operator. - No rule checks for indentation that mixes tabs and spaces. Prettier sets the indentation of every line, and Code Analyzer's
NoMixedIndentationrule reports every continuation line of an ApexDoc comment in a tab-indented class, because that line starts with a tab and then a space. An organisation that indents Apex with tabs disables the rule, as the samplecode-analyzer.ymldoes.
Sources:
- Apex Properties - Apex Developer Guide
- ApexDoc Comment Structure and Tags - Apex Developer Guide
- cesarParra/apexdocs: Generate documentation for your Salesforce Apex Classes.
- Document Apex Constructs and Features - Apex Developer Guide
- Null Coalescing Operator - Apex Developer Guide
- Safe Navigation Operator - Apex Developer Guide
- SCWells72/IcApexDoc: An API documentation generator for Salesforce Apex types and triggers and custom SObjects and fields.
- String Class - Apex Reference Guide
- Using the instanceof Keyword - Apex Developer Guide
- Variables - Apex Developer Guide
Naming¶
The naming standards for Apex are on the General: Naming page, with the naming standards of the other languages.
Complexity and Unused Code¶
- A method is simple enough to read in one pass. High cyclomatic or cognitive complexity and deeply nested
ifstatements are each reported at severity 3, at PMD's default thresholds. An organisation may change these thresholds in each rule's properties. Enforced byCyclomaticComplexity,CognitiveComplexity, andAvoidDeeplyNestedIfStmts. - Nested
ifstatements with nothing else in the outer one are combined into oneifwith&&:if (a && b) { ... }, notif (a) { if (b) { ... } }. This applies only when neitherifhas anelse. It is reported at severity 4, and a reviewer may accept two conditions that read better apart. Enforced byCombineNestedIfStatements.
// BAD: an if that holds only another if
if (account.NumberOfEmployees > minEmployees) {
if (account.AnnualRevenue > minRevenue) {
notifyOwner(account);
}
}
// GOOD: one if with &&
if (account.NumberOfEmployees > minEmployees && account.AnnualRevenue > minRevenue) {
notifyOwner(account);
}
- A method has at most 40 statements, and a class at most 500, counted as PMD counts non-commenting source statements (NCSS): comments and blank lines do not count. A longer method or class is split. One over its limit is reported at severity 4, at PMD's default thresholds. An organisation may change these limits with the rule's
methodReportLevelandclassReportLevelproperties in its ruleset. Enforced byNcssCount. - A class with too many fields is reported at severity 4, as a sign that related fields may belong together in a type of their own: an inner class, or an object of the data model, where several fields describe one thing. Enforced by
TooManyFields. - Code that is never used is removed: a local variable that is never read and a method that is never called, each reported at severity 3. A method that is never called does not block the pull request, but it appears in every scan, and the developer may be asked to remove it, or to comment it out until it is used. A utility class built for future use is the exception. Enforced by
UnusedLocalVariableandUnusedMethod.UnusedMethodreports only in a scan of the whole repository, not in a pull request scan of the delta package (Appendix E).
Error Prevention¶
Braces¶
The body of every if, else, for, and while is wrapped in braces, even when it is one line. A missing pair of braces blocks the pull request (severity 2). Enforced by IfStmtsMustUseBraces, IfElseStmtsMustUseBraces, ForLoopsMustUseBraces, and WhileLoopsMustUseBraces.
Apex does not require braces around a one-line body, and whether to write them is a long-running argument: the Linux kernel's coding style leaves them off a single statement, while Google's Java style and the SEI CERT C standard require them. These standards require them because of the next change, not the current one. When someone later adds a second line under an unbraced if and indents it to match, the new line looks conditional and is not. Apex reads braces, not indentation, so it runs every time:
if (opportunity.IsClosed)
notifyOwner(opportunity);
archiveLineItems(opportunity); // runs for every opportunity, closed or not
The best-known case is Apple's "goto fail" bug of 2014 (CVE-2014-1266): a duplicated goto fail; line under an unbraced if ran unconditionally, and iOS and OS X skipped the check of a server's signature on secure connections (Adam Langley's analysis). The CERT standard cites it as a defect that braces would probably have prevented. Braces also settle which if an else belongs to. A formatter does not add braces, so a rule requires them.
Switch Statements¶
- Use
switchinstead ofif / else iffor a supported type: a string, an integer, an enum, or the type of an sObject. The switch names every value it handles in one place, and the compiler checks that an enum value or a type is spelled correctly, where anifchain compares strings that it cannot check. A chain that compares one value with three or more values that awhenclause can hold is reported at severity 4; theminConditionsproperty sets the number. The change is not always word for word:==is case-insensitive, and awhenvalue of typeStringis case-sensitive (Switch Statements), so a chain that relies on that changes its behaviour as a switch. Enforced byPreferSwitchOverIfElseChains. - Every
switchstatement has awhen elseblock, which says what happens for a value the switch does not name. Without it, a new picklist value or a new enum value arrives and falls through the switch with no sign that it did; with it, the code throws, logs, or does nothing on purpose, and a reader sees which. Awhen elsethat does nothing holds a comment that says so. Aswitchwith nowhen elseis reported at severity 4. Enforced bySwitchWithoutWhenElse. - A
switchstatement should not hold anotherswitchstatement. The inner one moves to a method of its own, whose name says what it decides. A nestedswitchis reported at severity 3. Enforced byAvoidNestedSwitchStmts.
Empty Blocks¶
An empty block looks as if it does something, and does nothing. A reader cannot tell whether it was left empty on purpose or whether the code that belonged in it was lost. An empty catch block is worse: it swallows the exception, the transaction goes on as if nothing failed, and nobody learns of the failure (Exception Handling).
- No
try,catch, orfinallyblock is empty. A caught exception is handled, logged, or thrown again, and a comment does not make an emptycatchblock acceptable. An emptytryorfinallyblock protects nothing. An emptytry,catch, orfinallyblock blocks the pull request (severity 2). Enforced byEmptyCatchBlockandEmptyTryOrFinallyBlock. - No
ifblock is empty. An emptyiftests a condition and ignores the result. An emptyifblock blocks the pull request (severity 2). Enforced byEmptyIfStmt. - No
whileblock is empty. An emptywhiledoes its work, if any, in its condition, where a reader does not look for it. An emptywhileblock blocks the pull request (severity 2). Enforced byEmptyWhileStmt. - No method and no other block is empty. An empty method or block is removed, or it holds the code it was written for. Three empty blocks are allowed: a private constructor with no parameters, which stops a constants class or a utility class from being created; a virtual method of a base class, which a subclass overrides where it needs to; and a
when elsethat does nothing, which holds a comment that says so (Switch Statements). Any other empty method or block blocks the pull request (severity 2). This rule also reports an emptyif,while,try, orfinallyblock, so such a block is reported twice. Enforced byEmptyStatementBlock. - No instance initializer block, a pair of braces directly in a class, should be empty. An empty one is reported at severity 4. Enforced by
EmptyInitializer.
Constructors¶
A Flow, an Apex action, or a REST API call that runs an invocable action creates an instance of each class that holds the action's parameters, through the class's no-argument constructor. From API version 66.0, such a class must have a visible no-argument constructor: public in a class outside a package, and global in a global class (InvocableVariable Annotation). Salesforce first offered the requirement as a release update, in Summer '24, and an Apex action that refers to such a class with no visible no-argument constructor results in errors (Enforcing No-Argument Constructor on Apex Classes Used for Invocable Action Parameters). A class with no constructor has the default one. A class with an explicit constructor has no default constructor, and the compiler accepts the class without a no-argument constructor, so the fault shows only when a Flow uses the class.
- A class with an
@InvocableVariablemember and an explicit constructor also has a visible no-argument constructor. At API version 66.0 and later, a class without one blocks the pull request (severity 1). Enforced byRequireInvocableNoArgConstructor, which reads the class's API version from its-meta.xmlfile. At any API version, and where the version cannot be read, such a class blocks the pull request at severity 2. Enforced byInvocableClassNoArgConstructor. So a class at API version 66.0 or later is reported twice, on the same line, at severity 1 and at severity 2. The two reports describe one fault, and one no-argument constructor clears both, as one fix clears the two reports of an emptyifblock (Empty Blocks). - Only a constructor has the name of its class. A method with the class's name, in any case, reads as a constructor, and a reader takes it for one. Such a method is reported at severity 3. Enforced by
MethodWithSameNameAsEnclosingClass. - A class whose constructors are all private should have a public or global static member, such as a factory method or a constant, or no code can use it. A top-level class with neither is reported at severity 3. Enforced by
MissingStaticMethodInNonInstantiatableClassApex.
Abstract Classes¶
- An abstract class declares at least one method or constructor. It exists to be completed by a subclass, and an abstract class with only fields is a data holder that no code can create. One that extends no class and declares neither blocks the pull request (severity 1). Enforced by
AbstractClassWithoutAnyMethodApex.
Equality¶
- A class that overrides
equals(Object)also overrideshashCode(), and a class that overrideshashCode()also overridesequals(Object). A Map or a Set compares a key of a custom type with both methods (Using Custom Types in Map Keys and Sets), so a class with only one of them can put two equal objects in a Set, or fail to find a Map key that it holds. A class with only one of the two blocks the pull request (severity 2). Enforced byOverrideBothEqualsAndHashcode. - A
Mapdoes not use an interface as its key type where an abstract class implements that interface and definesequalsorhashCode. Methods such ascontainsKeyandgetdo not call that class'sequalsorhashCodefor such a key, so the map can hold two equal keys, or fail to find a key that it holds. Such aMap, as a field, a variable, or a parameter, is reported at severity 3. Enforced byAvoidInterfaceAsMapKey, which reports only when the environment variablePMD_APEX_ROOT_DIRECTORYis set (Appendix E).
Null Checks and Conditions¶
- A null check protects the expression after it.
x != null || x.size() > 0andx == null && x.isEmpty()readxexactly when it is null, so they throw the error that the check was written to prevent. Such a check blocks the pull request (severity 2). Enforced byBrokenNullCheckApex. - A null check before
equals()should check the object whose method is called:a != null && a.equals(b), nota != null && b.equals(a), wherebcan still be null. Such a check is reported at severity 4. Enforced byUnusedNullCheckInEqualsApex. - A comparison with a string literal should call the method on the literal:
'abc'.equals(s), nots.equals('abc'), which throws whensis null. The==operator does not throw, and is not reported. The other form is reported at severity 3. Enforced byPositionLiteralsFirstInComparisons. - A method that returns a
List, aSet, aMap, or an array should return an empty one, notnull, so that every caller can loop over the result without a null check.return nullin such a method is reported at severity 3. Enforced byReturnEmptyArrayRatherThanNull. - Each branch of an
if/else ifchain should test a different condition. A condition that repeats an earlier one in the same chain can never be true when it is reached, so its branch never runs. It is reported at severity 3. Enforced byFixDuplicateConditions. - An
ifcondition should not betrue,false, or a comparison of two literals, which is always the same: the block always runs or never runs. Code that was switched off withif (false)is removed. Such a condition is reported at severity 3. A constant that holds such a value is not seen. Enforced byUnconditionalIfStatementApex. - An assignment should change something.
count = countandthis.name = this.nameassign a value to itself and do nothing, usually in place ofthis.name = name. Such an assignment is reported at severity 3. Enforced byIdempotentOperationsApex.
Annotations¶
Every annotation is one that Apex defines (Annotations). For legacy reasons, Apex accepts an annotation that it does not define, and such an annotation does nothing. A later release can define the same name, and the code then means something else or no longer compiles. An annotation that Apex does not define is reported at severity 3. Enforced by AvoidNonExistentAnnotations.
Constants¶
- A value that the code uses is declared once, as a constant (
static final), and reused, so that a change is made in one place and no copy is left behind. A number other than 0, 1, and -1 written in the code is reported at severity 4; theallowedNumbersproperty lists the numbers that may be written out. A list index, a number in a loop condition, awhenvalue, and a number in a test class are not reported. Enforced byAvoidMagicNumbers.
Sources:
- Annotations - Apex Developer Guide
- Enforcing No-Argument Constructor on Apex Classes Used for Invocable Action… - Salesforce Help
- EXP19-C. Use braces for the body of an if, for, or while statement - CERT Secure Coding
- Google Java Style Guide
- ImperialViolet - Apple's SSL/TLS bug
- InvocableVariable Annotation - Apex Developer Guide
- Linux kernel coding style — The Linux Kernel documentation
- NVD - CVE-2014-1266
- Switch Statements - Apex Developer Guide
- Using Custom Types in Map Keys and Sets - Apex Developer Guide
API Version¶
The API version of a class or trigger decides how the platform runs it, so the same code can behave differently at two versions. Two examples:
- At API 67.0 and later, a database operation that states no access mode runs in user mode, and a class with no sharing declaration runs with sharing (What Changed at API Version 67.0).
- At API 61.0 and later, a method in a subclass no longer overrides a private method of the same signature in its superclass, so which of the two runs depends on the version (Apex Versioned Behavior Changes).
A class or trigger at an old API version blocks the pull request (severity 2). Enforced by BumpApiVersion and AvoidOldSalesforceApiVersions. The standard is in Metadata Standards: API Version.
One case has a rule of its own, because the second example above changes what a class does. An abstract or virtual class that has a private method is at API 61.0 or later (Update the API Version of Abstract or Virtual Classes to Prevent Overriding Private Methods). Below 61.0, a method of the same signature in a subclass overrides the private method, which no other version allows, so the class is raised to the version where the platform treats the method as private, as the rest of the org does. An abstract or virtual class with a private method and an API version below 61.0 blocks the pull request (severity 2). Enforced by MinVersionForAbstractVirtualClassesWithPrivateMethod.
Sources:
Sharing and Access Mode¶
These standards apply the principle of least privilege (PLP): code reaches the records, and reads and changes the fields, that the user who runs it may. Apex data security rests on two separate controls, and the standards treat them separately. Never describe Apex as "running in user mode" or "running in system mode": sharing belongs to a class or trigger, and access mode belongs to each database operation.
- Sharing decides which records the code can reach. It is declared on each class with
with sharing,without sharing, orinherited sharing. Triggers cannot declare sharing and always run without sharing, at every API version (Triggers). - Access mode decides whether a database operation enforces the running user's permissions. It is set on each SOQL query, SOSL search, DML statement, and
DatabaseorSearchmethod call. In user mode, the operation enforces the user's object permissions, field-level security, and sharing rules; in system mode, it enforces none of them.
What Changed at API Version 67.0¶
API version 67.0 (Summer '26) changed the defaults for both controls.
The defaults at API 66.0 and at API 67.0
Each class and trigger keeps the behaviour of the API version it is saved at, so one org can hold both sets of defaults at once, and the same code behaves differently at 66.0 and at 67.0.
| Behaviour | API 66.0 and earlier | API 67.0 and later |
|---|---|---|
| Database operation with no access mode stated | System mode: object permissions and field-level security are not enforced | User mode: object permissions, field-level security, and sharing are enforced |
| Class with no sharing declaration | Depends on its inheritance chain and its caller; usually without sharing | With sharing, or the sharing of its parent class where it extends one |
WITH SECURITY_ENFORCED in SOQL |
Compiles | Does not compile |
Source: the Apex Developer Guide, Versioned Behavior Changes (Version 68.0, page 800), and its page on the sharing keywords, Omitted Sharing.
Be Explicit Everywhere¶
At API 67.0 and later the defaults are the secure ones, so a class that declares no sharing and a database operation that states no access mode are no longer unsafe in themselves. These standards require both to be written all the same, in all code, production and test, at every API version:
- Every outer class declares its sharing.
- Every database operation states its access mode.
A missing declaration or a missing access mode blocks the pull request, at severity 2 at the least. Sharing and Access Mode Standards below name each rule and its severity.
An inner class is the one exception. It takes the sharing of the class that calls it, so the caller's declaration is the one that matters. The declaration is recommended on an inner class as well, and a missing one is reported without blocking.
The reason is the reader: a reviewer, a later developer, or an AI coding agent. Code that leaves these out can be understood only by someone who knows the API version of the class, which is stored in another file, and what the defaults were at that version. For sharing there is more to know. How a class with no declaration runs depends on whether it extends another class, then on its API version, and at 66.0 and earlier on whether it is an entry point, and of what kind. With the declaration written, the answer is in the class.
Salesforce recommends the same. The Apex Developer Guide recommends an explicit sharing declaration on every class that queries or changes data, because it shows intent and makes the code easier to maintain, and an explicit access mode on every database operation in a trigger and its handler classes, to remove ambiguity (Use the with sharing, without sharing, and inherited sharing Keywords). These standards ask for a little more than the guide does: the sharing declaration on every outer class, whether or not it touches data, and the access mode on every database operation, in any class.
Explicit code also keeps its behaviour when its API version changes (Raising a Class to API 67.0), making the API version irrelevant with regards to sharing and access.
Sharing and Access Mode Standards¶
These standards apply to every class and trigger, at any API version.
- Every outer class declares its sharing, a test class included:
with sharingby default,inherited sharingfor utility classes that should follow their caller. A missing declaration blocks the pull request (severity 1). Enforced byRequireExplicitSharing. - An inner class should declare its sharing too. With no declaration it takes the sharing of the class that calls it, so the declaration is not needed for the class to run as intended; it is recommended so that a reader does not have to trace the callers. A missing declaration is reported and does not block (severity 3). An exception class is exempt, and so is every inner class of a test class, and of a class that extends another class or implements an interface, such as a queueable, batchable, or schedulable class. Enforced by
RequireInnerClassSharing. without sharingis a deliberate, justified exception: it blocks the pull request (severity 2). Enforced byDoNotUseWithoutSharing.- Every database operation runs under a sharing declaration that the code states, along the whole path that reaches it. The {N} rules above read each class on its own; the Graph Engine follows the call path from an entry point, such as an
@AuraEnabledmethod, to each query and DML statement, and reports one that runs in awithout sharingclass, or in a class that inherits its sharing with no declaration of its own. A database operation on such a path blocks the pull request (severity 2). Enforced by the Graph Engine ruleDatabaseOperationsMustUseWithSharing(Developer Preview). - Every database operation states its access mode: inline SOQL and SOSL,
DatabaseandSearchmethods, and DML statements, in classes, triggers, and test classes. A missing access mode blocks the pull request (severity 2). Enforced by the Java ruleRequireAccessMode. - User mode is the default. A system-mode operation is a deliberate exception: a comment in the code should give the reason (Documentation), and it blocks the pull request (severity 2) until a reviewer accepts it, in a test class as anywhere else (Sharing and Access Mode in Test Classes). Enforced by the Java rule
FlagSystemMode. Also enforced by the Graph Engine ruleApexFlsViolation(Developer Preview), which follows the call path from an entry point, such as an@AuraEnabledmethod, to each query and DML statement and reports one with no object or field permission check on the path: user mode, a describe-result check, orSecurity.stripInaccessible. It reaches what the {N} Java rules cannot, a dynamic query built from a string and a system-mode operation in a helper that the entry point calls, and it names the entry point. A reviewed system-mode exception shows both rules' reports on the one line, and the owning team accepts both together. The exception needs no check of its own to be accepted: the reviewer approves it on its reason, and some operations have to run in system mode. A permission check before the operation, through the describe result, orSecurity.stripInaccessibleon what it returns, is a legitimate way to run a system-mode operation all the same, and one of them makes the exception easier to approve;stripInaccessiblein particular does away with the need to explain why system mode was necessary, and the Graph Engine rule accepts either as a check. WITH SECURITY_ENFORCEDis not accepted: useWITH USER_MODE. It checks theWHEREclause as well as the selected fields, handles polymorphic fields such asOwner, and reports every field-level security violation instead of only the first.WITH SECURITY_ENFORCEDin a query, or in a string that holds part of a query, blocks the pull request (severity 2). Enforced byNoSecurityEnforced.- New classes and triggers are saved at the current API version. An existing class is not required to be raised to the current version whenever it changes, because that can mean a large refactoring; when it is raised, follow Raising a Class to API 67.0 below.
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds WITH USER_MODE];
update as user accounts;
Integer total = Database.countQuery('SELECT COUNT() FROM Account', AccessLevel.USER_MODE);
Each of these three operations states its access mode, so it runs in user mode at any API version, and a reader can see that without knowing the version.
Existing Code¶
Both access mode rules run at severity 2, so a violation blocks the pull request. Most existing code predates these standards. Code Analyzer scans each changed file whole, so the first pull request that touches an old class reports operations that the pull request did not write.
Ideally, the gate uses a tool that reports only the violations on the lines that a pull request changes, so that work on the change can continue. Gearset and CodeScan can tell new violations from old ones (Choosing a Static Analysis Tool). In parallel, a separate effort reduces technical debt: it takes the violations that full code scans find (A.1.7, On Demand Full Repository Scan) and fixes them over time.
In a class at API 66.0 or earlier the unstated operations run in system mode, and stating the mode of each one is the work that makes the class safe to change and, later, to raise to API 67.0. In a class already at API 67.0 or later they run in user mode, and writing that down changes nothing but what the reader can see.
Triggers¶
A trigger always runs without sharing and cannot declare otherwise, at every API version. Since the Summer '26 release that holds in every case, for a trigger at any API version: the change came with the release, not with an API version. Before, it depended on what fired the trigger: a trigger fired by a plain record update ran without sharing, and a nested trigger, one fired by DML inside a class declared with sharing, ran with sharing, as that class did. Now the nested trigger runs without sharing too (Apex Triggers Always Run in a "without sharing" Context).
A query or a DML statement written directly in a trigger body follows the access mode default of the trigger's API version. At 67.0 and later that default is user mode, which reapplies the running user's sharing and so overrides the trigger's own context; at 66.0 and earlier it is system mode, and a trigger saved at 66.0 or earlier keeps that default after Summer '26, because the release changed the sharing context and not the access mode default. A system-mode operation in a trigger body, stated or defaulted, sees every record, because nothing in a trigger reapplies sharing, where the same operation in a with sharing handler class would not.
A change of this kind is the reason the standard asks for the access mode on every database operation (Be Explicit Everywhere): the code then says what it does, a reader does not have to know what fired the trigger or which release the org is on, and the security of a transaction does not move with a release.
The standard removes the ambiguity: the Trigger Actions Framework requires a one-line trigger body that hands off to the framework, with no database operation of its own (Appendix F.2 ends with a sample rule that enforces the one line). All data access lives in the handler classes, which declare their sharing and follow the access mode standard like any other class, so that no system-mode operation runs in the one context that sees every record by default. A database operation left in a trigger body states its access mode like any other, and a system-mode one blocks the pull request as it does anywhere else (Sharing and Access Mode Standards).
The change reaches the handler classes as well. A class declared inherited sharing takes the sharing of its caller. When the caller is a trigger, that is now without sharing in every case, so a handler declared inherited sharing runs without sharing whenever a trigger calls it, including a trigger that used to run with sharing because a with sharing class fired it. That is one more reason for a trigger framework, under which every handler runs the same way whatever fired the trigger, and for with sharing as the default declaration on a handler class (Sharing and Access Mode Standards). An organisation that has handler classes declared inherited sharing reviews each of them for what it does with that access: every system-mode operation in such a class now sees every record, and a user-mode operation reapplies the running user's sharing whatever the class declares.
A trigger holds no logic. Its body hands off to the trigger framework, and every decision, loop, query, and DML statement is in a handler class, where it can be called, tested, and reviewed like any other code. Logic in a trigger is a bad design under any framework: it cannot be called or tested by itself, and it runs outside the order and the bypasses that the framework gives every trigger action. The Trigger Actions Framework, the standard framework for every object, prohibits it (Automation Framework). A trigger that holds an if, a switch, a loop, or a try block blocks the pull request (severity 2). Enforced by AvoidLogicInTrigger.
That rule does not report a query, a DML statement, or a second call written as a plain statement in a trigger body. The sample rule RequireFrameworkTriggerBody reports all of them: it reports any trigger whose body is not exactly new MetadataTriggerHandler().run();, the one line that the Trigger Actions Framework requires, at severity 1 (Appendix F.2). It is in a file of its own, outside the reference rulesets, because an organisation that uses another framework would fail it on every trigger. An organisation that uses the Trigger Actions Framework adds that file to its rulesets.
A trigger declares no methods of its own, for the same reason: a method written in a trigger is logic in the trigger, and nothing outside the trigger can call or test it. It also breaks the one-line trigger that a trigger framework requires. A method declared in a trigger blocks the pull request (severity 2). Enforced by NoMethodsInTriggers.
Each object has at most one trigger, which hands off to the trigger framework (Automation: Orchestration). When an object has two triggers for the same event, the order in which they run is not guaranteed (Triggers and Order of Execution), and the framework no longer controls the order of the object's automations. A second trigger on an object blocks the pull request (severity 1): the rule reads every trigger in the project, in every package directory, and reports each trigger on an object that has another. A trigger whose name contains two underscores, such as ns__AccountTrigger, is from a managed package, and it is neither reported nor counted. Enforced by MultipleTriggersOnObject.
Raising a Class to API 67.0¶
What raising a class changes, and the five steps to take first
Raising an existing class to 67.0 can change what it does without changing a line of its code. Queries and DML that ran in system mode start enforcing the running user's permissions, so a user who lacks access to a field or object gets an exception where the code used to succeed, and a query may return fewer records once sharing applies. An undeclared class may also change from without sharing to with sharing.
Make the behaviour explicit before changing the version:
- Declare the class's sharing as it behaves today.
- State the access mode on every database operation, which
RequireAccessModealready requires. Where the class relies on system mode today, write it explicitly;FlagSystemModethen lists every such operation for review. - Review each system-mode operation and change it to user mode unless it has a real justification.
- Run the tests as a user with the minimum permissions the feature needs, using
System.runAs(Running As a User), and confirm the class still works. - Raise the API version. With every operation explicit, the version change no longer changes behaviour.
A trigger is raised the same way. A database operation in its body with no stated access mode runs in system mode at 66.0 and in user mode at 67.0, where it starts enforcing the running user's permissions and sharing, so steps 2 to 5 apply to a trigger body as to a class. A one-line trigger under the Trigger Actions Framework has no such operation, and its version changes nothing.
Sources:
Security¶
Standards that keep data and credentials from leaking out of Apex, under the principle of least privilege (PLP). Sharing and access mode have a section of their own, above, and what a class opens to other code is under Access Modifiers.
Access Modifiers¶
- Every method that is not a test method declares its access modifier: instance and static methods, constructors, and the helper and setup methods of a test class. A test method needs none. Apex treats a method with no modifier as private, so the modifier changes no behaviour; it is there so that a reader never has to remember the default. A missing modifier blocks the pull request (severity 2). Enforced by
DeclareAccessOnMethods. - Every outer class declares its access modifier. A missing modifier blocks the pull request (severity 1). Enforced by
RequireOuterClassAccess. - Every method, variable, property, and inner class is
privateunless code outside its class has to use it.protected,public, andglobaleach open a member to more code (Access Modifiers), and each is chosen only when code outside the class calls the member. This is the principle of least privilege (PLP) applied to code: a member that nothing outside can reach cannot be called in a way its author did not intend, and it can be changed or removed without breaking another class. No rule checks this. Reviewers do. - The getter of an
@AuraEnabledproperty ispublicorglobal. Since the Summer '21 release, a Lightning web component or an Aura component cannot read aprivateorprotectedgetter, so the component gets no value. This is a case of the bullet above: the component is the code outside the class that uses the getter. Aprivateorprotectedgetter of an@AuraEnabledproperty, and an@AuraEnabledproperty that isprivate, are reported at severity 3. Enforced byInaccessibleAuraEnabledGetter. - A test reaches a private or protected member through the
@TestVisibleannotation on that member, never through a wider access modifier. The annotation opens the member to test methods only and changes nothing for other code (TestVisible Annotation), where a member madepublicfor a test is open to all the Apex in its namespace. No rule checks this. Reviewers do. - A class or a member is
globalonly where code in another namespace has to call it, or where the platform requires it, as it does for an Apex REST class. Aglobaldeclaration is reported at severity 4. Enforced byAvoidGlobalModifier.
Debug Output and Error Disclosure¶
Debug output is a security control, not a style preference. Debug logs are downloadable, retained, and readable by anyone with access to view them, so a System.debug(record) call can expose full record data, including PII and PHI, to a far wider audience than the developer who wrote it. Error messages carry the same risk in the other direction: whatever an exception says is shown to the user.
- No
System.debugstatements, in production code or in a test class. A debug statement blocks the pull request (severity 2); enforced by the PMD ruleAvoidDebugStatements. A debug statement whose argument is expensive to build, such as a serialised record, is also enforced by the ApexGuru ruleExpensiveDebugStatements(severity 2). - No debug statement writes a credential. A debug statement that writes a value whose name says that it holds one, such as
passwordorsessionToken, puts the credential in a log that many people can read. Such a statement blocks the pull request (severity 2), by this rule as well as by the rule above. Enforced byApexDangerousMethods. The same rule reports a call toConfiguration.disableTriggerCRUDSecurity(), from the Apex Enterprise Patterns library (fflib), which turns off the object permission checks of the library's trigger handling; under the access mode standard, such a call has no place (Sharing and Access Mode Standards). - Any debug statement that does remain specifies a
LoggingLevel. One without a level is reported at severity 4; enforced by the PMD ruleDebugsShouldUseLoggingLevel. - Diagnostic logging goes through the org's logging framework, which keeps sensitive values out of log entries; see Observability.
- Uncaught exceptions are never shown to users verbatim: raw
DmlExceptionandQueryExceptionmessages and stack traces can reveal field names, org structure, and data. Log the full detail through the logging framework and show the user a generic message.
Session Ids¶
A session Id is a credential: whoever holds it can act as the user until the session ends.
- Apex does not read the session Id. A call to
UserInfo.getSessionId()blocks the pull request (severity 2) until it is removed or a reviewer accepts it. Enforced byAvoidUnauthorizedGetSessionIdInApex. - Apex does not write the session Id into a string either, as a merge field for a page or an email to fill in. A string that holds
{!API.Session_ID}blocks the pull request (severity 2), outside test code.{!$Api.Session_ID}, the usual form with a dollar sign, blocks the same way. Enforced byAvoidUnauthorizedApiSessionIdInApex, which reads the form without the dollar sign, andAvoidApiSessionIdInApexString, which reads the form with it. A Visualforce page does not read it either (HTML and Visualforce: Session Ids).
Cookies and Cryptography¶
- A cookie is created with
isSecureset totrue, so that it can only be accessed through HTTPS (Cookie Class). A cookie created withisSecureset tofalseis reported at severity 3. Enforced byAvoidInsecureCookies. - A digest, a message authentication code, or a signature is made with an algorithm of the SHA-2 family, such as
SHA-256,hmacSHA256, orRSA-SHA256(Crypto Class). MD5 and SHA-1 are not collision resistant, and NIST has announced that SHA-1 should be phased out by 31 December 2030 in favour of SHA-2 and SHA-3 (NIST Retires SHA-1 Cryptographic Algorithm). A call to a method of theCryptoclass that names MD5 or SHA-1 is reported at severity 3. Enforced byAvoidWeakCryptoAlgorithms. - An encryption key and an initialisation vector are generated, never written into the code. A key comes from
Crypto.generateAesKey, or from a store that the platform protects, and the vector is left toCrypto.encryptWithManagedIV, which makes a random one for each call (Crypto Class). A key written into the code is in the repository, so whoever can read the repository can decrypt the data, and a fixed vector makes the same text encrypt the same way every time. ACryptocall with a key or a vector made from a text literal blocks the pull request (severity 1). Enforced byApexBadCrypto. - Every call that encrypts or decrypts is reviewed for where its key comes from and how it is kept. A call to
Crypto.encrypt,Crypto.encryptWithManagedIV,Crypto.decrypt, orCrypto.decryptWithManagedIVblocks the pull request (severity 2) until a reviewer accepts it. It marks a place for review, not a defect. Enforced byHotspotEncrypt.
SOQL Injection¶
In a SOQL injection, text that a user controls is joined into a query string and changes what the query does, for example with a condition that matches every record (SOQL Injection). A query in system mode then returns records that the user cannot see.
- No input is joined into a query string. A dynamic query takes a value through a bind variable, with
Database.queryWithBindsor a:namein the string. A value that cannot be bound, such as a field name or a sort order, is checked against a list of the values that the code allows.String.escapeSingleQuotesis applied to any other value that is joined into the string. A variable joined into a query string without escaping blocks the pull request (severity 2). Enforced byApexSOQLInjection.
Cross-Site Scripting¶
Apex prepares much of what a page shows. When text that an attacker controls reaches the page unescaped, it runs there as script, as the user who opened the page (Cross Site Scripting (XSS)). The Visualforce standards cover the page itself (Visualforce: Cross-Site Scripting). The two standards below cover the Apex that feeds the page.
- A value read from a URL parameter is escaped, with an escape method of the
Stringclass such asescapeHtml4(), or checked against the values that the code expects, before the class uses it or returns it. An unescaped value from a URL parameter blocks the pull request (severity 2). Enforced byApexXSSFromURLParam. - An error message that
addErrorshows is escaped, which is the default. Where a message has HTML markup, and so is given with escaping turned off (falseas the second argument), it holds no value that a user entered. Salesforce warns that an unescaped message can carry harmful code (SObject Class). A call toaddErrorwith escaping turned off and a message built from a variable blocks the pull request (severity 2). Enforced byApexXSSFromEscapeFalse.
Cross-Site Request Forgery¶
In a cross-site request forgery, another site makes the user's browser send a request to the org, and the org runs it as the user (Cross-Site Request Forgery (CSRF)). The Visualforce standards explain the attack and cover the page itself (Visualforce: Cross-Site Request Forgery). The standard below covers the Apex behind the page: the constructor of a controller runs when the page loads, as the action method of apex:page does, before any form, and no token is checked.
- The code that runs when a page loads changes no data. A constructor, an instance or static initializer block, and a method named
init, in any case, hold no DML statement and no DML method of theDatabaseclass. They may query. Data changes in an action method, which the user starts. A DML statement in one of these places blocks the pull request (severity 1). Enforced byApexCSRF. The rule reads DML statements such asinsertandupdate. It does not read aDatabasemethod such asDatabase.insert, or a method that the constructor calls, so reviewers check those.
Open Redirects¶
An open redirect sends the user to an address that a URL parameter chose (Security Tips for Apex and Visualforce Development). The link that the attacker sends starts with the org's own address, so the user trusts it. The page then sends the user on to the attacker's site, which can look like the Salesforce login page.
- A redirect goes to a page that the code chooses. A
PageReferenceis built from a page reference such asPage.AccountSummary, from a record Id, or from a value that the code checks against the pages it may go to, and never from a URL parameter as it is. APageReferencebuilt from a URL parameter blocks the pull request (severity 2). Enforced byApexOpenRedirect.
Untrusted Input¶
Text that reaches Apex from outside the org is checked before the code relies on it.
- No security decision rests on the HTTP
Refererheader. Browsers, proxies, and load balancers often leave it out or strip it for privacy (Cross-Site Request Forgery Prevention Cheat Sheet), and it says where a request came from, not who sent it. Code that reads the header blocks the pull request (severity 2) until a reviewer accepts that it makes no such decision. Enforced byAvoidHttpReferer. - Every place that reads JSON is reviewed for where the text comes from and whether the result is checked before it is used. A call to
JSON.deserialize,JSON.deserializeStrict,JSON.deserializeUntyped, orJSON.createParser(JSON Class) blocks the pull request (severity 2) until a reviewer accepts it. It marks a place for review, not a defect. Enforced byHotspotDeserializeJson.
Callouts¶
A callout carries the org's data, and often a credential, to another system.
- A callout goes over HTTPS. An endpoint that starts with
http://sends the request, and any credential in it, as plain text, which anyone on the network path can read and change. Anhttp://endpoint blocks the pull request (severity 2). Enforced byApexInsecureEndpoint. - A callout authenticates through a Named Credential. The Named Credential holds the endpoint, and the External Credential that it uses holds the authentication and the secret (Create Named Credentials and External Credentials).
- Callout authentication in Apex does not set the authorization header directly (
request.setHeader('Authorization', authorizationHeader);), and uses a Named Credential (callout:My_Named_Credential/path) instead (severity 1). Enforced byApexSuggestUsingNamedCred.
Sources:
- Access Modifiers - Apex Developer Guide
- Cookie Class - Apex Reference Guide
- Create Named Credentials and External Credentials - Salesforce Help
- Cross Site Scripting (XSS) - Apex Developer Guide
- Cross-Site Request Forgery (CSRF) - Visualforce Developer Guide
- Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series
- Crypto Class - Apex Reference Guide
- JSON Class - Apex Reference Guide
- NIST Retires SHA-1 Cryptographic Algorithm - NIST
- Security Tips for Apex and Visualforce Development - Apex Developer Guide
- SObject Class - Apex Reference Guide
- SOQL Injection - Apex Developer Guide
- TestVisible Annotation - Apex Developer Guide
Hard-Coded Values¶
A value written into Apex is right in one org only, or only until someone changes it in Setup.
Hard-Coded Credentials¶
A credential written into a class is read by everyone who can read the repository, and it stays in the history after the class is corrected (Security).
- No credential is written into Apex. A password, a key, or a token is kept in an External Credential. A callout uses it through the Named Credential that points to the External Credential, so Apex never reads it. A field or a
Stringvariable whose name says that it holds a credential, such aspassword,apiKey, orsessionToken, and that is given a text literal blocks the pull request (severity 2). Enforced byAvoidHardcodedCredentialsInFieldDecls,AvoidHardcodedCredentialsInVarDecls, andAvoidHardcodedCredentialsInVarAssign. The {N} rules read the name, not the value, so they also report a harmless literal under such a name, such asHASH_ALGORITHM = 'SHA-256'. A false alarm of that kind is the price of a screen, and the owning team accepts it (Fixing a Blocking Violation). The rules do not read a test class. - No credential is written into the header of a request. A call to
setHeaderthat gives a text literal to a header such asAuthorizationblocks the pull request (severity 2). The wordBearerjoined to a variable is not reported, and neither is a merge field of a Named Credential. Enforced byAvoidHardcodedCredentialsInHttpHeader. - No password is written into a call to
System.setPassword. A text literal there blocks the pull request (severity 1), in a test class too. Enforced byAvoidHardcodedCredentialsInSetPassword.
Hard-Coded IP Addresses¶
An IP address written into code ties a class to one network address, and it changes only with a deployment.
- No IP address is written into Apex. An address is read from a Named Credential, a custom metadata record, or a custom setting. A string that is an IPv4 address, or a URL whose host is one, blocks the pull request (severity 1). Enforced by
AvoidHardcodedIpAddressApex.
Hard-Coded Email Addresses¶
An email address written into code sends mail to one fixed person, and it changes only with a deployment.
- No email address is written into Apex. An address is read from a custom metadata record or a custom setting, or the message is sent to a contact, lead, or user record through
setTargetObjectId(SingleEmailMessage Class). A string that holds an email address blocks the pull request (severity 1). A test class is not read, because a test creates its users with an address. Enforced byEmailAddressInLiteral.
Hard-Coded Salesforce URLs¶
Every sandbox and every production org has a host name of its own, so the absolute URL of one org is wrong in every other org, and it breaks when the org's My Domain name changes.
- No absolute URL of a Salesforce org is written into Apex. A link to a Visualforce page of the same org is a page reference,
Page.AccountSummary, wherever possible: the platform then knows that the class depends on the page, and does not let the page be deleted while the class refers to it (PageReference Class). Any other link within the org is a relative URL, even where the domain could be constructed. Building an absolute URL fromURL.getOrgDomainUrl(), which returns the canonical URL of the org the code is running in, is a last resort, for a link that has to be absolute, such as one sent in an email (URL Class). A login or token endpoint is read from a Named Credential (Integration). A string that holds the URL of a Salesforce org is reported at severity 3. Enforced byAvoidAbsoluteSalesforceUrl. The same standard holds in markup (HTML and Visualforce: Resources and Links) and in Flows (Automation: Hard-Coded Values).
Record IDs¶
A record ID belongs to one org. The same record has another ID in every sandbox, and a record that is deleted and made again gets a new one, so a class that carries an ID breaks when it is deployed to another org or when the record changes.
- No record ID is written into Apex. The code finds the record by a field that is the same in every org, such as a
DeveloperName, an external ID, or a name, or reads the ID from a custom metadata record or a custom setting, and it reaches the running user throughUserInfo.getUserId(). A text literal that has the form of a 15 or 18 character ID blocks the pull request (severity 1) until it is removed or a reviewer accepts it. Enforced byAvoidHardcodingId. The rule reads the form of the literal and not its use, so a test that builds an ID of the right form on purpose, or a one-off script for one org, is reported too, and the reviewer accepts it there. A hard-coded ID in production code is not accepted. The same standard applies to Flows (Flow) and to links in Visualforce (HTML and Visualforce: Resources and Links), at the same severity.
Record Types¶
A record type has two names. Its DeveloperName is its unique name in the API, and its Name is the label shown in the user interface (RecordType). A label can be changed in Setup at any time, with no deployment.
- Apex refers to a record type by its
DeveloperNameor by its Id, never by itsName. This covers a query onRecordType, a filter on a record's record type, such asWHERE RecordType.DeveloperName = 'Partner', and a comparison in code. The Id is read from the describe result, withgetRecordTypeInfosByDeveloperName(), which needs no query (DescribeSObjectResult Class), or from a query that filters on theDeveloperName. No rule checks a query or a comparison. Reviewers do, and it does not block a pull request. getRecordTypeInfosByName()is not called. It looks a record type up by its label, wheregetRecordTypeInfosByDeveloperName()looks it up by the name that does not change. A call to it is reported at severity 3. Enforced byAvoidRecordTypeInfosByName.
Sources:
Testing¶
TODO
Consider moving this section to the Testing page (testing.md, Content item 144).
Standards for Apex tests: when a test is written, how much of the code they cover, what makes a test class, how a test is linked to the code it tests, which users a test runs as, what a test checks, and how a test class declares its sharing and states its access mode. They apply the Testing principle, that behaviour is proven and not assumed, to Apex (Testing).
Test-Driven Development¶
A test is written before the code it tests. Salesforce strongly recommends a test-driven development process for Apex, which it describes as test development that happens at the same time as code development (Writing Tests). These standards go one step further: the test comes first.
- Before a change is coded, its test cases are listed: one for each behaviour that the change adds or corrects, for each user persona that reaches it (Running As a User).
- Each test is written first, and it fails. Then the code is written that makes it pass, and then the code and the test are cleaned up without changing what they do. The next test on the list follows (Test Driven Development).
- A bug fix starts with a test that reproduces the bug. The test fails before the fix and passes after it, so the bug cannot come back unnoticed.
No rule can tell in what order a test and its code were written. Test-driven development is a practice that the team keeps, and reviewers can ask to see the failing test.
Coverage¶
- Apex test coverage across the org is at least 85%. Salesforce requires 75% before Apex can be deployed to production, and some coverage on every trigger (Testing Best Practices). The higher floor leaves room for an urgent fix to deploy even when it lowers the figure a little.
- Each class is ideally closer to 90%, with tests that cover every realistic scenario for it.
- 100% is not a target. The floor is a minimum, and tests exist to demonstrate behaviour, not to reach a number (Proven Functionality).
- Coverage is never padded. Code whose only purpose is to raise the figure, such as a method of repeated
i++;statements that a test calls, is obvious in a code review, and it breaks several standards at once. Filler statements block the pull request (severity 2); a false positive is reviewed like any other blocking violation. Enforced by the ApexGuru ruleWritingFillerStatements.
Test Classes¶
-
No test uses
@IsTest(SeeAllData=true). A test creates the data it needs, and by default it cannot see the records already in the org (Isolation of Test Data from Organization Data in Unit Tests). There are rare exceptions, such as some uses ofConnectApi, most of whose methods fail in a test without it (Testing ConnectApi Code). Even then, the annotation blocks the pull request (severity 2) until a reviewer accepts it. Enforced byApexUnitTestShouldNotUseSeeAllDataTrue.An organisation that accepts these tests can have the rule pass a test that calls
ConnectApi. The rule's entry in its stock rules file takes this property, in a<properties>element:<property name="violationSuppressXPath" value="(ancestor-or-self::Method, ancestor-or-self::UserClass)[1]//MethodCallExpression[starts-with(lower-case(@FullMethodName), 'connectapi.')]" />The rule then passes a test method with
SeeAllData=truethat calls aConnectApimethod itself, and a test class withSeeAllData=truein which any method calls one. It still reports a test method whose call is in a helper method, and a test whose call is in another class, because the property reads only the method or the class that has the annotation.For More Information: Connect in Apex
Some
ConnectApimethods have a test method of their own, with the same name after asetTestprefix, and with the same parameters and one more, the result. A test builds the output that it expects and registers it with thesetTestmethod, and the regular method then returns that output in the test, without reading org data. A few methods, such asChatterFeeds.getFeedElementsFromFeed, can be tested only this way, because they cannot read org data in a test at all; the Usage section of such a method says so (Testing ConnectApi Code).These methods may not remove the need for
SeeAllData=true. MostConnectApimethods have nosetTestmethod, among them the common writes, such asChatterFeeds.postFeedElement, so a test that also calls one of those still needs the annotation. Another way is to keepConnectApiout of the test: the class callsConnectApithrough an interface, and the test gives the class a mock of that interface (Testing Chatter in Apex without seeAllData=true).These classes have
setTestmethods. Point at a class to see the names of itssetTestmethods, and select it to open its methods in the Apex Reference Guide: -
A test class states whether it can run in parallel:
@IsTest(IsParallel=true)when it can, and@IsTest(IsParallel=false)when it cannot.IsParallel=trueruns the class in parallel even when parallel Apex testing is disabled for the org (IsTest Annotation). A test class that states neither is reported at severity 5, for the developer's information.A test that cannot run in parallel is not reported: a class that uses
SeeAllData=true, callsTest.getStandardPricebookId(),System.schedule(), orSystem.enqueueJob(), or creates aUser, aGroupMember, aContentNote, or another setup object that cannot be mixed with other sObjects in one DML transaction (sObjects That Can't Be Used Together in DML Operations). Enforced byTestClassIsParallel. -
A test class should have at least one test method. A test helper is the exception: a class with a public or global static method, such as a test data factory, or with a member that carries
@TestVisible. A test class with no test method is reported at severity 3. Enforced byTestClassWithoutTestCasesApex.
Test Methods¶
- Every test method is in a class that carries the
@IsTestannotation on the class itself. Since API version 28.0 the platform does not compile a test method anywhere else (What Are Apex Unit Tests?); in older code, a test method outside a test class blocks the pull request (severity 1). Enforced byTestMethodsMustBeInTestClasses. - A test method is marked with the
@IsTestannotation, not thetestMethodkeyword. The keyword blocks the pull request (severity 2). Enforced byApexUnitTestMethodShouldHaveIsTestAnnotation. Also enforced by the ApexGuru ruleUsingTheTestMethodKeyword(severity 4), a second layer over the PMD rule. - A test method should call
Test.startTest()andTest.stopTest()around the code under test. The code between them gets its own set of governor limits, and asynchronous work, such as a queued job or a future method, runs whenTest.stopTest()is called. A test method that does not call both is reported at severity 4. Enforced byUnitTestsShouldIncludeStartTest.
Testing Patterns¶
TODO
Test data patterns: @TestSetup and test data factories.
- A test reaches a private member of the class it tests through
@TestVisible, not through a wider access modifier (Access Modifiers). - Production code should not call
Test.isRunningTest(). Code that behaves one way in a test and another way in the org is not tested by its tests; a mock or a test double takes the place of the behaviour that a test must avoid. A call outside a test class is reported at severity 4. Enforced byAvoidUsingTestIsRunningTest.
Test Selection¶
A pull request runs the tests that bear on its change and not every local test (Test Levels). Where the pull request gate uses the RunRelevantTests test level (Beta, API 66.0 and later), Salesforce chooses those tests, and it finds a test only through a reference in code or through an annotation. These standards make sure that every test can be found.
- Every test class says what it tests, or says that it always runs: its
@IsTestannotation carriestestFor, naming the classes and triggers it tests, orcritical=true. A test class with neither blocks the pull request (severity 2). Enforced byRequireTestForOrCritical. - The tests of a trigger name it:
@IsTest(testFor='ApexTrigger:TriggerName'). A test that reaches a trigger only by inserting or updating a record is not selected when the trigger changes. - A test that reaches its class only by name, through
Type.forNameor another dynamic call, names that class:@IsTest(testFor='ApexClass:ClassName'). - Tests that are intended to cover declarative automation, such as the tests of a Flow, of a validation rule, and of anything else that is not Apex, must have
critical=true, becausetestForcannot name declarative metadata: it accepts Apex classes and triggers only. A change with no Apex in it selects no test by itself, and a critical test runs on every pull request. - At least one test class in the org must be marked
critical=true. When no test is selected, a changed trigger can be accepted with no coverage at all; once any test runs, every changed class and trigger must be covered.
Whether testFor names the right class or trigger, and whether critical=true is deserved, is for reviewers to check. Both parameters compile only in a class at API 66.0 or later, so an older test class is raised to that version when it is next changed.
Running As a User¶
Unless it says otherwise, a test runs as the user who started it, and that is often a System Administrator, or at least a user with Modify All Data. A test that runs as that user passes where an end user would be refused, so it does not show what the people who use the code will experience. Tests run as realistic users, each with the least privilege that the persona holds (PLP).
-
A test calls
System.runAsfor every user persona involved in the processes that call the code, each with the permissions that persona really holds, so that its permissions and sharing are exercised. A test method with noSystem.runAsat all blocks the pull request (severity 2). Enforced byApexUnitTestClassShouldHaveRunAs. Whether every persona is covered is for reviewers to check. Code that callsConnectApiis no exception. The Apex Developer Guide's Testing ConnectApi Code states that Connect in Apex does not support therunAssystem method, but tests in scratch orgs at API 68.0 showed that this is not the case: aConnectApipost and read insideSystem.runAsran without an error, and as therunAsuser. Such tests still needSeeAllData=true, unless everyConnectApimethod that they call has asetTestmethod (Test Classes).How runAs with ConnectApi was tested, and the results
Two test classes ran in Developer Edition scratch orgs, each run in a new org. Each class ran at API 68.0, and the class with
SeeAllData=trueran again as a copy saved at API 31.0, the oldest version at which it compiles. The classes ran twice: first with no sharing declared, and then with every class declaredwith sharing, the most restrictive declaration. A second user, with the Standard User profile, was made in the org before each run. Every case gave the same result in both runs, at both API versions.Case SeeAllData=trueWhere the ConnectApicall ranResult A post ( ChatterFeeds.postFeedElement) and a read (ChatterUsers.getUser(null, 'me'))Yes Outside System.runAsPassed, as the user who started the test The same post and read Yes Inside System.runAswith the current userPassed, with no exception, as the user who started the test The same post and read Yes Inside System.runAswith the other userPassed, with no exception. The post's actor, its CreatedById, and the user thatgetUserreturned were the other userA post before and after a System.runAsblock that holds noConnectApicodeYes Outside the block Passed, as the user who started the test setTestGetFeedElementsFromFeed, thengetFeedElementsFromFeedNo Outside System.runAs, and inside it with each userPassed: the registered page was returned every time The read ( ChatterUsers.getUser(null, 'me'))No Outside System.runAs, and inside it with the other userFailed, with and without System.runAs:System.UnsupportedOperationException: ConnectApi methods are not supported in data siloed tests. Please use @IsTest(SeeAllData=true).So
System.runAsworks withConnectApi: it does not throw, and it is not ignored, becauseConnectApiacts as therunAsuser. The only exception came from a missingSeeAllData=true. The tests cover twoConnectApimethods, a post and a read, and do not show that everyConnectApimethod behaves the same way.The classes below are the classes that ran in the second run,
with sharing; only their comments were added after the run. The comments mark the line that threw, and the lines where the guide would lead a reader to expect an exception that did not come. They were written to test the platform, so they write a debug line with the user ids of each case.ConnectApiRunAsSeeAllDataTest.cls: the tests with SeeAllData=true
ConnectApiRunAsSeeAllDataTest.cls/** * Cases 1, 2, 3, and 5: ConnectApi calls with SeeAllData=true, outside System.runAs, * inside runAs(current user), and inside runAs(another user). * * Every method catches twice: once around the ConnectApi call ("inner") and once around * the whole runAs block ("outer"). Each catch records the exception type, line, and message. * The assertions then say which branch ran: * - an assertion message that starts "inner caught" or "outer caught" means the exception * was thrown and could be caught there; * - a raw platform exception as the failure means it could not be caught; * - an assertion on the actor says which user ConnectApi acted as. * * The other user is created from the CLI before the run (sf org create user, last name * ConnectApiRunAsProbe), so that no test inserts a User and a FeedItem in one transaction. * * Results, in two Developer Edition scratch orgs (the second run with every * class declared with sharing), at API 68.0 and again with a copy saved at API 31.0: all seven * methods passed in every run. No line threw an exception. Inside System.runAs(other), ConnectApi * acted as the other user; inside runAs(current user) and outside runAs, as the user who started * the test. The comments below mark the lines where an exception was expected and not found. */ @IsTest(SeeAllData=true) private with sharing class ConnectApiRunAsSeeAllDataTest { private static final String PROBE_LAST_NAME = 'ConnectApiRunAsProbe'; private with sharing class Outcome { Id original; Id contextUser; Id actor; Id createdBy; Id parent; Id meId; String innerError; String outerError; String summary() { return 'original=' + original + ', contextUser=' + contextUser + ', actor=' + actor + ', createdBy=' + createdBy + ', parent=' + parent + ', getUserMe=' + meId + ', inner=' + innerError + ', outer=' + outerError; } } private static User otherUser() { return [SELECT Id FROM User WHERE LastName = :PROBE_LAST_NAME AND IsActive = TRUE LIMIT 1]; } private static String describe(String label, Exception e) { return label + ' caught ' + e.getTypeName() + ' at line ' + e.getLineNumber() + ': ' + e.getMessage(); } private static void post(Outcome o, String label) { o.contextUser = UserInfo.getUserId(); try { // No exception in any method. Inside runAs(other), the post was made as the other user: // its actor, its CreatedById, and its parent were the other user. ConnectApi.FeedElement fe = ConnectApi.ChatterFeeds.postFeedElement(null, UserInfo.getUserId(), ConnectApi.FeedElementType.FeedItem, 'ConnectApi runAs probe: ' + label); o.actor = ((ConnectApi.ActorWithId) ((ConnectApi.FeedItem) fe).actor).id; FeedItem row = [SELECT CreatedById, ParentId FROM FeedItem WHERE Id = :fe.id]; o.createdBy = row.CreatedById; o.parent = row.ParentId; } catch (Exception e) { o.innerError = describe('inner', e); } } private static void read(Outcome o) { o.contextUser = UserInfo.getUserId(); try { // No exception in any method. Inside runAs(other), 'me' was the other user. ConnectApi.UserDetail me = ConnectApi.ChatterUsers.getUser(null, 'me'); o.meId = me.id; } catch (Exception e) { o.innerError = describe('inner', e); } } private static void report(String method, Outcome o) { System.debug(LoggingLevel.ERROR, 'RESULT ' + method + ': ' + o.summary()); } // Case 1: baseline, no runAs. @IsTest static void case1BaselinePost() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); post(o, 'case1BaselinePost'); report('case1BaselinePost', o); Assert.isNull(o.innerError, o.summary()); Assert.areEqual(o.contextUser, o.actor, 'actor is not the context user: ' + o.summary()); Assert.areEqual(o.contextUser, o.createdBy, 'createdBy is not the context user: ' + o.summary()); } @IsTest static void case1BaselineRead() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); read(o); report('case1BaselineRead', o); Assert.isNull(o.innerError, o.summary()); Assert.areEqual(o.contextUser, o.meId, 'getUser me is not the context user: ' + o.summary()); } // Case 2: runAs(current user). @IsTest static void case2RunAsSelfPost() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here, // and ConnectApi acted as the current user, as it does with no runAs. System.runAs(new User(Id = UserInfo.getUserId())) { post(o, 'case2RunAsSelfPost'); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case2RunAsSelfPost', o); Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(o.contextUser, o.actor, 'actor is not the context user: ' + o.summary()); Assert.areEqual(o.contextUser, o.createdBy, 'createdBy is not the context user: ' + o.summary()); } @IsTest static void case2RunAsSelfRead() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here, // and ConnectApi acted as the current user, as it does with no runAs. System.runAs(new User(Id = UserInfo.getUserId())) { read(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case2RunAsSelfRead', o); Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(o.contextUser, o.meId, 'getUser me is not the context user: ' + o.summary()); } // Case 3: runAs(another user). @IsTest static void case3RunAsOtherPost() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); User other = otherUser(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here, // and ConnectApi acted as the other user, not as the user who started the test. System.runAs(other) { post(o, 'case3RunAsOtherPost'); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case3RunAsOtherPost', o); Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(other.Id, o.contextUser, 'UserInfo inside runAs is not the other user: ' + o.summary()); Assert.areEqual(o.contextUser, o.actor, 'actor is not the runAs user: ' + o.summary()); Assert.areEqual(o.contextUser, o.createdBy, 'createdBy is not the runAs user: ' + o.summary()); } @IsTest static void case3RunAsOtherRead() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); User other = otherUser(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here, // and ConnectApi acted as the other user, not as the user who started the test. System.runAs(other) { read(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case3RunAsOtherRead', o); Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(other.Id, o.contextUser, 'UserInfo inside runAs is not the other user: ' + o.summary()); Assert.areEqual(o.contextUser, o.meId, 'getUser me is not the runAs user: ' + o.summary()); } // Case 5: ConnectApi before and after a runAs(another user) block that holds no ConnectApi code. @IsTest static void case5ConnectApiOutsideRunAsBlock() { Outcome before = new Outcome(); before.original = UserInfo.getUserId(); post(before, 'case5 before'); Id insideUser; String outerError; try { // This block holds no ConnectApi code. The posts before and after it acted as the // user who started the test. System.runAs(otherUser()) { insideUser = UserInfo.getUserId(); Account a = new Account(Name = 'ConnectApi runAs probe'); insert a; } } catch (Exception e) { outerError = describe('outer', e); } Outcome after = new Outcome(); after.original = UserInfo.getUserId(); post(after, 'case5 after'); String summary = 'before: ' + before.summary() + ' | insideUser=' + insideUser + ', runAsOuter=' + outerError + ' | after: ' + after.summary(); System.debug(LoggingLevel.ERROR, 'RESULT case5ConnectApiOutsideRunAsBlock: ' + summary); Assert.isNull(before.innerError, summary); Assert.isNull(outerError, summary); Assert.isNull(after.innerError, summary); Assert.areEqual(before.original, before.actor, 'before: actor is not the original user: ' + summary); Assert.areEqual(after.original, after.actor, 'after: actor is not the original user: ' + summary); } }ConnectApiRunAsSetTestTest.cls: the tests without SeeAllData
ConnectApiRunAsSetTestTest.cls/** * Case 4: a ConnectApi setTest method without SeeAllData, outside System.runAs, inside * runAs(current user), and inside runAs(another user). Case 5: ConnectApi.ChatterUsers.getUser * without SeeAllData, outside and inside runAs, to tell a SeeAllData failure from a runAs failure. * * Each method catches around the ConnectApi call ("inner") and around the runAs block ("outer"), * and puts the exception type, line, and message in its assertion message. * * The other user is created from the CLI before the run (sf org create user, last name * ConnectApiRunAsProbe). A User is setup data, which a test can query without SeeAllData. * * Results, in two Developer Edition scratch orgs (the second run with every * class declared with sharing), at API 68.0: the four case 4 methods passed, and the two case 5 * methods failed, as designed, because getUser needs SeeAllData=true. No exception came from * System.runAs. The comments below mark the line that threw, and the lines where an exception * was expected and not found. */ @IsTest(critical=true IsParallel=false) private with sharing class ConnectApiRunAsSetTestTest { private static final String PROBE_LAST_NAME = 'ConnectApiRunAsProbe'; private with sharing class Outcome { Id original; Id contextUser; Integer elements; Id meId; String setInner; String innerError; String outerError; String summary() { return 'original=' + original + ', contextUser=' + contextUser + ', elements=' + elements + ', getUserMe=' + meId + ', setInner=' + setInner + ', inner=' + innerError + ', outer=' + outerError; } } private static User otherUser() { return [SELECT Id FROM User WHERE LastName = :PROBE_LAST_NAME AND IsActive = TRUE LIMIT 1]; } private static String describe(String label, Exception e) { return label + ' caught ' + e.getTypeName() + ' at line ' + e.getLineNumber() + ': ' + e.getMessage(); } private static void setTestPage(Outcome o) { try { ConnectApi.FeedElementPage testPage = new ConnectApi.FeedElementPage(); List<ConnectApi.FeedItem> items = new List<ConnectApi.FeedItem>(); items.add(new ConnectApi.FeedItem()); items.add(new ConnectApi.FeedItem()); testPage.elements = items; // No exception in any method: a setTest method needs no SeeAllData, inside or outside runAs. ConnectApi.ChatterFeeds.setTestGetFeedElementsFromFeed(null, ConnectApi.FeedType.News, 'me', testPage); } catch (Exception e) { o.setInner = describe('setInner', e); } } private static void getPage(Outcome o) { o.contextUser = UserInfo.getUserId(); try { // Returned the 2 registered elements in every case 4 method, inside and outside runAs. ConnectApi.FeedElementPage page = ConnectApi.ChatterFeeds.getFeedElementsFromFeed(null, ConnectApi.FeedType.News, 'me'); o.elements = page.elements.size(); } catch (Exception e) { o.innerError = describe('inner', e); } } private static void read(Outcome o) { o.contextUser = UserInfo.getUserId(); try { // Threw in both case 5 methods, with and without runAs: System.UnsupportedOperationException, // "ConnectApi methods are not supported in data siloed tests. Please use // @IsTest(SeeAllData=true)." The cause is the missing SeeAllData=true, not runAs. The catch // below caught it; its line number, 478, is not a line of this class. ConnectApi.UserDetail me = ConnectApi.ChatterUsers.getUser(null, 'me'); o.meId = me.id; } catch (Exception e) { o.innerError = describe('inner', e); } } private static void report(String method, Outcome o) { System.debug(LoggingLevel.ERROR, 'RESULT ' + method + ': ' + o.summary()); } private static void assertPage(Outcome o) { Assert.isNull(o.setInner, o.summary()); Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(2, o.elements, 'the test page was not returned: ' + o.summary()); } @IsTest static void case4SetTestNoRunAs() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); setTestPage(o); getPage(o); report('case4SetTestNoRunAs', o); assertPage(o); } @IsTest static void case4SetOutsideGetInsideRunAsSelf() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); setTestPage(o); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here. System.runAs(new User(Id = UserInfo.getUserId())) { getPage(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case4SetOutsideGetInsideRunAsSelf', o); assertPage(o); } @IsTest static void case4SetOutsideGetInsideRunAsOther() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); User other = otherUser(); setTestPage(o); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here. System.runAs(other) { getPage(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case4SetOutsideGetInsideRunAsOther', o); Assert.areEqual(other.Id, o.contextUser, 'UserInfo inside runAs is not the other user: ' + o.summary()); assertPage(o); } @IsTest static void case4SetAndGetInsideRunAsOther() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); User other = otherUser(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception was thrown here. System.runAs(other) { setTestPage(o); getPage(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case4SetAndGetInsideRunAsOther', o); Assert.areEqual(other.Id, o.contextUser, 'UserInfo inside runAs is not the other user: ' + o.summary()); assertPage(o); } @IsTest static void case5GetUserNoSeeAllDataNoRunAs() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); read(o); report('case5GetUserNoSeeAllDataNoRunAs', o); // Failed here, as designed: innerError holds the exception that read() caught. Assert.isNull(o.innerError, o.summary()); Assert.areEqual(o.contextUser, o.meId, 'getUser me is not the context user: ' + o.summary()); } @IsTest static void case5GetUserNoSeeAllDataRunAsOther() { Outcome o = new Outcome(); o.original = UserInfo.getUserId(); User other = otherUser(); try { // The Apex Developer Guide says that Connect in Apex does not support runAs. No exception came from runAs here; // only the getUser call inside it failed, for want of SeeAllData=true. System.runAs(other) { read(o); } } catch (Exception e) { o.outerError = describe('outer', e); } report('case5GetUserNoSeeAllDataRunAsOther', o); // Failed here, as designed: innerError holds the exception that read() caught inside the block. Assert.isNull(o.innerError, o.summary()); Assert.isNull(o.outerError, o.summary()); Assert.areEqual(o.contextUser, o.meId, 'getUser me is not the runAs user: ' + o.summary()); } } -
A test never runs as the current user.
System.runAs(new User(Id = UserInfo.getUserId()))satisfies the rule above and changes nothing: the test still runs as whoever started it. Running as the current user blocks the pull request (severity 2). Enforced byDoNotRunAsCurrentUser.
Assertions¶
Coverage percentage measures which lines ran, not whether anything was checked. A test suite that achieves coverage through assertion-free test methods, or through Assert.isTrue(true) patterns that pass unconditionally, satisfies the coverage gate while providing no actual regression protection.
- Every test method asserts the behaviour it exercises. A test method with no assertion of its own blocks the pull request (severity 2); enforced by the PMD rule
ApexUnitTestClassShouldHaveAsserts. The rule cannot see into a helper method, so it also reports a test that asserts only through one. That is a case for the author to make to the reviewer, as Severity Levels allows at severity 2. A tool that remembers when a violation has been accepted, as CodeScan does, raises a test class false positive once and not again. - The assertion is best written in the test method itself. A helper that checks a result returns a Boolean, and the test method asserts it; a test method whose only assertions sit inside a helper is the case the rule above reports.
- Every assertion should include a message,
Assert.fail()included. The message is shown only when the assertion fails, as part of the error (Assert Class), so it says what went wrong, not what the test expects. Many developers get this wrong: they write the outcome that the test expects, such as'The account is active', and when the assertion fails, the test output states the opposite of what happened. A message such as'The account was not activated'tells the developer what failed without a look at the test. The message is recommended so that a failed test can be understood from its output; an assertion with no message is reported and does not block (severity 3). Enforced byApexAssertionsShouldIncludeMessage. - Assertions should use the
Assertclass, such asAssert.areEqualandAssert.isTrue, not the olderSystem.assert,System.assertEquals, andSystem.assertNotEquals. The older forms are reported at severity 4. Enforced byRefactorAssert. - An assertion is only in test code. A failed assertion cannot be caught with
tryandcatch(Assert Class), so production code that checks a condition throws an exception of its own instead. An assertion outside a test class or a test method blocks the pull request (severity 2). Enforced byNonUnitTestContainsAsserts. - An assertion that cannot fail is not an assertion:
Assert.isTrue(true)checks nothing. An assertion whose result is fixed by the values written into it blocks the pull request (severity 2), so a test cannot meet the assertion rules with an assertion that proves nothing:true,false, ornullwritten into it, as inAssert.isTrue(true),Assert.isNull(null), andAssert.areEqual(true, true). Enforced byUnnecessaryBooleanAssertionApex.- Any other value written into it: a string, a number, or a new instance in
Assert.isNullorAssert.isNotNull, as inAssert.isNotNull(new List<String>())andAssert.isNotNull('x'), and a literal on both sides ofAssert.areEqualorAssert.areNotEqual, as inAssert.areEqual(1, 1). Enforced byMisuseOfAssertClass. A new instance as the expected value ofAssert.areEqualis a correct test and is not reported, as inAssert.areEqual(new List<String>(), names).
- An assertion should use the method that fits the check, so that a failure shows what went wrong. Two values are compared with
Assert.areEqualorAssert.areNotEqual, which show both values on failure, not withAssert.isTrue(a == b)orAssert.isTrue(a.equals(b)), which show only that the result was false. A Boolean is checked withAssert.isTrueorAssert.isFalse, not withAssert.areEqual(true, flag). Each is reported at severity 3. Enforced byUseAreEqualInsteadOfAssertBoolean,UseAreEqualInsteadOfIsTrue, andUseIsTrueInsteadOfAreEqual. - Assertions are meaningful. A test does not stop at asserting that no exception was thrown: it asserts every significant value that the code might have changed, in the middle of the test method as well as at the end.
- Assertions are realistic, and they cover every code path: ideally, every path for each user persona (Running As a User).
The rules find an assertion whose values are written into it. No rule can find every assertion that cannot fail, or judge whether an assertion is meaningful or realistic, so reviewers check those standards. They are blocking standards all the same: a reviewer rejects a pull request whose tests break them.
How the pipeline reports coverage and test quality is in Section 5.
Sharing and Access Mode in Test Classes¶
A test class is held to the same standards as any other class (Be Explicit Everywhere).
- A test class declares its sharing. A missing declaration blocks the pull request (severity 1). Enforced by
RequireExplicitSharing. - A test class states the access mode on every database operation. A missing access mode blocks the pull request (severity 2). Enforced by the Java rule
RequireAccessMode. - A system-mode operation in a test class is flagged as it is in any other class (severity 2). A test's database operations should run as a user created for that test class or test method (Running As a User), so a test should rarely need system mode. Enforced by the Java rule
FlagSystemMode.
At API 67.0 and later a test class with no declaration runs with sharing, and an operation with no access mode runs in user mode, so neither is needed for the test to behave correctly. Both are required for the reader: with them written, nobody has to check the class's API version to know how the test runs.
Sources:
- Assert Class - Apex Reference Guide
- Isolation of Test Data from Organization Data in Unit Tests - Apex Developer Guide
- IsTest Annotation - Apex Developer Guide
- sObjects That Can't Be Used Together in DML Operations - Apex Developer Guide
- Test Driven Development - Martin Fowler
- Testing Best Practices - Apex Developer Guide
- Testing Chatter in Apex without seeAllData=true - David Reed
- Testing ConnectApi Code - Apex Developer Guide
- What Are Apex Unit Tests? - Apex Developer Guide
- Writing Tests - Apex Developer Guide
Asynchronous Apex¶
Asynchronous Apex runs under rules of its own, and so does a Flow that it starts, because the Flow inherits the context of the Apex (Security Considerations for Flow Design). Code that can run asynchronously checks its context before it starts more asynchronous work (Execution Context).
- Asynchronous work that a class starts is a Queueable job, not a future method. Salesforce recommends Queueable Apex in place of future methods: a Queueable job does the same work, and it also returns a job Id to monitor, takes parameters of any type, and can start the next job in a chain (Queueable Apex, Future Methods). A method with the
@futureannotation is reported at severity 4. Enforced byAvoidFutureAnnotation. - A Queueable job can attach a finalizer, and it does not need one. A finalizer runs after the job, whether the job succeeded or failed, and it is the place for a recovery action, such as logging the failure or queuing the job again (Transaction Finalizers). A job whose failure needs a recovery action attaches one with
System.attachFinalizer. A Queueable job with no finalizer is reported at severity 5, for the developer's information. Enforced byQueueableWithoutFinalizer. - A scheduled job hands its work to a batch job or a queueable job. Scheduled Apex is asynchronous, and it still runs under the synchronous limits (Execution Governors and Limits).
- A batch job is started once for a whole set of records, never inside a loop and never once for each record of a trigger. The org runs only a few batch jobs at one time and holds a limited number waiting behind them (Execution Governors and Limits). A batch job started inside a loop blocks the pull request (severity 2). Enforced by
OperationWithLimitsInLoop. - A batch class that implements
Database.Statefulkeeps noDatabaseresult in an instance field: noSaveResult,UpsertResult,DeleteResult,MergeResult,UndeleteResult, orEmptyRecycleBinResult, alone or in a collection. A stateful batch keeps its instance fields from one transaction to the next (Use Batch Apex), and a field of one of these types can fail to be serialised between them, at random, with an error that does not name the cause. The field keeps the record Ids or the error messages instead, or, where the value is needed in one transaction only, it is declaredtransient. Such a field is reported at severity 3. Enforced byAvoidStatefulDatabaseResult. - A future method is not called from a batch job or from another future method, where the platform does not allow it. An asynchronous job that has more work to do queues one further job, which is all that the platform allows it (Execution Governors and Limits).
- A synchronous transaction is kept short. The org allows only a limited number of transactions that run for more than a few seconds to run at the same time, and it refuses any more that start while they are running. Time spent waiting for a callout is not counted (Execution Governors and Limits). Slow work is moved to asynchronous Apex.
- Asynchronous work is sized as well. Every asynchronous execution draws on an allocation for the whole org (Limits Counted for the Whole Org).
Apart from a future method, a Queueable job with no finalizer, a batch job started in a loop, and a Database result kept by a stateful batch, no rule checks these standards. Reviewers do, and they do not block a pull request.
TODO(David)
The rest of this section. It will cover Batch Apex, more on Queueable Apex, and platform events. One standard to state: a platform event that the org publishes and consumes itself is not used to start a new transaction asynchronously.
Execution Context¶
The platform allows different asynchronous work in different contexts. A batch job or a future method cannot call a future method; a Queueable job can call up to 50. A synchronous transaction can add up to 50 Queueable jobs to the queue, and an asynchronous one can add only one (Execution Governors and Limits, Queueable Apex). A limit that is exceeded throws an exception that the code cannot catch, so the transaction fails. A shared method, such as a trigger action or a service class, runs in all of these contexts. So does a Flow: a Flow that Apex starts inherits the context of the Apex (Security Considerations for Flow Design), and a record-triggered Flow runs in the transaction of the DML that fires it (Triggers and Order of Execution). An Apex action in that Flow that calls a future method fails in a batch job, as the Apex would.
- Code that starts asynchronous work checks the context it runs in first, and starts nothing that the context does not allow. Where the context does not allow it, the code does the work another way, such as in the current transaction, or it leaves the work to the job that is running. The check decides how the work starts; the work itself is the same in every context. No rule checks this. Reviewers do.
Code that can be reached from an asynchronous job, such as a trigger action that the DML of a batch job fires, or an Apex action in a Flow that a Queueable job starts, first checks whether it is already in an asynchronous context, before it starts asynchronous work of its own. Four methods of the System class answer that question together: System.isFuture(), System.isQueueable(), System.isBatch(), and System.isScheduled().
Boolean isAsync = System.isFuture() || System.isQueueable() || System.isBatch() || System.isScheduled();
Apex has a method for each part of the check:
| Method | What it returns |
|---|---|
System.isFuture() |
true when a future method invoked the code. Salesforce advises this check before a future method is called, because a future method cannot call another (System Class) |
System.isBatch() |
true when a batch job invoked the code. A batch job cannot call a future method, so Salesforce advises this check before a future method is called. At API version 35.0 and earlier, it is also true in a Queueable job |
System.isQueueable() |
true when a Queueable job invoked the code, and false when a batch job or a future method did |
System.isScheduled() |
true when a scheduled Apex job invoked the code |
System.isFunctionCallback() |
true when the callback of an asynchronous Salesforce Function invoked the code, at API version 51.0 and later |
Request.getCurrent().getQuiddity() |
The kind of execution, as a value of the Quiddity enum, such as SYNCHRONOUS, FUTURE, QUEUEABLE, TRANSACTION_FINALIZER_QUEUEABLE, BATCH_APEX, SCHEDULED, or INVOCABLE_ACTION (Request Class, Quiddity Enum). It names contexts that no System method names, such as a Queueable job with a finalizer attached, an invocable action, or a platform event publish callback |
Limits.getQueueableJobs() and Limits.getLimitQueueableJobs() |
How many Queueable jobs the transaction has added to the queue, and how many it may add (Limits Class) |
Limits.getFutureCalls() and Limits.getLimitFutureCalls() |
How many future methods the transaction has called, and how many it may call |
Sources:
- Execution Governors and Limits - Apex Developer Guide
- Future Methods - Apex Developer Guide
- Limits Class - Apex Reference Guide
- Queueable Apex - Apex Developer Guide
- Quiddity Enum - Apex Reference Guide
- Request Class - Apex Reference Guide
- Security Considerations for Flow Design - Secure Coding Guide
- System Class - Apex Reference Guide
- Transaction Finalizers - Apex Developer Guide
- Triggers and Order of Execution - Apex Developer Guide
- Use Batch Apex - Apex Developer Guide
Large Data Volumes¶
TODO
Stub. Selective queries and indexes, data skew, batch sizes, archiving, and queries that time out at volume.
- Every query in an org that holds large data volumes has a selective filter, and the rules that report a non-selective one run at severity 2 there, where these standards set them at 4 for every org (Queries). The platform refuses a non-selective query against a large object, so a filter that passes in a developer sandbox fails in production.
- Code written for large data volumes is tested in a full sandbox, with production's volume of data, before it reaches production. A query, a batch, or a report that runs well on a few thousand records says nothing about how it runs on millions, and the full sandbox is the one place short of production that holds them (Version Control: Branches).
Exception Handling¶
TODO(David, 2026-10-08)
Stub. He has a lot more to say here. Also to cover: custom exception classes, what a catch block does, partial success in DML, and logging. What an error message shows a user is already written, under Security: Debug Output and Error Disclosure. The standards for an empty catch, try, or finally block are written under Error Prevention: Empty Blocks (Content item 149).
- The developer knows which exceptions the code in a
tryblock can throw, and acatchblock names them: it catches a specific standard exception type, such asDmlExceptionorQueryException, wherever possible (Exception Class and Built-In Exceptions). - Catching the generic
Exceptiontype is rare. - No operation runs on a value that may be null. A variable is given a value where it is declared, or it is checked for null before a method is called on it, a field is read from it, or it is used in an expression; a
NullPointerExceptionis a defect that the tests should have found. An operation on a value that is null on some path from an entry point blocks the pull request (severity 2). Enforced by the Graph Engine ruleApexNullPointerException(Developer Preview). - A variable used in the
WHEREclause of a query is checked for null first. With a null value,WHERE Name = :namematches nothing and still reads the whole table, so a selective query becomes a full scan. A query variable with no null check on its path is reported at severity 3. Enforced by the Graph Engine ruleMissingNullCheckOnSoqlVariable(Developer Preview).
No rule checks the first two standards. Reviewers do.
Architecture¶
TODO
Stub. Layers and the separation of concerns: trigger handlers, services, selectors, and domain classes; where business logic lives; dependencies between classes.
- Constants that several classes share are kept in a constants class, and each functional area has one:
BillingConstantsfor billing, not one class for the whole org and not several for one area (Naming has the suffix). A change to the constants of one area then touches that area's class alone. No rule checks this. Reviewers do. - A
publicorglobalmethod or constructor takes noBooleanparameter that switches what it does. A call such asprocess(records, true)does not say whattruemeans, and a method that a flag switches often does two things. Two methods with clear names, or an enum, say it. ABooleanthat is data, such as the value of a checkbox that a component sends, is fine. EachBooleanparameter of apublicorglobalmethod or constructor is reported at severity 4, as a sign to look at the method again. Enforced byAvoidBooleanMethodParameters. - An abstract class or an interface that is not
globalhas at least one class that extends or implements it. One that has none cannot be instantiated and does nothing, so it is implemented or removed. Such a type is reported at severity 4. Enforced by the Graph Engine ruleUnimplementedType(Developer Preview). - No block of code is copied from one place to another. A duplicated block is a method that two callers share, or a class that both extend, so that a fix is made once. A block of Apex that appears more than once across the repository is reported at severity 5, for the developer's information (Refactoring Duplicates). Enforced by
DetectCopyPasteForApex.
Standards Not Adopted¶
Why Only Some Are Listed
Not every standard that was left out is listed here, only those that merit an explanation. Standards Not Adopted on the Code Standards page says why.
Standards that were researched and left out on purpose, each with the reason.
- A class that only carries data, with fields and a constructor and no other method, is allowed. It is a normal part of Apex: a wrapper returned to a Lightning component, a class that
JSON.deserialize()fills, and the request and response classes of an integration.
Integration and Callouts¶
TODO
Stub. Callouts through Named Credentials, timeouts and retries, callouts after DML, and mocking a callout in a test. The two security standards of a callout (HTTPS, and authentication through a Named Credential) are written under Security: Callouts. Which API an integration uses to load records is written, on the Integration page of the Metadata Standards (Choosing an API).
Rules¶
The rules that this page mentions. The Standard column gives each rule's standard in one line, and the Section column links to the section that mentions the rule; "Not yet written" marks a rule of this page that no section mentions yet. Appendix F.6 holds the full cross-reference.
Each rule's name in a standard above links to its row here. How a rule behaves is described with the rules: a row links to the rule and, where there is one, to its note. PMD's own ApexCRUDViolation and ApexSharingViolations are not enforced (Stock PMD Rules Not Adopted).
| Rule | Kind | Severity | Standard | Section |
|---|---|---|---|---|
Abstract |
Custom PMD XPath | 1 | An abstract class declares a method or a constructor | Abstract Classes |
Apex |
Stock PMD | 1 | Generate encryption keys and initialisation vectors; never write them into the code | Cookies and Cryptography |
ApexCSRF |
Stock PMD | 1 | Code that runs when a page loads changes no data: no DML in a constructor, an initializer block, or an init method |
Cross-Site Request Forgery |
Apex |
Stock PMD | 1 | Do not set the Authorization header directly; use a Named Credential |
Callouts |
Avoid |
Stock Code Analyzer PMD | 1 | No password is written into a call to System.setPassword |
Hard-Coded Credentials |
Avoid |
Custom PMD XPath | 1 | No IP address is written into Apex; read it from a Named Credential, custom metadata, or a custom setting Notes on this rule. | Hard-Coded IP Addresses |
Avoid |
Stock PMD | 1 | No record ID is written into Apex; find the record by DeveloperName, an external ID, or a name |
Record IDs |
Email |
Custom PMD XPath | 1 | No email address written into Apex | Hard-Coded Email Addresses |
Multiple |
Custom PMD Java | 1 | Each object has at most one trigger | Triggers |
One |
Stock PMD | 1 | Declare each variable on its own line | Formatting |
Require |
Custom PMD XPath | 1 | Every outer class declares its sharing, a test class included: with sharing by default, inherited sharing for a utility class Notes on this rule. |
Sharing and Access Mode Standards Sharing and Access Mode in Test Classes |
Require |
Custom PMD Java | 1 | A class with an @InvocableVariable member and an explicit constructor also has a visible no-argument constructor at API 66.0 and later |
Constructors |
Require |
Custom PMD XPath | 1 | Every outer class declares its access modifier | Access Modifiers |
Test |
Stock PMD | 1 | Every test method is in a class that carries @IsTest |
Test Methods |
Apex |
Stock PMD | 2 | No debug statement writes a credential, and no call to Configuration.disableTriggerCRUDSecurity() |
Debug Output and Error Disclosure |
Apex |
Stock Code Analyzer SFGE | 2 | Every query and DML statement on a path from an entry point runs in user mode or behind a permission check | Sharing and Access Mode Standards |
Apex |
Stock PMD | 2 | A callout goes over HTTPS; no http:// endpoint |
Callouts |
Apex |
Stock Code Analyzer SFGE | 2 | No operation on a value that may be null; assign it or check it first | Exception Handling |
Apex |
Stock PMD | 2 | A redirect goes to a page that the code chooses, never to a URL parameter as it is | Open Redirects |
Apex |
Stock PMD | 2 | No input is joined into a query string; use bind variables, an allowlist, or String.escapeSingleQuotes |
SOQL Injection |
Apex |
Stock PMD | 2 | Every test method asserts the behaviour it exercises | Assertions |
Apex |
Stock PMD | 2 | A test calls System.runAs for every user persona that reaches the code |
Running As a User |
Apex |
Stock PMD | 2 | No testMethod keyword; mark a test method with @IsTest |
Test Methods |
Apex |
Stock PMD | 2 | Do not use @IsTest(SeeAllData=true); a test creates the data it needs |
Test Classes |
Apex |
Stock PMD | 2 | An addError message that is not escaped holds no value that a user entered |
Cross-Site Scripting |
Apex |
Stock PMD | 2 | Escape or validate a value read from a URL parameter before the class uses or returns it | Cross-Site Scripting |
Avoid |
Custom PMD XPath | 2 | Apex does not write {!$Api.Session_ID} into a string |
Session Ids |
Avoid |
Stock Code Analyzer SFGE | 2 | No query or DML statement inside a loop, in the loop or in a method it calls | Bulkification |
Avoid |
Stock PMD | 2 | No System.debug statements, in production code or in a test class |
Debug Output and Error Disclosure |
Avoid |
Stock PMD | 2 | No fixed index on Trigger.new or Trigger.old, such as Trigger.new[0] |
Bulkification |
Avoid |
Stock Code Analyzer Regex | 2 | No Limits.getHeapSize() inside a loop |
CPU Time and Heap |
Avoid |
Stock Code Analyzer PMD | 2 | No credential in a field declaration; keep passwords, keys, and tokens in an External Credential | Hard-Coded Credentials |
Avoid |
Stock Code Analyzer PMD | 2 | No credential is written into a request header | Hard-Coded Credentials |
Avoid |
Stock Code Analyzer PMD | 2 | No credential assigned to a variable as a text literal | Hard-Coded Credentials |
Avoid |
Stock Code Analyzer PMD | 2 | No credential in a variable declaration as a text literal | Hard-Coded Credentials |
Avoid |
Custom PMD XPath | 2 | No security decision from the HTTP Referer header |
Untrusted Input |
Avoid |
Stock PMD | 2 | A trigger holds no logic: no if, switch, loop, or try block |
Triggers |
Avoid |
Stock Code Analyzer SFGE | 2 | No Schema.getGlobalDescribe() or Schema.describeSObjects() inside a loop, or twice on one path |
Efficient Code |
Avoid |
Stock PMD | 2 | Every SOQL query and SOSL search has a WHERE clause or a LIMIT |
Queries |
Avoid |
Stock Code Analyzer Regex | 2 | Every metadata file states a recent API version | API Version |
Avoid |
Stock Code Analyzer PMD | 2 | Apex does not write the session Id into a string | Session Ids |
Avoid |
Stock Code Analyzer PMD | 2 | Apex does not read the session Id; no UserInfo.getSessionId() |
Session Ids |
Broken |
Custom PMD XPath | 2 | A null check does not read the value it checks for null | Null Checks and Conditions |
Bump |
Custom PMD XPath | 2 | Every metadata file is at or above the API version floor Notes on this rule. | API Version |
Database |
Stock Code Analyzer SFGE | 2 | Every database operation runs under a stated sharing declaration along the whole call path | Sharing and Access Mode Standards |
Declare |
Custom PMD XPath | 2 | Every method except a test method declares its access modifier Notes on this rule. | Access Modifiers |
Dml |
Stock Code Analyzer ApexGuru | 2 | No DML statement inside a loop | Bulkification |
Dml |
Custom PMD XPath | 2 | No call inside a loop to a method that runs DML | Bulkification |
Do |
Custom PMD XPath | 2 | A test never runs as the current user | Running As a User |
Do |
Custom PMD XPath | 2 | without sharing is a deliberate, justified exception |
Sharing and Access Mode Standards |
Eagerly |
Stock PMD | 2 | Load describe results lazily with SObjectDescribeOptions.DEFERRED |
Efficient Code |
Empty |
Stock PMD | 2 | No empty catch block |
Empty Blocks |
Empty |
Stock PMD | 2 | No empty if block |
Empty Blocks |
Empty |
Stock PMD | 2 | No empty method or block, except a private no-argument constructor, a virtual base method, and a commented when else |
Empty Blocks |
Empty |
Stock PMD | 2 | No empty try or finally block |
Empty Blocks |
Empty |
Stock PMD | 2 | No empty while block |
Empty Blocks |
Expensive |
Stock Code Analyzer ApexGuru | 2 | No debug statement whose argument is expensive to build | Debug Output and Error Disclosure |
Expensive |
Stock Code Analyzer ApexGuru | 2 | Refactor a method that uses a large share of the org's CPU time | Efficient Code |
Flag |
Custom PMD Java | 2 | User mode is the default; a system-mode operation is a deliberate exception, with its reason in a comment | Sharing and Access Mode Standards Sharing and Access Mode in Test Classes |
For |
Stock PMD | 2 | Wrap the body of every for loop in braces, even one line |
Braces |
Hotspot |
Custom PMD XPath | 2 | Every place that reads JSON is reviewed | Untrusted Input |
Hotspot |
Custom PMD XPath | 2 | Every call that encrypts or decrypts is reviewed | Cookies and Cryptography |
If |
Stock PMD | 2 | Wrap the body of every else in braces, even one line |
Braces |
If |
Stock PMD | 2 | Wrap the body of every if in braces, even one line |
Braces |
Invocable |
Stock PMD | 2 | A class with an @InvocableVariable member and an explicit constructor also has a visible no-argument constructor |
Constructors |
Limits |
Stock Code Analyzer ApexGuru | 2 | Limits.getHeapSize() is not called often |
CPU Time and Heap |
Min |
Stock Code Analyzer Regex | 2 | An abstract or virtual class with a private method is at API 61.0 or later | API Version |
Misuse |
Custom PMD XPath | 2 | No assertion fixed by a string, a number, or a new instance written into it | Assertions |
No |
Custom PMD XPath | 2 | No colon between an ApexDoc tag and its text | ApexDoc |
No |
Custom PMD Java | 2 | No credential is kept in a field; keep it in an External Credential | Objects and Fields: Secrets in Fields Settings: Custom Settings and Custom Metadata Types |
No |
Custom PMD XPath | 2 | No @last modified tag, such as @last modified on, in an ApexDoc comment |
ApexDoc |
No |
Custom PMD XPath | 2 | A trigger declares no methods | Triggers |
Non |
Custom PMD XPath | 2 | Assertions only in test code | Assertions |
No |
Custom PMD XPath | 2 | Do not use WITH SECURITY_ENFORCED; use WITH USER_MODE |
Sharing and Access Mode Standards |
No |
Custom Code Analyzer Regex | 2 | The author is a person or a team, never a tool default such as ChangeMeIn@UserSettingsUnder.SFDoc |
ApexDoc |
No |
Custom Code Analyzer Regex | 2 | No PMD or Code Analyzer suppression without an approving review from the owning team | Documentation |
Null |
Custom PMD XPath | 2 | Not yet written | Not yet written |
Operation |
Stock PMD | 2 | No SOQL, SOSL, DML, Database method, email send, or asynchronous call inside a loop |
Bulkification Asynchronous Apex |
Override |
Stock PMD | 2 | A class that overrides equals(Object) also overrides hashCode(), and the reverse |
Equality |
Require |
Custom PMD Java | 2 | Every database operation states its access mode: inline SOQL and SOSL, Database and Search methods, and DML |
Sharing and Access Mode Standards Sharing and Access Mode in Test Classes |
Require |
Custom PMD XPath | 2 | Every test class carries testFor, naming the classes and triggers it tests, or critical=true Notes on this rule. |
Test Selection |
Schema |
Stock Code Analyzer ApexGuru | 2 | Avoid Schema.getGlobalDescribe(); describe only the objects needed |
Efficient Code |
Soql |
Stock Code Analyzer ApexGuru | 2 | No SOQL query inside a loop | Bulkification |
Soql |
Stock Code Analyzer ApexGuru | 2 | Filter records in the WHERE clause, not in Apex after the query |
Queries |
Soql |
Stock Code Analyzer ApexGuru | 2 | Every SOQL query has a WHERE clause or a LIMIT |
Queries |
Statements |
Custom PMD XPath | 2 | Each statement on its own line | Formatting |
Unnecessary |
Custom PMD XPath | 2 | No assertion fixed by true, false, or null written into it |
Assertions |
While |
Stock PMD | 2 | Wrap the body of every while loop in braces, even one line |
Braces |
Writing |
Stock Code Analyzer ApexGuru | 2 | Never pad coverage with filler statements | Coverage |
Apex |
Stock PMD | 3 | Every assertion includes a message that says what went wrong, Assert.fail() included |
Assertions |
Avoid |
Custom PMD XPath | 3 | No absolute URL of a Salesforce org in Apex; use a page reference or a relative URL Notes on this rule. | Hard-Coded Salesforce URLs |
Avoid |
Stock PMD | 3 | No deeply nested if statements |
Complexity and Unused Code |
Avoid |
Custom PMD XPath | 3 | Make a describe call whose result does not change once, before the loop | Efficient Code |
Avoid |
Custom PMD XPath | 3 | Create a cookie with isSecure set to true Notes on this rule. |
Cookies and Cryptography |
Avoid |
Stock PMD | 3 | No interface as a Map key type where an abstract class implements it and defines equals or hashCode |
Equality |
Avoid |
Custom PMD XPath | 3 | No switch inside a switch |
Switch Statements |
Avoid |
Stock PMD | 3 | Every annotation is one that Apex defines | Annotations |
Avoid |
Custom PMD XPath | 3 | Do not call getRecordTypeInfosByName(); use getRecordTypeInfosByDeveloperName() |
Record Types |
Avoid |
Custom PMD XPath | 3 | Count records with an aggregate query, not size() on a query |
Queries |
Avoid |
Stock PMD | 3 | A Database.Stateful batch class keeps no Database result, such as a SaveResult, in an instance field |
Asynchronous Apex |
Avoid |
Custom PMD XPath | 3 | Use an algorithm of the SHA-2 family for a digest, a MAC, or a signature; no MD5 or SHA-1 Notes on this rule. | Cookies and Cryptography |
Busy |
Stock Code Analyzer ApexGuru | 3 | No loop written to pass time; use a delayed Queueable job or a scheduled job | CPU Time and Heap |
Cognitive |
Stock PMD | 3 | Keep the cognitive complexity of a method low | Complexity and Unused Code |
Copying |
Stock Code Analyzer ApexGuru | 3 | Copy a list or a set with addAll(), not element by element in a loop |
Efficient Code |
Cyclomatic |
Stock PMD | 3 | Keep the cyclomatic complexity of a method low | Complexity and Unused Code |
Fix |
Custom PMD XPath | 3 | Each branch of an if / else if chain tests a different condition |
Null Checks and Conditions |
Idempotent |
Custom PMD XPath | 3 | No assignment of a value to itself | Null Checks and Conditions |
Inaccessible |
Stock PMD | 3 | The getter of an @AuraEnabled property is public or global |
Access Modifiers |
Logic |
Custom PMD XPath | 3 | Negate a comparison with the opposite operator | Expressions |
Method |
Stock PMD | 3 | Only a constructor has the name of its class | Constructors |
Missing |
Stock Code Analyzer SFGE | 3 | Check a variable used in a WHERE clause for null first |
Exception Handling |
Missing |
Custom PMD XPath | 3 | A class with only private constructors has a public static member | Constructors |
No |
Custom Code Analyzer Regex | 3 | At most one blank line in a row | Formatting |
No |
Custom Code Analyzer Regex | 3 | No empty block comment | Documentation |
No |
Custom PMD XPath | 3 | No one-letter variable names, except i and j as loop counters and e in a catch block |
General: Apex |
Operation |
Stock PMD | 3 | Make an expensive call, such as Schema.describeSObjects(), once before a loop, never inside it |
Efficient Code |
Position |
Custom PMD XPath | 3 | Call equals on the string literal, not on the variable |
Null Checks and Conditions |
Redundant Soql |
Stock Code Analyzer ApexGuru | 3 | Do not run the same query twice in a transaction; keep the result in a map | Queries |
Require |
Custom PMD XPath | 3 | An inner class should declare its sharing too | Sharing and Access Mode Standards |
Return |
Custom PMD XPath | 3 | Return an empty collection, not null |
Null Checks and Conditions |
S |
Stock Code Analyzer ApexGuru | 3 | Build a map of records by Id with the map constructor or putAll(), not put in a loop |
Efficient Code |
Soql Aggregation |
Stock Code Analyzer ApexGuru | 3 | Use an aggregate query (COUNT(), SUM()) for counts and totals, not a loop |
Queries |
Soql |
Stock Code Analyzer ApexGuru | 3 | No SOQL query in a method that a loop calls | Bulkification |
Test |
Custom PMD XPath | 3 | Every test class has a test method, except a test helper | Test Classes |
Trigger |
Custom PMD XPath | 3 | A trigger name ends in Trigger, or in the one suffix the organisation sets Notes on this rule. |
General: Apex Metadata Standards: Naming |
Unconditional |
Custom PMD XPath | 3 | No if condition that is always true or always false |
Null Checks and Conditions |
Unused |
Stock PMD | 3 | Remove a local variable that is never read | Complexity and Unused Code |
Unused |
Stock PMD | 3 | Remove a method that is never called | Complexity and Unused Code |
Use |
Custom PMD XPath | 3 | Compare two values with Assert.areEqual, not == in Assert.isTrue |
Assertions |
Use |
Custom PMD XPath | 3 | Compare two values with Assert.areEqual, not equals() in Assert.isTrue |
Assertions |
Use |
Custom PMD XPath | 3 | Check a Boolean with Assert.isTrue or Assert.isFalse, not Assert.areEqual |
Assertions |
Apex |
Stock PMD | 4 | Every class, method, and property has an ApexDoc comment with a description, a @param for each parameter, and a @return where a value is returned |
ApexDoc |
Apex |
Custom PMD XPath | 4 | Every ApexDoc description has at least 10 characters that say what the thing is for Notes on this rule. | ApexDoc |
Aura |
Custom PMD XPath | 4 | A class that a Lightning component calls ends in Controller |
General: Apex Metadata Standards: Naming |
Avoid |
Stock PMD | 4 | No Boolean parameter that switches what a public or global method does; use two methods or an enum |
Architecture |
Avoid |
Stock PMD | 4 | Start asynchronous work as a Queueable job, not a future method | Asynchronous Apex |
Avoid |
Stock PMD | 4 | global only where another namespace or the platform requires it |
Access Modifiers |
Avoid |
Custom PMD XPath | 4 | Declare a value once as a constant (static final); no magic numbers other than 0, 1, and -1 |
Expressions Constants |
Avoid |
Custom PMD XPath | 4 | Write filters that can use an index; no NOT IN, !=, or LIKE with a leading wildcard Notes on this rule. |
Queries |
Avoid |
Custom PMD XPath | 4 | A value that is only read is a property, not a field with a getter method | Expressions |
Avoid |
Custom PMD XPath | 4 | Production code does not call Test.isRunningTest() |
Testing Patterns |
Boolean |
Custom PMD XPath | 4 | A Boolean method is named as a question, not with get |
General: Apex |
Combine |
Custom PMD XPath | 4 | Combine nested if statements with nothing else in the outer one into one if with && |
Complexity and Unused Code |
Debugs |
Stock PMD | 4 | Any debug statement that remains specifies a LoggingLevel |
Debug Output and Error Disclosure |
Deprecated |
Custom PMD XPath | 4 | Every element with the @Deprecated annotation has a @deprecated tag with the reason and the alternative |
ApexDoc |
Empty |
Custom PMD XPath | 4 | No empty instance initializer block | Empty Blocks |
Exception |
Custom PMD XPath | 4 | A method comment names each exception it throws with @throws |
ApexDoc |
Expensive |
Stock Code Analyzer ApexGuru | 4 | Compare strings with equals or equalsIgnoreCase, not == |
Efficient Code |
Field |
Stock PMD | 4 | Declare fields before the first method of a class | Formatting |
Field |
Custom PMD XPath | 4 | A field or property does not take the name of its class, and a local variable does not take the name of a class | General: Apex |
Field |
Custom PMD XPath | 4 | A field or property does not share its name with a method of its class | General: Apex |
Inefficient |
Custom PMD XPath | 4 | Test for a blank string with String.isBlank |
Expressions |
Ncss |
Stock PMD | 4 | A method has at most 40 statements, and a class at most 500 | Complexity and Unused Code |
Prefer |
Custom PMD XPath | 4 | Use the null coalescing operator for a default: name ?? 'none' |
Expressions |
Prefer |
Custom PMD XPath | 4 | Use the safe navigation operator for one call on an object that may be null: account?.close() |
Expressions |
Prefer |
Custom PMD XPath | 4 | Use switch, not an if / else if chain, for a string, an integer, an enum, or an sObject type |
Switch Statements |
Refactor |
Custom PMD XPath | 4 | Assertions use the Assert class, not System.assert |
Assertions |
Regex |
Custom PMD XPath | 4 | Compile a regular expression that runs more than once as a static final Pattern |
Efficient Code |
Require |
Custom PMD XPath | 4 | A getter that queries loads its value lazily | Lazy Loading |
Simplified |
Custom PMD XPath | 4 | No ternary expression with true or false as a result |
Expressions |
Simplify |
Custom PMD XPath | 4 | Return a condition directly, not through an if that returns true or false |
Expressions |
Simplify |
Custom PMD XPath | 4 | No null check next to instanceof |
Expressions |
Soql |
Stock Code Analyzer ApexGuru | 4 | No NOT IN or != in a WHERE clause; they cannot use an index |
Queries |
Soql |
Stock Code Analyzer ApexGuru | 4 | Serve a query that runs often, on data that rarely changes, from Platform Cache | Queries |
Soql |
Stock Code Analyzer ApexGuru | 4 | Select only the fields the code reads | Queries |
Soql |
Stock Code Analyzer ApexGuru | 4 | No LIKE filter that starts with a wildcard; it cannot use an index |
Queries |
Sorting |
Stock Code Analyzer ApexGuru | 4 | Sort records with ORDER BY, not in Apex after the query |
Queries |
Switch |
Custom PMD XPath | 4 | Every switch has a when else block |
Switch Statements |
Too |
Stock PMD | 4 | Split a class with too many fields; related fields belong in a type of their own | Complexity and Unused Code |
Unimplemented |
Stock Code Analyzer SFGE | 4 | An abstract class or an interface that is not global has at least one implementation |
Architecture |
Unit |
Custom PMD XPath | 4 | A test method calls Test.startTest() and Test.stopTest() |
Test Methods |
Unused |
Custom PMD XPath | 4 | A null check before equals checks the object whose method is called |
Null Checks and Conditions |
Use |
Custom PMD XPath | 4 | Test for an empty collection with isEmpty() |
Expressions |
Using |
Stock Code Analyzer ApexGuru | 4 | No testMethod keyword; mark a test method with @IsTest |
Test Methods |
apexguru-other |
Stock Code Analyzer ApexGuru | 5 | A finding of a new ApexGuru rule is reported for information until these standards place it | ApexGuru |
Detect |
Stock Code Analyzer CPD | 5 | No copied blocks of Apex; share a method or a base class | Architecture |
No |
Stock Code Analyzer Regex | 5 | No line ends in a space or a tab, and no line is only whitespace | Formatting |
No |
Custom PMD XPath | 5 | Return a value directly: return calculate();, not a variable that is set and then only returned |
Expressions |
No |
Custom Code Analyzer Regex | 5 | No TODO, TO-DO, FIXME, FIX-ME, or HACK comment stays in Apex |
Documentation |
Queueable |
Stock PMD | 5 | A Queueable job whose failure needs a recovery action attaches a finalizer | Asynchronous Apex |
Test |
Custom PMD XPath | 5 | A test class states IsParallel=true or IsParallel=false |
Test Classes |