Skip to content

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 Limits class say how much of each limit the transaction has used and how much it is allowed, and the OrgLimits class 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, empApi components, 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 ConnectApi calls 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.new or Trigger.old by a fixed index, such as Trigger.new[0]. Such code handles the first record of the batch and ignores the others, and nothing fails to show it. A fixed index on Trigger.new or Trigger.old, in a trigger or in any class, blocks the pull request (severity 2). Enforced by AvoidDirectAccessTriggerMap. 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, Database method, 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 by OperationWithLimitsInLoop. Also enforced by the Graph Engine rule AvoidDatabaseOperationInLoop (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 by DmlInLoopAcrossMethods (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 rules SoqlInALoop (severity 2), SoqlInALoopOneHop (severity 3), which looks one method call away from the loop, and DmlInALoop (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 to EventBus.publish for a platform event that is published after commit, and every write through a class of the ConnectApi namespace (Execution Governors and Limits). None of them runs inside a loop, and platform events are published as one list in one call. OperationWithLimitsInLoop reports the first four in a loop. It does not report EventBus.publish or a ConnectApi call, 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 by OperationWithLimitsInLoop.
  • 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 WHERE clause or a LIMIT. 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 by AvoidNonRestrictiveQueries. 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 rule SoqlWithoutAWhereClauseOrLimitStatement (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 1 does 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 without ORDER BY the 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 rule Soql Aggregation. A count taken with size() on a query, which loads every record only to count it, is also reported at severity 3. Enforced by AvoidSizeOfSoql.
  • 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 WHERE clause, and not in Apex after it. A query that returns more records than the code wants, so that an if in 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 rule SoqlWithApexFilter.
  • 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 rule SortingInApex.
  • A filter is written so that its query can use an index. NOT IN and != cannot, and neither can a LIKE whose pattern starts with a wildcard, so each of them reads the whole table (Large Data Volumes). A negative expression in a WHERE clause 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, and Database.getQueryLocator, and the WithBinds form 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 to Limits.getHeapSize() inside a loop blocks the pull request (severity 2). Enforced by AvoidGetHeapSizeInLoop. Also enforced by the ApexGuru rule LimitsGetHeapsizeMethods (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() or Schema.describeSObjects(), is made once before a loop, never inside it. A call inside a loop is reported at severity 3. Enforced by OperationWithHighCostInLoop. Also enforced by the Graph Engine rule AvoidMultipleMassSchemaLookups (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, where Schema.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 rule SchemaGetGlobalDescribeNotEfficient.
  • 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 with putAll(), never with add or put in a loop over the source. A copy made in a loop is reported at severity 3. Enforced by the ApexGuru rules CopyingListOrSetElementsUsingAForLoop and SObjectMapInAForLoop.
  • A string is compared with equals or equalsIgnoreCase, 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 rule ExpensiveStringComparison.
  • 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(), and fieldSets.getMap(). A call that uses a variable of the loop, such as field.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 by AvoidDescribeInLoops.
  • 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 a Pattern object, and Pattern.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 other String methods that take a regular expression are not reported. Enforced by RegexPatternsMustBeStaticFinal.
  • 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 by EagerlyLoadedDescribeSObjectResult.

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.

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 reads SELECT and select alike (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, for Database.query or Search.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, or do. Apex code coverage counts lines, so a line that holds two statements, or a one-line block such as if (x > 0) { a = 5; }, counts as covered when only part of it ran. Prettier never writes these forms, so a report usually means that a prettier-ignore comment kept the line. Such a line blocks the pull request (severity 2). Enforced by StatementsShouldBeOnSeparateLines.
  • 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, not 1.15 (Constants). Enforced by AvoidMagicNumbers.
  • A default for a value that may be null is written with the null coalescing operator: name ?? 'none', not name != 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' becomes obj?.getName() ?? 'none', which also gives the default when getName() itself returns null. Enforced by PreferNullCoalescingOverTernary.
  • A single call or read on an object that may be null uses the safe navigation operator: account?.close(), not if (account != null) { account.close(); } (Safe Navigation Operator). An if that only checks for null before one such statement is reported at severity 4. Enforced by PreferSafeNavigationOperator.
  • A value that is only read is a property, not a field with a getter method: public String name { get; private set; }, not a private name and a getName() 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 by AvoidTrivialPropertyGetters.

// 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 by NoUnnecessaryReturnVariables.
// 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), and x >= y, not !(x < y). A negated comparison is reported at severity 3. Enforced by LogicInversionApex.
  • A condition should be returned directly: return x > 0;, not an if that returns true in one branch and false in the other. Such an if is reported at severity 4. Enforced by SimplifyBooleanReturnsApex.
  • A ternary expression should not have true or false as a result: flag || other, not flag ? true : other. It is reported at severity 4. Enforced by SimplifiedTernaryApex.
  • A null check next to instanceof should be removed: o instanceof Account, not o != null && o instanceof Account. From API version 32.0, instanceof returns false for null (Using the instanceof Keyword). Such a check is reported at severity 4. Enforced by SimplifyConditionalApex.
  • A blank string should be tested with String.isBlank(s), which returns true for white space, an empty string, or null (String Class), not with s.trim().length() == 0, s.trim().isEmpty(), or s.trim() == ''. Such a test is reported at severity 4. Enforced by InefficientEmptyStringCheckApex.
  • An empty collection should be tested with isEmpty(): xs.isEmpty(), not xs.size() == 0, and !xs.isEmpty(), not xs.size() > 0. A comparison of size() with zero is reported at severity 4. Enforced by UseCollectionIsEmptyApex.

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, or HACK in 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 by NoUnresolvedTodosApex.
  • 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 @SuppressWarnings annotation, a // NOPMD comment, or a code-analyzer-suppress marker blocks the pull request (severity 2) until the violation is fixed or the owning team approves the suppression. Enforced by NoUnapprovedSuppression.

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 by ApexDoc. 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 @description tag, which is not in Salesforce's list of tags, and both are accepted. A comment with no description is reported at severity 4. Enforced by ApexDoc. 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 as TBD is reported at severity 4. Enforced by ApexDocDescriptionLength.
  • 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 by NoColonAfterApexDocTag.
  • A class's ApexDoc comment has an @author tag, so that a reader knows whom to ask about the class. Salesforce recommends @author on 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 the ApexDoc rule. No rule checks for the @author tag: 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.SFDoc as 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 by NoSfdocDefaultAuthor.
  • An ApexDoc comment has no @last modified tag, such as @last modified on or @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 by NoLastModifiedApexDocTag.
  • 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 @param that is misnamed or out of order, is reported at severity 4. Enforced by ApexDoc.
  • A method's comment describes the value that the method returns with @return. A method that returns a value has one, and a void method 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 @return is reported at severity 4. Enforced by ApexDoc.
  • 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 by ExceptionDocumentationRequired.
  • 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() and name(), 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 @since and @see (Document Apex Constructs and Features).
  • Every other tag is optional. The one exception is @deprecated on an element with the @Deprecated annotation, below.
  • The @since tag, the version or the date that the thing was introduced, is optional. Salesforce recommends it on a class.
  • The @version tag, the version of a class, an interface, or an enum, is optional. Salesforce recommends it on a class.
  • The @group tag, the group that the thing belongs to in generated documentation, is optional. Salesforce recommends it on a class.
  • The @see tag, a reference to another class, method, etc., is optional. Salesforce recommends it on a class. Multiple @see tags 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 @example tag, a usage example, is optional. The example is formatted as code when it is in a {@code} tag (ApexDoc Comment Structure and Tags).
  • The @deprecated tag marks a thing as deprecated and gives the reason and the alternative. Every element with the @Deprecated annotation 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 @deprecated tag, 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 by DeprecatedDocumentationRequired.
  • 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 @see tag (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 @param or @return, because ApexDoc reads 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 NoMixedIndentation rule 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 sample code-analyzer.yml does.

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 if statements are each reported at severity 3, at PMD's default thresholds. An organisation may change these thresholds in each rule's properties. Enforced by CyclomaticComplexity, CognitiveComplexity, and AvoidDeeplyNestedIfStmts.
  • Nested if statements with nothing else in the outer one are combined into one if with &&: if (a && b) { ... }, not if (a) { if (b) { ... } }. This applies only when neither if has an else. It is reported at severity 4, and a reviewer may accept two conditions that read better apart. Enforced by CombineNestedIfStatements.
// 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 methodReportLevel and classReportLevel properties in its ruleset. Enforced by NcssCount.
  • 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 UnusedLocalVariable and UnusedMethod. UnusedMethod reports 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 switch instead of if / else if for 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 an if chain compares strings that it cannot check. A chain that compares one value with three or more values that a when clause can hold is reported at severity 4; the minConditions property sets the number. The change is not always word for word: == is case-insensitive, and a when value of type String is case-sensitive (Switch Statements), so a chain that relies on that changes its behaviour as a switch. Enforced by PreferSwitchOverIfElseChains.
  • Every switch statement has a when else block, 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. A when else that does nothing holds a comment that says so. A switch with no when else is reported at severity 4. Enforced by SwitchWithoutWhenElse.
  • A switch statement should not hold another switch statement. The inner one moves to a method of its own, whose name says what it decides. A nested switch is reported at severity 3. Enforced by AvoidNestedSwitchStmts.

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, or finally block is empty. A caught exception is handled, logged, or thrown again, and a comment does not make an empty catch block acceptable. An empty try or finally block protects nothing. An empty try, catch, or finally block blocks the pull request (severity 2). Enforced by EmptyCatchBlock and EmptyTryOrFinallyBlock.
  • No if block is empty. An empty if tests a condition and ignores the result. An empty if block blocks the pull request (severity 2). Enforced by EmptyIfStmt.
  • No while block is empty. An empty while does its work, if any, in its condition, where a reader does not look for it. An empty while block blocks the pull request (severity 2). Enforced by EmptyWhileStmt.
  • 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 else that 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 empty if, while, try, or finally block, so such a block is reported twice. Enforced by EmptyStatementBlock.
  • 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 @InvocableVariable member 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 by RequireInvocableNoArgConstructor, which reads the class's API version from its -meta.xml file. At any API version, and where the version cannot be read, such a class blocks the pull request at severity 2. Enforced by InvocableClassNoArgConstructor. 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 empty if block (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 overrides hashCode(), and a class that overrides hashCode() also overrides equals(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 by OverrideBothEqualsAndHashcode.
  • A Map does not use an interface as its key type where an abstract class implements that interface and defines equals or hashCode. Methods such as containsKey and get do not call that class's equals or hashCode for such a key, so the map can hold two equal keys, or fail to find a key that it holds. Such a Map, as a field, a variable, or a parameter, is reported at severity 3. Enforced by AvoidInterfaceAsMapKey, which reports only when the environment variable PMD_APEX_ROOT_DIRECTORY is set (Appendix E).

Null Checks and Conditions

  • A null check protects the expression after it. x != null || x.size() > 0 and x == null && x.isEmpty() read x exactly 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 by BrokenNullCheckApex.
  • A null check before equals() should check the object whose method is called: a != null && a.equals(b), not a != null && b.equals(a), where b can still be null. Such a check is reported at severity 4. Enforced by UnusedNullCheckInEqualsApex.
  • A comparison with a string literal should call the method on the literal: 'abc'.equals(s), not s.equals('abc'), which throws when s is null. The == operator does not throw, and is not reported. The other form is reported at severity 3. Enforced by PositionLiteralsFirstInComparisons.
  • A method that returns a List, a Set, a Map, or an array should return an empty one, not null, so that every caller can loop over the result without a null check. return null in such a method is reported at severity 3. Enforced by ReturnEmptyArrayRatherThanNull.
  • Each branch of an if / else if chain 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 by FixDuplicateConditions.
  • An if condition should not be true, false, or a comparison of two literals, which is always the same: the block always runs or never runs. Code that was switched off with if (false) is removed. Such a condition is reported at severity 3. A constant that holds such a value is not seen. Enforced by UnconditionalIfStatementApex.
  • An assignment should change something. count = count and this.name = this.name assign a value to itself and do nothing, usually in place of this.name = name. Such an assignment is reported at severity 3. Enforced by IdempotentOperationsApex.

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; the allowedNumbers property lists the numbers that may be written out. A list index, a number in a loop condition, a when value, and a number in a test class are not reported. Enforced by AvoidMagicNumbers.

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.

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, or inherited 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 Database or Search method 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 sharing by default, inherited sharing for utility classes that should follow their caller. A missing declaration blocks the pull request (severity 1). Enforced by RequireExplicitSharing.
  • 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 sharing is a deliberate, justified exception: it blocks the pull request (severity 2). Enforced by DoNotUseWithoutSharing.
  • 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 @AuraEnabled method, to each query and DML statement, and reports one that runs in a without sharing class, 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 rule DatabaseOperationsMustUseWithSharing (Developer Preview).
  • Every database operation states its access mode: inline SOQL and SOSL, Database and Search methods, and DML statements, in classes, triggers, and test classes. A missing access mode blocks the pull request (severity 2). Enforced by the Java rule RequireAccessMode.
  • 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 rule ApexFlsViolation (Developer Preview), which follows the call path from an entry point, such as an @AuraEnabled method, 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, or Security.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, or Security.stripInaccessible on 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; stripInaccessible in 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_ENFORCED is not accepted: use WITH USER_MODE. It checks the WHERE clause as well as the selected fields, handles polymorphic fields such as Owner, and reports every field-level security violation instead of only the first. WITH SECURITY_ENFORCED in a query, or in a string that holds part of a query, blocks the pull request (severity 2). Enforced by NoSecurityEnforced.
  • 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:

  1. Declare the class's sharing as it behaves today.
  2. State the access mode on every database operation, which RequireAccessMode already requires. Where the class relies on system mode today, write it explicitly; FlagSystemMode then lists every such operation for review.
  3. Review each system-mode operation and change it to user mode unless it has a real justification.
  4. 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.
  5. 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.

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 private unless code outside its class has to use it. protected, public, and global each 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 @AuraEnabled property is public or global. Since the Summer '21 release, a Lightning web component or an Aura component cannot read a private or protected getter, 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. A private or protected getter of an @AuraEnabled property, and an @AuraEnabled property that is private, are reported at severity 3. Enforced by InaccessibleAuraEnabledGetter.
  • A test reaches a private or protected member through the @TestVisible annotation 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 made public for a test is open to all the Apex in its namespace. No rule checks this. Reviewers do.
  • A class or a member is global only where code in another namespace has to call it, or where the platform requires it, as it does for an Apex REST class. A global declaration is reported at severity 4. Enforced by AvoidGlobalModifier.

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.debug statements, in production code or in a test class. A debug statement blocks the pull request (severity 2); enforced by the PMD rule AvoidDebugStatements. A debug statement whose argument is expensive to build, such as a serialised record, is also enforced by the ApexGuru rule ExpensiveDebugStatements (severity 2).
  • No debug statement writes a credential. A debug statement that writes a value whose name says that it holds one, such as password or sessionToken, 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 by ApexDangerousMethods. The same rule reports a call to Configuration.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 rule DebugsShouldUseLoggingLevel.
  • 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 DmlException and QueryException messages 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 by AvoidUnauthorizedGetSessionIdInApex.
  • 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 by AvoidUnauthorizedApiSessionIdInApex, which reads the form without the dollar sign, and AvoidApiSessionIdInApexString, 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 isSecure set to true, so that it can only be accessed through HTTPS (Cookie Class). A cookie created with isSecure set to false is reported at severity 3. Enforced by AvoidInsecureCookies.
  • A digest, a message authentication code, or a signature is made with an algorithm of the SHA-2 family, such as SHA-256, hmacSHA256, or RSA-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 the Crypto class that names MD5 or SHA-1 is reported at severity 3. Enforced by AvoidWeakCryptoAlgorithms.
  • 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 to Crypto.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. A Crypto call with a key or a vector made from a text literal blocks the pull request (severity 1). Enforced by ApexBadCrypto.
  • 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, or Crypto.decryptWithManagedIV blocks the pull request (severity 2) until a reviewer accepts it. It marks a place for review, not a defect. Enforced by HotspotEncrypt.

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.queryWithBinds or a :name in 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.escapeSingleQuotes is 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 by ApexSOQLInjection.

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 String class such as escapeHtml4(), 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 by ApexXSSFromURLParam.
  • An error message that addError shows is escaped, which is the default. Where a message has HTML markup, and so is given with escaping turned off (false as 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 to addError with escaping turned off and a message built from a variable blocks the pull request (severity 2). Enforced by ApexXSSFromEscapeFalse.

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 the Database class. 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 by ApexCSRF. The rule reads DML statements such as insert and update. It does not read a Database method such as Database.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 PageReference is built from a page reference such as Page.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. A PageReference built from a URL parameter blocks the pull request (severity 2). Enforced by ApexOpenRedirect.

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 Referer header. 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 by AvoidHttpReferer.
  • 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, or JSON.createParser (JSON Class) blocks the pull request (severity 2) until a reviewer accepts it. It marks a place for review, not a defect. Enforced by HotspotDeserializeJson.

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. An http:// endpoint blocks the pull request (severity 2). Enforced by ApexInsecureEndpoint.
  • 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 by ApexSuggestUsingNamedCred.

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 String variable whose name says that it holds a credential, such as password, apiKey, or sessionToken, and that is given a text literal blocks the pull request (severity 2). Enforced by AvoidHardcodedCredentialsInFieldDecls, AvoidHardcodedCredentialsInVarDecls, and AvoidHardcodedCredentialsInVarAssign. The {N} rules read the name, not the value, so they also report a harmless literal under such a name, such as HASH_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 setHeader that gives a text literal to a header such as Authorization blocks the pull request (severity 2). The word Bearer joined to a variable is not reported, and neither is a merge field of a Named Credential. Enforced by AvoidHardcodedCredentialsInHttpHeader.
  • 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 by AvoidHardcodedCredentialsInSetPassword.

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 by EmailAddressInLiteral.

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 from URL.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 by AvoidAbsoluteSalesforceUrl. 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 through UserInfo.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 by AvoidHardcodingId. 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 DeveloperName or by its Id, never by its Name. This covers a query on RecordType, a filter on a record's record type, such as WHERE RecordType.DeveloperName = 'Partner', and a comparison in code. The Id is read from the describe result, with getRecordTypeInfosByDeveloperName(), which needs no query (DescribeSObjectResult Class), or from a query that filters on the DeveloperName. 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, where getRecordTypeInfosByDeveloperName() looks it up by the name that does not change. A call to it is reported at severity 3. Enforced by AvoidRecordTypeInfosByName.

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 rule WritingFillerStatements.

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 of ConnectApi, 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 by ApexUnitTestShouldNotUseSeeAllDataTrue.

    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=true that calls a ConnectApi method itself, and a test class with SeeAllData=true in 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 ConnectApi methods have a test method of their own, with the same name after a setTest prefix, and with the same parameters and one more, the result. A test builds the output that it expects and registers it with the setTest method, and the regular method then returns that output in the test, without reading org data. A few methods, such as ChatterFeeds.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. Most ConnectApi methods have no setTest method, among them the common writes, such as ChatterFeeds.postFeedElement, so a test that also calls one of those still needs the annotation. Another way is to keep ConnectApi out of the test: the class calls ConnectApi through an interface, and the test gives the class a mock of that interface (Testing Chatter in Apex without seeAllData=true).

    These classes have setTest methods. Point at a class to see the names of its setTest methods, 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=true runs 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, calls Test.getStandardPricebookId(), System.schedule(), or System.enqueueJob(), or creates a User, a GroupMember, a ContentNote, 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 by TestClassIsParallel.

  • 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 by TestClassWithoutTestCasesApex.

Test Methods

  • Every test method is in a class that carries the @IsTest annotation 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 by TestMethodsMustBeInTestClasses.
  • A test method is marked with the @IsTest annotation, not the testMethod keyword. The keyword blocks the pull request (severity 2). Enforced by ApexUnitTestMethodShouldHaveIsTestAnnotation. Also enforced by the ApexGuru rule UsingTheTestMethodKeyword (severity 4), a second layer over the PMD rule.
  • A test method should call Test.startTest() and Test.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 when Test.stopTest() is called. A test method that does not call both is reported at severity 4. Enforced by UnitTestsShouldIncludeStartTest.

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 by AvoidUsingTestIsRunningTest.

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 @IsTest annotation carries testFor, naming the classes and triggers it tests, or critical=true. A test class with neither blocks the pull request (severity 2). Enforced by RequireTestForOrCritical.
  • 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.forName or 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, because testFor cannot 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.runAs for 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 no System.runAs at all blocks the pull request (severity 2). Enforced by ApexUnitTestClassShouldHaveRunAs. Whether every persona is covered is for reviewers to check. Code that calls ConnectApi is no exception. The Apex Developer Guide's Testing ConnectApi Code states that Connect in Apex does not support the runAs system method, but tests in scratch orgs at API 68.0 showed that this is not the case: a ConnectApi post and read inside System.runAs ran without an error, and as the runAs user. Such tests still need SeeAllData=true, unless every ConnectApi method that they call has a setTest method (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=true ran 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 declared with 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=true Where the ConnectApi call ran Result
    A post (ChatterFeeds.postFeedElement) and a read (ChatterUsers.getUser(null, 'me')) Yes Outside System.runAs Passed, as the user who started the test
    The same post and read Yes Inside System.runAs with the current user Passed, with no exception, as the user who started the test
    The same post and read Yes Inside System.runAs with the other user Passed, with no exception. The post's actor, its CreatedById, and the user that getUser returned were the other user
    A post before and after a System.runAs block that holds no ConnectApi code Yes Outside the block Passed, as the user who started the test
    setTestGetFeedElementsFromFeed, then getFeedElementsFromFeed No Outside System.runAs, and inside it with each user Passed: the registered page was returned every time
    The read (ChatterUsers.getUser(null, 'me')) No Outside System.runAs, and inside it with the other user Failed, with and without System.runAs: System.UnsupportedOperationException: ConnectApi methods are not supported in data siloed tests. Please use @IsTest(SeeAllData=true).

    So System.runAs works with ConnectApi: it does not throw, and it is not ignored, because ConnectApi acts as the runAs user. The only exception came from a missing SeeAllData=true. The tests cover two ConnectApi methods, a post and a read, and do not show that every ConnectApi method 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 by DoNotRunAsCurrentUser.

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 by ApexAssertionsShouldIncludeMessage.
  • Assertions should use the Assert class, such as Assert.areEqual and Assert.isTrue, not the older System.assert, System.assertEquals, and System.assertNotEquals. The older forms are reported at severity 4. Enforced by RefactorAssert.
  • An assertion is only in test code. A failed assertion cannot be caught with try and catch (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 by NonUnitTestContainsAsserts.
  • 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, or null written into it, as in Assert.isTrue(true), Assert.isNull(null), and Assert.areEqual(true, true). Enforced by UnnecessaryBooleanAssertionApex.
    • Any other value written into it: a string, a number, or a new instance in Assert.isNull or Assert.isNotNull, as in Assert.isNotNull(new List<String>()) and Assert.isNotNull('x'), and a literal on both sides of Assert.areEqual or Assert.areNotEqual, as in Assert.areEqual(1, 1). Enforced by MisuseOfAssertClass. A new instance as the expected value of Assert.areEqual is a correct test and is not reported, as in Assert.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.areEqual or Assert.areNotEqual, which show both values on failure, not with Assert.isTrue(a == b) or Assert.isTrue(a.equals(b)), which show only that the result was false. A Boolean is checked with Assert.isTrue or Assert.isFalse, not with Assert.areEqual(true, flag). Each is reported at severity 3. Enforced by UseAreEqualInsteadOfAssertBoolean, UseAreEqualInsteadOfIsTrue, and UseIsTrueInsteadOfAreEqual.
  • 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.

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 @future annotation is reported at severity 4. Enforced by AvoidFutureAnnotation.
  • 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 by QueueableWithoutFinalizer.
  • 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.Stateful keeps no Database result in an instance field: no SaveResult, UpsertResult, DeleteResult, MergeResult, UndeleteResult, or EmptyRecycleBinResult, 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 declared transient. Such a field is reported at severity 3. Enforced by AvoidStatefulDatabaseResult.
  • 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

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 try block can throw, and a catch block names them: it catches a specific standard exception type, such as DmlException or QueryException, wherever possible (Exception Class and Built-In Exceptions).
  • Catching the generic Exception type 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 NullPointerException is 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 rule ApexNullPointerException (Developer Preview).
  • A variable used in the WHERE clause of a query is checked for null first. With a null value, WHERE Name = :name matches 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 rule MissingNullCheckOnSoqlVariable (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: BillingConstants for 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 public or global method or constructor takes no Boolean parameter that switches what it does. A call such as process(records, true) does not say what true means, and a method that a flag switches often does two things. Two methods with clear names, or an enum, say it. A Boolean that is data, such as the value of a checkbox that a component sends, is fine. Each Boolean parameter of a public or global method or constructor is reported at severity 4, as a sign to look at the method again. Enforced by AvoidBooleanMethodParameters.
  • An abstract class or an interface that is not global has 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 rule UnimplementedType (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
AbstractClassWithoutAnyMethodApex Custom PMD XPath 1 An abstract class declares a method or a constructor Abstract Classes
ApexBadCrypto 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
ApexSuggestUsingNamedCred Stock PMD 1 Do not set the Authorization header directly; use a Named Credential Callouts
AvoidHardcodedCredentialsInSetPassword Stock Code Analyzer PMD 1 No password is written into a call to System.setPassword Hard-Coded Credentials
AvoidHardcodedIpAddressApex 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
AvoidHardcodingId Stock PMD 1 No record ID is written into Apex; find the record by DeveloperName, an external ID, or a name Record IDs
EmailAddressInLiteral Custom PMD XPath 1 No email address written into Apex Hard-Coded Email Addresses
MultipleTriggersOnObject Custom PMD Java 1 Each object has at most one trigger Triggers
OneDeclarationPerLine Stock PMD 1 Declare each variable on its own line Formatting
RequireExplicitSharing 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
RequireInvocableNoArgConstructor 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
RequireOuterClassAccess Custom PMD XPath 1 Every outer class declares its access modifier Access Modifiers
TestMethodsMustBeInTestClasses Stock PMD 1 Every test method is in a class that carries @IsTest Test Methods
ApexDangerousMethods Stock PMD 2 No debug statement writes a credential, and no call to Configuration.disableTriggerCRUDSecurity() Debug Output and Error Disclosure
ApexFlsViolation 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
ApexInsecureEndpoint Stock PMD 2 A callout goes over HTTPS; no http:// endpoint Callouts
ApexNullPointerException Stock Code Analyzer SFGE 2 No operation on a value that may be null; assign it or check it first Exception Handling
ApexOpenRedirect Stock PMD 2 A redirect goes to a page that the code chooses, never to a URL parameter as it is Open Redirects
ApexSOQLInjection Stock PMD 2 No input is joined into a query string; use bind variables, an allowlist, or String.escapeSingleQuotes SOQL Injection
ApexUnitTestClassShouldHaveAsserts Stock PMD 2 Every test method asserts the behaviour it exercises Assertions
ApexUnitTestClassShouldHaveRunAs Stock PMD 2 A test calls System.runAs for every user persona that reaches the code Running As a User
ApexUnitTestMethodShouldHaveIsTestAnnotation Stock PMD 2 No testMethod keyword; mark a test method with @IsTest Test Methods
ApexUnitTestShouldNotUseSeeAllDataTrue Stock PMD 2 Do not use @IsTest(SeeAllData=true); a test creates the data it needs Test Classes
ApexXSSFromEscapeFalse Stock PMD 2 An addError message that is not escaped holds no value that a user entered Cross-Site Scripting
ApexXSSFromURLParam Stock PMD 2 Escape or validate a value read from a URL parameter before the class uses or returns it Cross-Site Scripting
AvoidApiSessionIdInApexString Custom PMD XPath 2 Apex does not write {!$Api.Session_ID} into a string Session Ids
AvoidDatabaseOperationInLoop Stock Code Analyzer SFGE 2 No query or DML statement inside a loop, in the loop or in a method it calls Bulkification
AvoidDebugStatements Stock PMD 2 No System.debug statements, in production code or in a test class Debug Output and Error Disclosure
AvoidDirectAccessTriggerMap Stock PMD 2 No fixed index on Trigger.new or Trigger.old, such as Trigger.new[0] Bulkification
AvoidGetHeapSizeInLoop Stock Code Analyzer Regex 2 No Limits.getHeapSize() inside a loop CPU Time and Heap
AvoidHardcodedCredentialsInFieldDecls Stock Code Analyzer PMD 2 No credential in a field declaration; keep passwords, keys, and tokens in an External Credential Hard-Coded Credentials
AvoidHardcodedCredentialsInHttpHeader Stock Code Analyzer PMD 2 No credential is written into a request header Hard-Coded Credentials
AvoidHardcodedCredentialsInVarAssign Stock Code Analyzer PMD 2 No credential assigned to a variable as a text literal Hard-Coded Credentials
AvoidHardcodedCredentialsInVarDecls Stock Code Analyzer PMD 2 No credential in a variable declaration as a text literal Hard-Coded Credentials
AvoidHttpReferer Custom PMD XPath 2 No security decision from the HTTP Referer header Untrusted Input
AvoidLogicInTrigger Stock PMD 2 A trigger holds no logic: no if, switch, loop, or try block Triggers
AvoidMultipleMassSchemaLookups Stock Code Analyzer SFGE 2 No Schema.getGlobalDescribe() or Schema.describeSObjects() inside a loop, or twice on one path Efficient Code
AvoidNonRestrictiveQueries Stock PMD 2 Every SOQL query and SOSL search has a WHERE clause or a LIMIT Queries
AvoidOldSalesforceApiVersions Stock Code Analyzer Regex 2 Every metadata file states a recent API version API Version
AvoidUnauthorizedApiSessionIdInApex Stock Code Analyzer PMD 2 Apex does not write the session Id into a string Session Ids
AvoidUnauthorizedGetSessionIdInApex Stock Code Analyzer PMD 2 Apex does not read the session Id; no UserInfo.getSessionId() Session Ids
BrokenNullCheckApex Custom PMD XPath 2 A null check does not read the value it checks for null Null Checks and Conditions
BumpApiVersion Custom PMD XPath 2 Every metadata file is at or above the API version floor Notes on this rule. API Version
DatabaseOperationsMustUseWithSharing Stock Code Analyzer SFGE 2 Every database operation runs under a stated sharing declaration along the whole call path Sharing and Access Mode Standards
DeclareAccessOnMethods Custom PMD XPath 2 Every method except a test method declares its access modifier Notes on this rule. Access Modifiers
DmlInALoop Stock Code Analyzer ApexGuru 2 No DML statement inside a loop Bulkification
DmlInLoopAcrossMethods Custom PMD XPath 2 No call inside a loop to a method that runs DML Bulkification
DoNotRunAsCurrentUser Custom PMD XPath 2 A test never runs as the current user Running As a User
DoNotUseWithoutSharing Custom PMD XPath 2 without sharing is a deliberate, justified exception Sharing and Access Mode Standards
EagerlyLoadedDescribeSObjectResult Stock PMD 2 Load describe results lazily with SObjectDescribeOptions.DEFERRED Efficient Code
EmptyCatchBlock Stock PMD 2 No empty catch block Empty Blocks
EmptyIfStmt Stock PMD 2 No empty if block Empty Blocks
EmptyStatementBlock 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
EmptyTryOrFinallyBlock Stock PMD 2 No empty try or finally block Empty Blocks
EmptyWhileStmt Stock PMD 2 No empty while block Empty Blocks
ExpensiveDebugStatements Stock Code Analyzer ApexGuru 2 No debug statement whose argument is expensive to build Debug Output and Error Disclosure
ExpensiveMethods Stock Code Analyzer ApexGuru 2 Refactor a method that uses a large share of the org's CPU time Efficient Code
FlagSystemMode 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
ForLoopsMustUseBraces Stock PMD 2 Wrap the body of every for loop in braces, even one line Braces
HotspotDeserializeJson Custom PMD XPath 2 Every place that reads JSON is reviewed Untrusted Input
HotspotEncrypt Custom PMD XPath 2 Every call that encrypts or decrypts is reviewed Cookies and Cryptography
IfElseStmtsMustUseBraces Stock PMD 2 Wrap the body of every else in braces, even one line Braces
IfStmtsMustUseBraces Stock PMD 2 Wrap the body of every if in braces, even one line Braces
InvocableClassNoArgConstructor Stock PMD 2 A class with an @InvocableVariable member and an explicit constructor also has a visible no-argument constructor Constructors
LimitsGetHeapsizeMethods Stock Code Analyzer ApexGuru 2 Limits.getHeapSize() is not called often CPU Time and Heap
MinVersionForAbstractVirtualClassesWithPrivateMethod Stock Code Analyzer Regex 2 An abstract or virtual class with a private method is at API 61.0 or later API Version
MisuseOfAssertClass Custom PMD XPath 2 No assertion fixed by a string, a number, or a new instance written into it Assertions
NoColonAfterApexDocTag Custom PMD XPath 2 No colon between an ApexDoc tag and its text ApexDoc
NoCredentialNamedField 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
NoLastModifiedApexDocTag Custom PMD XPath 2 No @last modified tag, such as @last modified on, in an ApexDoc comment ApexDoc
NoMethodsInTriggers Custom PMD XPath 2 A trigger declares no methods Triggers
NonUnitTestContainsAsserts Custom PMD XPath 2 Assertions only in test code Assertions
NoSecurityEnforced Custom PMD XPath 2 Do not use WITH SECURITY_ENFORCED; use WITH USER_MODE Sharing and Access Mode Standards
NoSfdocDefaultAuthor Custom Code Analyzer Regex 2 The author is a person or a team, never a tool default such as ChangeMeIn@UserSettingsUnder.SFDoc ApexDoc
NoUnapprovedSuppression Custom Code Analyzer Regex 2 No PMD or Code Analyzer suppression without an approving review from the owning team Documentation
NullValueCheckBeforeEmptyCheck Custom PMD XPath 2 Not yet written Not yet written
OperationWithLimitsInLoop Stock PMD 2 No SOQL, SOSL, DML, Database method, email send, or asynchronous call inside a loop Bulkification
Asynchronous Apex
OverrideBothEqualsAndHashcode Stock PMD 2 A class that overrides equals(Object) also overrides hashCode(), and the reverse Equality
RequireAccessMode 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
RequireTestForOrCritical 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
SchemaGetGlobalDescribeNotEfficient Stock Code Analyzer ApexGuru 2 Avoid Schema.getGlobalDescribe(); describe only the objects needed Efficient Code
SoqlInALoop Stock Code Analyzer ApexGuru 2 No SOQL query inside a loop Bulkification
SoqlWithApexFilter Stock Code Analyzer ApexGuru 2 Filter records in the WHERE clause, not in Apex after the query Queries
SoqlWithoutAWhereClauseOrLimitStatement Stock Code Analyzer ApexGuru 2 Every SOQL query has a WHERE clause or a LIMIT Queries
StatementsShouldBeOnSeparateLines Custom PMD XPath 2 Each statement on its own line Formatting
UnnecessaryBooleanAssertionApex Custom PMD XPath 2 No assertion fixed by true, false, or null written into it Assertions
WhileLoopsMustUseBraces Stock PMD 2 Wrap the body of every while loop in braces, even one line Braces
WritingFillerStatements Stock Code Analyzer ApexGuru 2 Never pad coverage with filler statements Coverage
ApexAssertionsShouldIncludeMessage Stock PMD 3 Every assertion includes a message that says what went wrong, Assert.fail() included Assertions
AvoidAbsoluteSalesforceUrl 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
AvoidDeeplyNestedIfStmts Stock PMD 3 No deeply nested if statements Complexity and Unused Code
AvoidDescribeInLoops Custom PMD XPath 3 Make a describe call whose result does not change once, before the loop Efficient Code
AvoidInsecureCookies Custom PMD XPath 3 Create a cookie with isSecure set to true Notes on this rule. Cookies and Cryptography
AvoidInterfaceAsMapKey Stock PMD 3 No interface as a Map key type where an abstract class implements it and defines equals or hashCode Equality
AvoidNestedSwitchStmts Custom PMD XPath 3 No switch inside a switch Switch Statements
AvoidNonExistentAnnotations Stock PMD 3 Every annotation is one that Apex defines Annotations
AvoidRecordTypeInfosByName Custom PMD XPath 3 Do not call getRecordTypeInfosByName(); use getRecordTypeInfosByDeveloperName() Record Types
AvoidSizeOfSoql Custom PMD XPath 3 Count records with an aggregate query, not size() on a query Queries
AvoidStatefulDatabaseResult Stock PMD 3 A Database.Stateful batch class keeps no Database result, such as a SaveResult, in an instance field Asynchronous Apex
AvoidWeakCryptoAlgorithms 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
BusyLoopDelay Stock Code Analyzer ApexGuru 3 No loop written to pass time; use a delayed Queueable job or a scheduled job CPU Time and Heap
CognitiveComplexity Stock PMD 3 Keep the cognitive complexity of a method low Complexity and Unused Code
CopyingListOrSetElementsUsingAForLoop Stock Code Analyzer ApexGuru 3 Copy a list or a set with addAll(), not element by element in a loop Efficient Code
CyclomaticComplexity Stock PMD 3 Keep the cyclomatic complexity of a method low Complexity and Unused Code
FixDuplicateConditions Custom PMD XPath 3 Each branch of an if / else if chain tests a different condition Null Checks and Conditions
IdempotentOperationsApex Custom PMD XPath 3 No assignment of a value to itself Null Checks and Conditions
InaccessibleAuraEnabledGetter Stock PMD 3 The getter of an @AuraEnabled property is public or global Access Modifiers
LogicInversionApex Custom PMD XPath 3 Negate a comparison with the opposite operator Expressions
MethodWithSameNameAsEnclosingClass Stock PMD 3 Only a constructor has the name of its class Constructors
MissingNullCheckOnSoqlVariable Stock Code Analyzer SFGE 3 Check a variable used in a WHERE clause for null first Exception Handling
MissingStaticMethodInNonInstantiatableClassApex Custom PMD XPath 3 A class with only private constructors has a public static member Constructors
NoConsecutiveBlankLines Custom Code Analyzer Regex 3 At most one blank line in a row Formatting
NoEmptyBlockComments Custom Code Analyzer Regex 3 No empty block comment Documentation
NoSingleLetterVariableName Custom PMD XPath 3 No one-letter variable names, except i and j as loop counters and e in a catch block General: Apex
OperationWithHighCostInLoop Stock PMD 3 Make an expensive call, such as Schema.describeSObjects(), once before a loop, never inside it Efficient Code
PositionLiteralsFirstInComparisons 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
RequireInnerClassSharing Custom PMD XPath 3 An inner class should declare its sharing too Sharing and Access Mode Standards
ReturnEmptyArrayRatherThanNull Custom PMD XPath 3 Return an empty collection, not null Null Checks and Conditions
SObjectMapInAForLoop 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
SoqlInALoopOneHop Stock Code Analyzer ApexGuru 3 No SOQL query in a method that a loop calls Bulkification
TestClassWithoutTestCasesApex Custom PMD XPath 3 Every test class has a test method, except a test helper Test Classes
TriggerNameSuffix 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
UnconditionalIfStatementApex Custom PMD XPath 3 No if condition that is always true or always false Null Checks and Conditions
UnusedLocalVariable Stock PMD 3 Remove a local variable that is never read Complexity and Unused Code
UnusedMethod Stock PMD 3 Remove a method that is never called Complexity and Unused Code
UseAreEqualInsteadOfAssertBoolean Custom PMD XPath 3 Compare two values with Assert.areEqual, not == in Assert.isTrue Assertions
UseAreEqualInsteadOfIsTrue Custom PMD XPath 3 Compare two values with Assert.areEqual, not equals() in Assert.isTrue Assertions
UseIsTrueInsteadOfAreEqual Custom PMD XPath 3 Check a Boolean with Assert.isTrue or Assert.isFalse, not Assert.areEqual Assertions
ApexDoc 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
ApexDocDescriptionLength Custom PMD XPath 4 Every ApexDoc description has at least 10 characters that say what the thing is for Notes on this rule. ApexDoc
AuraEnabledControllerNaming Custom PMD XPath 4 A class that a Lightning component calls ends in Controller General: Apex
Metadata Standards: Naming
AvoidBooleanMethodParameters Stock PMD 4 No Boolean parameter that switches what a public or global method does; use two methods or an enum Architecture
AvoidFutureAnnotation Stock PMD 4 Start asynchronous work as a Queueable job, not a future method Asynchronous Apex
AvoidGlobalModifier Stock PMD 4 global only where another namespace or the platform requires it Access Modifiers
AvoidMagicNumbers Custom PMD XPath 4 Declare a value once as a constant (static final); no magic numbers other than 0, 1, and -1 Expressions
Constants
AvoidNonSelectiveSoqlFilter 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
AvoidTrivialPropertyGetters Custom PMD XPath 4 A value that is only read is a property, not a field with a getter method Expressions
AvoidUsingTestIsRunningTest Custom PMD XPath 4 Production code does not call Test.isRunningTest() Testing Patterns
BooleanGetMethodNameApex Custom PMD XPath 4 A Boolean method is named as a question, not with get General: Apex
CombineNestedIfStatements Custom PMD XPath 4 Combine nested if statements with nothing else in the outer one into one if with && Complexity and Unused Code
DebugsShouldUseLoggingLevel Stock PMD 4 Any debug statement that remains specifies a LoggingLevel Debug Output and Error Disclosure
DeprecatedDocumentationRequired Custom PMD XPath 4 Every element with the @Deprecated annotation has a @deprecated tag with the reason and the alternative ApexDoc
EmptyInitializer Custom PMD XPath 4 No empty instance initializer block Empty Blocks
ExceptionDocumentationRequired Custom PMD XPath 4 A method comment names each exception it throws with @throws ApexDoc
ExpensiveStringComparison Stock Code Analyzer ApexGuru 4 Compare strings with equals or equalsIgnoreCase, not == Efficient Code
FieldDeclarationsShouldBeAtStart Stock PMD 4 Declare fields before the first method of a class Formatting
FieldNameMatchesClassName 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
FieldNameMatchesMethodName Custom PMD XPath 4 A field or property does not share its name with a method of its class General: Apex
InefficientEmptyStringCheckApex Custom PMD XPath 4 Test for a blank string with String.isBlank Expressions
NcssCount Stock PMD 4 A method has at most 40 statements, and a class at most 500 Complexity and Unused Code
PreferNullCoalescingOverTernary Custom PMD XPath 4 Use the null coalescing operator for a default: name ?? 'none' Expressions
PreferSafeNavigationOperator Custom PMD XPath 4 Use the safe navigation operator for one call on an object that may be null: account?.close() Expressions
PreferSwitchOverIfElseChains 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
RefactorAssert Custom PMD XPath 4 Assertions use the Assert class, not System.assert Assertions
RegexPatternsMustBeStaticFinal Custom PMD XPath 4 Compile a regular expression that runs more than once as a static final Pattern Efficient Code
RequireLazyLoadingInGetters Custom PMD XPath 4 A getter that queries loads its value lazily Lazy Loading
SimplifiedTernaryApex Custom PMD XPath 4 No ternary expression with true or false as a result Expressions
SimplifyBooleanReturnsApex Custom PMD XPath 4 Return a condition directly, not through an if that returns true or false Expressions
SimplifyConditionalApex Custom PMD XPath 4 No null check next to instanceof Expressions
SoqlWithNegativeExpressions Stock Code Analyzer ApexGuru 4 No NOT IN or != in a WHERE clause; they cannot use an index Queries
SoqlWithoutPlatformCache Stock Code Analyzer ApexGuru 4 Serve a query that runs often, on data that rarely changes, from Platform Cache Queries
SoqlWithUnusedFields Stock Code Analyzer ApexGuru 4 Select only the fields the code reads Queries
SoqlWithWildcardFilter Stock Code Analyzer ApexGuru 4 No LIKE filter that starts with a wildcard; it cannot use an index Queries
SortingInApex Stock Code Analyzer ApexGuru 4 Sort records with ORDER BY, not in Apex after the query Queries
SwitchWithoutWhenElse Custom PMD XPath 4 Every switch has a when else block Switch Statements
TooManyFields Stock PMD 4 Split a class with too many fields; related fields belong in a type of their own Complexity and Unused Code
UnimplementedType Stock Code Analyzer SFGE 4 An abstract class or an interface that is not global has at least one implementation Architecture
UnitTestsShouldIncludeStartTest Custom PMD XPath 4 A test method calls Test.startTest() and Test.stopTest() Test Methods
UnusedNullCheckInEqualsApex Custom PMD XPath 4 A null check before equals checks the object whose method is called Null Checks and Conditions
UseCollectionIsEmptyApex Custom PMD XPath 4 Test for an empty collection with isEmpty() Expressions
UsingTheTestMethodKeyword 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
DetectCopyPasteForApex Stock Code Analyzer CPD 5 No copied blocks of Apex; share a method or a base class Architecture
NoTrailingWhitespace Stock Code Analyzer Regex 5 No line ends in a space or a tab, and no line is only whitespace Formatting
NoUnnecessaryReturnVariables Custom PMD XPath 5 Return a value directly: return calculate();, not a variable that is set and then only returned Expressions
NoUnresolvedTodosApex Custom Code Analyzer Regex 5 No TODO, TO-DO, FIXME, FIX-ME, or HACK comment stays in Apex Documentation
QueueableWithoutFinalizer Stock PMD 5 A Queueable job whose failure needs a recovery action attaches a finalizer Asynchronous Apex
TestClassIsParallel Custom PMD XPath 5 A test class states IsParallel=true or IsParallel=false Test Classes