{"kind":"Skill","metadata":{"namespace":"community","name":"salesforce-apex-quality","version":"0.1.0"},"spec":{"description":"Apex code quality guardrails for Salesforce development. Enforces bulk-safety rules (no SOQL/DML in loops), sharing model requirements, CRUD/FLS security, SOQL injection prevention, PNB test coverage (Positive / Negative / Bulk), and modern Apex idioms. Use this skill when reviewing or generating Apex classes, trigger handlers, batch jobs, or test classes to catch governor limit risks, security gaps, and quality issues before deployment.","files":{"SKILL.md":"---\nname: salesforce-apex-quality\ndescription: 'Apex code quality guardrails for Salesforce development. Enforces bulk-safety rules (no SOQL/DML in loops), sharing model requirements, CRUD/FLS security, SOQL injection prevention, PNB test coverage (Positive / Negative / Bulk), and modern Apex idioms. Use this skill when reviewing or generating Apex classes, trigger handlers, batch jobs, or test classes to catch governor limit risks, security gaps, and quality issues before deployment.'\n---\n\n# Salesforce Apex Quality Guardrails\n\nApply these checks to every Apex class, trigger, and test file you write or review.\n\n## Step 1 — Governor Limit Safety Check\n\nScan for these patterns before declaring any Apex file acceptable:\n\n### SOQL and DML in Loops — Automatic Fail\n\n```apex\n// ❌ NEVER — causes LimitException at scale\nfor (Account a : accounts) {\n    List\u003cContact\u003e contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop\n    update a; // DML in loop\n}\n\n// ✅ ALWAYS — collect, then query/update once\nSet\u003cId\u003e accountIds = new Map\u003cId, Account\u003e(accounts).keySet();\nMap\u003cId, List\u003cContact\u003e\u003e contactsByAccount = new Map\u003cId, List\u003cContact\u003e\u003e();\nfor (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {\n    if (!contactsByAccount.containsKey(c.AccountId)) {\n        contactsByAccount.put(c.AccountId, new List\u003cContact\u003e());\n    }\n    contactsByAccount.get(c.AccountId).add(c);\n}\nupdate accounts; // DML once, outside the loop\n```\n\nRule: if you see `[SELECT` or `Database.query`, `insert`, `update`, `delete`, `upsert`, `merge` inside a `for` loop body — stop and refactor before proceeding.\n\n## Step 2 — Sharing Model Verification\n\nEvery class must declare its sharing intent explicitly. Undeclared sharing inherits from the caller — unpredictable behaviour.\n\n| Declaration | When to use |\n|---|---|\n| `public with sharing class Foo` | Default for all service, handler, selector, and controller classes |\n| `public without sharing class Foo` | Only when the class must run elevated (e.g. system-level logging, trigger bypass). Requires a code comment explaining why. |\n| `public inherited sharing class Foo` | Framework entry points that should respect the caller's sharing context |\n\nIf a class does not have one of these three declarations, **add it before writing anything else**.\n\n## Step 3 — CRUD / FLS Enforcement\n\nApex code that reads or writes records on behalf of a user must verify object and field access. The platform does **not** enforce FLS or CRUD automatically in Apex.\n\n```apex\n// Check before querying a field\nif (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {\n    throw new System.NoAccessException();\n}\n\n// Or use WITH USER_MODE in SOQL (API 56.0+)\nList\u003cContact\u003e contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];\n\n// Or use Database.query with AccessLevel\nList\u003cContact\u003e contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);\n```\n\nRule: any Apex method callable from a UI component, REST endpoint, or `@InvocableMethod` **must** enforce CRUD/FLS. Internal service methods called only from trusted contexts may use `with sharing` instead.\n\n## Step 4 — SOQL Injection Prevention\n\n```apex\n// ❌ NEVER — concatenates user input into SOQL string\nString soql = 'SELECT Id FROM Account WHERE Name = \\'' + userInput + '\\'';\n\n// ✅ ALWAYS — bind variable\nString soql = [SELECT Id FROM Account WHERE Name = :userInput];\n\n// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist\nSet\u003cString\u003e allowedFields = new Set\u003cString\u003e{'Name', 'Industry', 'AnnualRevenue'};\nif (!allowedFields.contains(userInput)) {\n    throw new IllegalArgumentException('Field not permitted: ' + userInput);\n}\n```\n\n## Step 5 — Modern Apex Idioms\n\nPrefer current language features (API 62.0 / Winter '25+):\n\n| Old pattern | Modern replacement |\n|---|---|\n| `if (obj != null) { x = obj.Field__c; }` | `x = obj?.Field__c;` |\n| `x = (y != null) ? y : defaultVal;` | `x = y ?? defaultVal;` |\n| `System.assertEquals(expected, actual)` | `Assert.areEqual(expected, actual)` |\n| `System.assert(condition)` | `Assert.isTrue(condition)` |\n| `[SELECT ... WHERE ...]` with no sharing context | `[SELECT ... WHERE ... WITH USER_MODE]` |\n\n## Step 6 — PNB Test Coverage Checklist\n\nEvery feature must be tested across all three paths. Missing any one of these is a quality failure:\n\n### Positive Path\n- Expected input → expected output.\n- Assert the exact field values, record counts, or return values — not just that no exception was thrown.\n\n### Negative Path\n- Invalid input, null values, empty collections, and error conditions.\n- Assert that exceptions are thrown with the correct type and message.\n- Assert that no records were mutated when the operation should have failed cleanly.\n\n### Bulk Path\n- Insert/update/delete **200–251 records** in a single test transaction.\n- Assert that all records processed correctly — no partial failures from governor limits.\n- Use `Test.startTest()` / `Test.stopTest()` to isolate governor limit counters for async work.\n\n### Test Class Rules\n```apex\n@isTest(SeeAllData=false)   // Required — no exceptions without a documented reason\nprivate class AccountServiceTest {\n\n    @TestSetup\n    static void makeData() {\n        // Create all test data here — use a factory if one exists in the project\n    }\n\n    @isTest\n    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {\n        // Positive path\n        List\u003cAccount\u003e accounts = [SELECT Id FROM Account LIMIT 10];\n        Test.startTest();\n        AccountService.processAccounts(accounts);\n        Test.stopTest();\n        // Assert meaningful outcomes — not just no exception\n        List\u003cAccount\u003e updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];\n        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');\n    }\n}\n```\n\n## Step 7 — Trigger Architecture Checklist\n\n- [ ] One trigger per object. If a second trigger exists, consolidate into the handler.\n- [ ] Trigger body contains only: context checks, handler invocation, and routing logic.\n- [ ] No business logic, SOQL, or DML directly in the trigger body.\n- [ ] If a trigger framework (Trigger Actions Framework, ff-apex-common, custom base class) is already in use — extend it. Do not create a parallel pattern.\n- [ ] Handler class is `with sharing` unless the trigger requires elevated access.\n\n## Quick Reference — Hardcoded Anti-Patterns Summary\n\n| Pattern | Action |\n|---|---|\n| SOQL inside `for` loop | Refactor: query before the loop, operate on collections |\n| DML inside `for` loop | Refactor: collect mutations, DML once after the loop |\n| Class missing sharing declaration | Add `with sharing` (or document why `without sharing`) |\n| `escape=\"false\"` on user data (VF) | Remove — auto-escaping enforces XSS prevention |\n| Empty `catch` block | Add logging and appropriate re-throw or error handling |\n| String-concatenated SOQL with user input | Replace with bind variable or whitelist validation |\n| Test with no assertion | Add a meaningful `Assert.*` call |\n| `System.assert` / `System.assertEquals` style | Upgrade to `Assert.isTrue` / `Assert.areEqual` |\n| Hardcoded record ID (`'001...'`) | Replace with queried or inserted test record ID |\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/salesforce-development/skills/salesforce-apex-quality"}},"content_hash":[136,121,212,179,162,207,9,203,174,242,68,228,19,195,46,166,219,110,242,61,118,163,127,210,169,125,7,27,180,187,111,162],"trust_level":"unsigned","yanked":false}
