salesforce-apex-quality

Par github · awesome-copilot

Garde-fous de qualité du code Apex pour le développement Salesforce. Applique les règles de sécurité bulk (pas de SOQL/DML dans les boucles), les exigences du modèle de partage, la sécurité CRUD/FLS, la prévention des injections SOQL, la couverture de tests PNB (Positif / Négatif / Bulk), et les idiomes Apex modernes. Utilisez cette skill lors de la revue ou de la génération de classes Apex, de handlers de triggers, de batch jobs ou de classes de test, afin de détecter les risques liés aux governor limits, les failles de sécurité et les problèmes de qualité avant le déploiement.

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

Garanties de qualité Salesforce Apex

Appliquez ces vérifications à chaque classe Apex, trigger et fichier de test que vous écrivez ou examinez.

Étape 1 — Vérification de la sécurité des limites de gouvernance

Scannez ces patterns avant de déclarer un fichier Apex acceptable :

SOQL et DML dans les boucles — Échec automatique

// ❌ JAMAIS — provoque LimitException à l'échelle
for (Account a : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL dans la boucle
    update a; // DML dans la boucle
}

// ✅ TOUJOURS — collecter, puis interroger/mettre à jour une seule fois
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 une seule fois, en dehors de la boucle

Règle : si vous voyez [SELECT ou Database.query, insert, update, delete, upsert, merge à l'intérieur du corps d'une boucle for — arrêtez et refactorisez avant de continuer.

Étape 2 — Vérification du modèle de partage

Chaque classe doit déclarer explicitement son intention de partage. Le partage non déclaré hérite de l'appelant — comportement imprévisible.

Déclaration Quand l'utiliser
public with sharing class Foo Par défaut pour tous les service, handler, selector et controller classes
public without sharing class Foo Uniquement quand la classe doit s'exécuter avec des droits élevés (ex. journalisation système, bypass de trigger). Nécessite un commentaire de code expliquant pourquoi.
public inherited sharing class Foo Points d'entrée du framework qui doivent respecter le contexte de partage de l'appelant

Si une classe n'a pas l'une de ces trois déclarations, ajoutez-la avant d'écrire autre chose.

Étape 3 — Application de la sécurité CRUD / FLS

Le code Apex qui lit ou écrit des enregistrements au nom d'un utilisateur doit vérifier l'accès aux objets et aux champs. La plateforme n'applique pas automatiquement FLS ou CRUD dans Apex.

// Vérifier avant de interroger un champ
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
    throw new System.NoAccessException();
}

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

// Ou utiliser Database.query avec AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);

Règle : toute méthode Apex appelable à partir d'un composant UI, d'un endpoint REST, ou d'une @InvocableMethod doit appliquer CRUD/FLS. Les méthodes de service internes appelées uniquement à partir de contextes de confiance peuvent utiliser with sharing à la place.

Étape 4 — Prévention de l'injection SOQL

// ❌ JAMAIS — concatène l'entrée utilisateur dans la chaîne SOQL
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// ✅ TOUJOURS — variable de liaison
String soql = [SELECT Id FROM Account WHERE Name = :userInput];

// ✅ Pour SOQL dynamique avec des noms de champs contrôlés par l'utilisateur — valider par rapport à une whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
    throw new IllegalArgumentException('Field not permitted: ' + userInput);
}

Étape 5 — Idiomes Apex modernes

Préférez les fonctionnalités de langage actuelles (API 62.0 / Winter '25+) :

Ancien pattern Remplacement moderne
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 ...] sans contexte de partage [SELECT ... WHERE ... WITH USER_MODE]

Étape 6 — Liste de contrôle des tests PNB

Chaque fonctionnalité doit être testée sur les trois chemins. En manquer un seul constitue une défaillance de qualité :

Chemin positif

  • Entrée attendue → sortie attendue.
  • Affirmer les valeurs de champ exactes, les nombres d'enregistrements ou les valeurs retournées — pas seulement qu'aucune exception n'a été levée.

Chemin négatif

  • Entrée invalide, valeurs null, collections vides et conditions d'erreur.
  • Affirmer que les exceptions sont levées avec le type et le message corrects.
  • Affirmer qu'aucun enregistrement n'a été muté quand l'opération aurait dû échouer proprement.

Chemin en masse

  • Insérer/mettre à jour/supprimer 200–251 enregistrements dans une seule transaction de test.
  • Affirmer que tous les enregistrements ont été traités correctement — aucun échec partiel dû aux limites de gouvernance.
  • Utiliser Test.startTest() / Test.stopTest() pour isoler les compteurs de limites de gouvernance pour le travail asynchrone.

Règles de classe de test

@isTest(SeeAllData=false)   // Obligatoire — aucune exception sans raison documentée
private class AccountServiceTest {

    @TestSetup
    static void makeData() {
        // Créer toutes les données de test ici — utiliser une factory si elle existe dans le projet
    }

    @isTest
    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
        // Chemin positif
        List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
        Test.startTest();
        AccountService.processAccounts(accounts);
        Test.stopTest();
        // Affirmer des résultats significatifs — pas seulement aucune exception
        List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
    }
}

Étape 7 — Liste de contrôle de l'architecture des triggers

  • [ ] Un trigger par objet. Si un deuxième trigger existe, le consolider dans le handler.
  • [ ] Le corps du trigger contient uniquement : vérifications de contexte, invocation du handler et logique de routage.
  • [ ] Aucune logique métier, SOQL ou DML directement dans le corps du trigger.
  • [ ] Si un framework de trigger (Trigger Actions Framework, ff-apex-common, classe de base personnalisée) est déjà en use — l'étendre. Ne pas créer un pattern parallèle.
  • [ ] La classe handler est with sharing sauf si le trigger nécessite un accès élevé.

Référence rapide — Résumé des anti-patterns codés en dur

Pattern Action
SOQL à l'intérieur de la boucle for Refactoriser : interroger avant la boucle, opérer sur les collections
DML à l'intérieur de la boucle for Refactoriser : collecter les mutations, DML une seule fois après la boucle
Classe sans déclaration de partage Ajouter with sharing (ou documenter pourquoi without sharing)
escape="false" sur les données utilisateur (VF) Supprimer — l'auto-échappement applique la prévention XSS
Bloc catch vide Ajouter la journalisation et re-lancer ou gérer l'erreur de manière appropriée
SOQL concaténé en chaîne avec entrée utilisateur Remplacer par une variable de liaison ou une validation whitelist
Test sans assertion Ajouter un appel Assert.* significatif
Style System.assert / System.assertEquals Mettre à niveau vers Assert.isTrue / Assert.areEqual
ID d'enregistrement codé en dur ('001...') Remplacer par un ID d'enregistrement de test interrogé ou inséré

Skills similaires