SfSkills Domains Docs GitHub

Home / devops / code-coverage-orphan-class-cleanup

code-coverage-orphan-class-cleanup

Use when an org's overall Apex coverage is sliding toward 75% because of orphaned (uncovered, unreferenced) classes inflating the denominator. Triggers: 'production deploy blocked at 74% coverage', 'find apex classes with 0% coverage', 'orphan apex class report', 'org coverage dropping despite green test runs'. NOT for writing tests on actively used classes (use apex/test-class-standards) or for raising coverage on partially-covered classes.

Open the skill folder on GitHub


Code Coverage Orphan Class Cleanup

Activate when an org's overall Apex coverage has crept toward the 75% deploy threshold and the source of the drag is uncovered, unreferenced classes accumulated over years of half-finished features. The skill produces a ranked list of orphan classes, separates "delete-safe" from "test-required-to-keep," and emits a destructive-changes manifest.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Coverage denominator

The 75% threshold is computed across all unmanaged Apex in the org (classes + triggers, excluding test classes). Removing a 200-line class with 0% coverage subtracts 200 from both the numerator and the denominator's "uncovered" portion — net positive on the percentage.

"Orphan" definition

A class is an orphan candidate when all of the following hold:

If all four hold, the class is delete-safe. If only the first three hold and the class has, e.g., a custom button reference, it must keep its test.

Coverage queries that lie

ApexCodeCoverage (per-test) and ApexCodeCoverageAggregate (latest-run) only reflect the last test run. If the run was incomplete or skipped a class because a dependency failed, coverage shows zero for that class for reasons other than missing tests. Always run a clean --test-level RunLocalTests before treating coverage as authoritative.


Common Patterns

Pattern: pre-deploy coverage rescue

When to use: Friday's deploy fails at 74.3%. You need to ship by Monday.

How it works: Query ApexCodeCoverageAggregate for 0%-coverage classes ordered by NumLinesCovered DESC + NumLinesUncovered DESC. Static-reference-check the top 20. Of those that pass all four orphan criteria, build a destructive-changes manifest and deploy with --purge-on-delete=true (sandbox first).

Why not the alternative: Writing 4,000 lines of stub tests under deadline pressure produces brittle tests that obscure real coverage signal.

Pattern: scheduled tech-debt cleanup

When to use: Quarterly engineering tax.

How it works: Same query, but instead of destructive deploy, file a per-class issue with the orphan evidence. Engineers triage: delete, test, or document why kept.

Pattern: package-deprecation cleanup

When to use: A managed package was uninstalled but unmanaged "ghost" classes remain (e.g., from an early-stage 1GP install).

How it works: Filter classes by namespace prefix in name, age, and zero-reference. Most are bulk-deletable.


Decision Guidance

Class stateActionReason
0% coverage, 0 source refs, 0 metadata refs, no @RestResourceDeletePure dead weight on the denominator
0% coverage, used by a Flow/button/REST mappingWrite test or document waiverActive code; test debt is real debt
Low (<75%) coverage, actively referencedOut of scope — see apex/test-class-standardsDifferent problem (improve tests)
0% coverage, looks unused, but team uncertainMark @deprecated, deploy, watch one cycleCheap reversibility before destructive change

  1. Run a clean sf apex run test --test-level RunLocalTests --code-coverage --result-format json to refresh ApexCodeCoverageAggregate. Without a clean run, coverage data is unreliable.
  2. Query orphan candidates: SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered FROM ApexCodeCoverageAggregate WHERE NumLinesCovered = 0.
  3. For each candidate, search the source tree for references: name token in .cls, .trigger, .flow-meta.xml, .weblink-meta.xml, .flexipage-meta.xml, REST URL mappings.
  4. Check metadata-only references: aura:component, LWC @AuraEnabled callers via connectedCallback patterns, Process Builder bindings.
  5. Categorize each candidate: delete-safe, needs-test-to-stay, deprecated-but-kept-for-back-compat.
  6. Build destructive-changes manifest for delete-safe classes. Deploy in sandbox first; verify no tests start failing because of compile-time references the search missed.
  7. Promote to higher environments. Track org-wide coverage delta — should jump cleanly upward.

Review Checklist


Salesforce-Specific Gotchas

  1. Coverage queries reflect only the last run — Skipping the clean test run before querying produces phantom orphans (and phantom keeps).
  2. REST endpoint classes have no Apex callers but are not orphans — @RestResource(urlMapping='/api/v1/things') is invoked by an HTTP request, never by Apex. Filter by annotation before declaring orphan.
  3. Scheduled-job classes can be invoked from System.schedule calls in other classes or from Setup-only schedule entries — Setup → Apex Jobs → Scheduled Jobs lists schedules that have no Apex caller record. Check before deleting any Schedulable class.
  4. Deleting a class fails if it's referenced by a Flow — The destructive deploy returns a clear error, but only after the apparently-clean static check. Flow XML must be searched for <actionName>ClassName</actionName>.

Output Artifacts

ArtifactDescription
orphan-candidates.csvClass name, lines, last-modified date, reference-search summary
delete-safe.xmldestructiveChanges.xml for verified-orphan classes
keep-with-test.mdClasses that must stay, with the test gap they need
coverage-before-after.txtOrg-wide coverage delta proof

Package files