mcpField
S
salesforce-apex-quality

salesforce-apex-quality

github.com/githubテスト・品質40.5k884 回インストール今日更新
リポジトリ

salesforce-apex-quality について

Salesforce Apex のクラス、トリガー、テストをレビューするチェックリストです。明示的なルールに沿って、ガバナー制限やセキュリティ、クエリ、テスト品質を確認できます。

主な機能

  1. 01ループ内の SOQL と DML の検査
  2. 02sharing と CRUD/FLS の確認
  3. 03SOQL インジェクションの防止
  4. 04最新の Apex イディオムの指針
  5. 05正常・異常・一括テストの確認
  6. 06トリガー構成のルール

活用シーン

  1. 01マージ前の Apex サービスのレビュー
  2. 02ループ内の SOQL または DML の検出
  3. 03UI から呼ばれるメソッドの CRUD/FLS 確認
  4. 04一括処理のテストカバレッジ評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/github/awesome-copilot --skill salesforce-apex-quality

インストール後、Claude Code で/salesforce-apex-qualityを入力して読み込まれたか確認します。

詳細

Salesforce Apex 品質ガードレール

作成またはレビューするすべての Apex クラス、トリガー、テストファイルに、これらのチェックを適用してください。

ステップ 1 — ガバナ制限の安全性チェック

Apex ファイルを許容可能と判断する前に、次のパターンがないか確認してください。

ループ内の SOQL と DML — 自動的に不合格
// ❌ NEVER — causes LimitException at scale
for (Account a : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop
    update a; // DML in loop
}

// ✅ ALWAYS — collect, then query/update once
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccount.containsKey(c.AccountId)) {
        contactsByAccount.put(c.AccountId, new List<Contact>());
    }
    contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML once, outside the loop

ルール:for ループ本体内に [SELECT、Database.query、insert、update、delete、upsert、merge がある場合は、そこで作業を止め、先にリファクタリングしてください。

ステップ 2 — 共有モデルの確認

すべてのクラスで、共有の意図を明示的に宣言する必要があります。共有が宣言されていない場合は呼び出し元から継承されるため、動作が予測できません。

宣言使用する状況
public with sharing class Fooすべてのサービス、ハンドラー、セレクター、コントローラークラスのデフォルト
public without sharing class Fooクラスを昇格された権限で実行する必要がある場合のみ(例:システムレベルのログ記録、トリガーのバイパス)。理由を説明するコードコメントが必要です。
public inherited sharing class Foo呼び出し元の共有コンテキストを尊重する必要があるフレームワークのエントリーポイント

クラスにこれら3つの宣言のいずれもない場合は、ほかの作業を始める前に追加してください。

ステップ 3 — CRUD / FLS の適用

ユーザーに代わってレコードを読み書きする Apex コードでは、オブジェクトおよび項目へのアクセスを確認する必要があります。プラットフォームは Apex で FLS や CRUD を自動的に適用しません。

// Check before querying a field
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
    throw new System.NoAccessException();
}

// Or use WITH USER_MODE in SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];

// Or use Database.query with AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);

ルール:UI コンポーネント、REST エンドポイント、または @InvocableMethod から呼び出し可能な Apex メソッドでは、CRUD/FLS を必ず適用してください。信頼できるコンテキストからのみ呼び出される内部サービスメソッドでは、代わりに with sharing を使用できます。

ステップ 4 — SOQL インジェクションの防止

// ❌ NEVER — concatenates user input into SOQL string
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// ✅ ALWAYS — bind variable
String soql = [SELECT Id FROM Account WHERE Name = :userInput];

// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
    throw new IllegalArgumentException('Field not permitted: ' + userInput);
}

ステップ 5 — 最新の Apex のイディオム

現在の言語機能(API 62.0 / Winter '25 以降)を優先してください。

古いパターン最新の代替パターン
if (obj != null) { x = obj.Field__c; }x = obj?.Field__c;
x = (y != null) ? y : defaultVal;x = y ?? defaultVal;
System.assertEquals(expected, actual)Assert.areEqual(expected, actual)
System.assert(condition)Assert.isTrue(condition)
共有コンテキストなしの [SELECT ... WHERE ...][SELECT ... WHERE ... WITH USER_MODE]

ステップ 6 — PNB テストカバレッジのチェックリスト

すべての機能を、次の3つのパスすべてでテストする必要があります。いずれかが欠けている場合は、品質上の不合格です。

正常系
  • 期待される入力 → 期待される出力。
  • 例外が発生しなかったことだけでなく、正確な項目値、レコード件数、または戻り値をアサートしてください。
異常系
  • 無効な入力、null 値、空のコレクション、エラー条件。
  • 正しい型とメッセージで例外がスローされることをアサートしてください。
  • 操作が正常に失敗すべき場合に、レコードが変更されていないことをアサートしてください。
一括処理
  • 1回のテストトランザクションで200~251件のレコードを挿入/更新/削除してください。
  • すべてのレコードが正しく処理されたことをアサートしてください。ガバナ制限による部分的な失敗がないことを確認してください。
  • 非同期処理のガバナ制限カウンターを分離するには、Test.startTest() / Test.stopTest() を使用してください。
テストクラスのルール
@isTest(SeeAllData=false)   // Required — no exceptions without a documented reason
private class AccountServiceTest {

    @TestSetup
    static void makeData() {
        // Create all test data here — use a factory if one exists in the project
    }

    @isTest
    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
        // Positive path
        List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
        Test.startTest();
        AccountService.processAccounts(accounts);
        Test.stopTest();
        // Assert meaningful outcomes — not just no exception
        List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
    }
}

ステップ 7 — トリガーアーキテクチャのチェックリスト

  • オブジェクトごとにトリガーは1つにしてください。2つ目のトリガーがある場合は、ハンドラーに統合してください。
  • トリガー本体に含めるのは、コンテキストの確認、ハンドラーの呼び出し、ルーティングロジックのみです。
  • トリガー本体にビジネスロジック、SOQL、DMLを直接記述しないでください。
  • トリガーフレームワーク(Trigger Actions Framework、ff-apex-common、カスタム基底クラス)がすでに使われている場合は、それを拡張してください。並行する別のパターンを作成しないでください。
  • トリガーで昇格されたアクセス権が必要でない限り、ハンドラークラスには with sharing を指定してください。

クイックリファレンス — ハードコードされたアンチパターンの概要

パターン対応
for ループ内の SOQLリファクタリング:ループの前にクエリを実行し、コレクションを操作します
for ループ内の DMLリファクタリング:変更内容を収集し、ループの後に DML を一度だけ実行します
共有宣言のないクラスwith sharing を追加します(または、without sharing にする理由を記載します)
ユーザーデータに対する escape="false"(VF)削除します — 自動エスケープにより XSS を防止します
空の catch ブロックログ記録と、適切な再スローまたはエラー処理を追加します
ユーザー入力を連結した SOQL 文字列バインド変数またはホワイトリスト検証に置き換えます
アサーションのないテスト意味のある Assert.* 呼び出しを追加します
System.assert / System.assertEquals スタイルAssert.isTrue / Assert.areEqual に更新します
ハードコードされたレコード ID('001...')クエリまたは挿入したテストレコードの ID に置き換えます

コメント