Skip to main content
MoxyCode analyses your Apex and LWC code against a set of built-in rules covering governor limits, security, LWC best practices, and code quality. New rules are added regularly — this page reflects the current rule set.

Severity levels

🔴 CriticalGovernor limit violations and security issues — fix before deploying
🟡 WarningBest practice violations — should be addressed
🔵 InfoCode quality suggestions — low urgency, no production risk

Apex rules

Critical

Rule ID: soql-in-loopA SOQL query is being executed inside a for loop. Each iteration of the loop counts as a separate query against Salesforce’s governor limit of 100 SOQL queries per transaction.Why it matters: In production, this will throw a System.LimitException: Too many SOQL queries: 101 error when the loop runs more than 100 times.Fix: Move the SOQL query outside the loop. Collect the IDs you need first, then query with a WHERE Id IN :idSet pattern.
    // ❌ Before
    for (Account acc : accounts) {
        List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
    }

    // ✅ After
    Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
    List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds];
Rule ID: dml-in-loopA DML statement (insert, update, delete, upsert, or undelete) is being executed inside a for loop. Each iteration counts against Salesforce’s governor limit of 150 DML statements per transaction.Why it matters: In production, this will throw a System.LimitException: Too many DML statements: 151 error when the loop runs more than 150 times.Fix: Collect records in a list inside the loop, then perform a single bulk DML operation after the loop.
    // ❌ Before
    for (Account acc : accounts) {
        acc.Name = 'Updated';
        update acc;
    }

    // ✅ After
    for (Account acc : accounts) {
        acc.Name = 'Updated';
    }
    update accounts;
Rule ID: future-in-loopA @future method is being called inside a for loop. Each call counts against Salesforce’s governor limit of 50 future calls per transaction.Why it matters: In production, this will throw a System.LimitException: Too many future calls: 51 error when the loop runs more than 50 times.Fix: Refactor the @future method to accept a collection of IDs and call it once outside the loop.
    // ❌ Before
    for (Account acc : accounts) {
        processAsync(acc.Id);
    }

    // ✅ After
    Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
    processAsync(accountIds);
Rule ID: hardcoded-idA Salesforce record ID is hardcoded directly in the code. Record IDs are org-specific and will be different in every sandbox, scratch org, and production environment.Why it matters: Code with hardcoded IDs will fail silently or throw errors when deployed to a different org. This is a common source of bugs when promoting code from sandbox to production.Fix: Use a Custom Label, Custom Setting, or Custom Metadata Type to store org-specific IDs and reference them in your code instead.

Warning

Rule ID: missing-sharing-keywordAn Apex class is declared without an explicit with sharing, without sharing, or inherited sharing keyword.Why it matters: Without an explicit sharing declaration, the class inherits the sharing context of its caller — which may be without sharing. This can expose records the running user should not have access to.Fix: Add an explicit sharing keyword to every Apex class.
    // ❌ Before
    public class AccountService {

    // ✅ After
    public with sharing class AccountService {
Rule ID: soql-mode-unspecifiedA SOQL query does not explicitly specify USER_MODE or SYSTEM_MODE.Why it matters: Without an explicit mode, SOQL runs in system mode by default — ignoring the running user’s field-level security and object permissions. This can expose data the user should not have access to.Fix: Add WITH USER_MODE to enforce the running user’s permissions, or WITH SYSTEM_MODE to make the intent explicit.
    // ❌ Before
    List<Account> accounts = [SELECT Id, Name FROM Account];

    // ✅ After
    List<Account> accounts = [SELECT Id, Name FROM Account WITH USER_MODE];
Rule ID: unsafe-single-soqlA SOQL query expected to return a single record is assigned directly to a variable without null protection or a try-catch block.Why it matters: If the query returns no records, Salesforce throws a System.QueryException: List has no rows for assignment to SObject error — which will crash the transaction.Fix: Wrap the query in a try-catch block or query into a list and check for results first.
    // ❌ Before
    Account acc = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1];

    // ✅ After
    List<Account> accounts = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1];
    Account acc = accounts.isEmpty() ? null : accounts[0];
Rule ID: direct-trigger-logicBusiness logic is written directly in the trigger body instead of delegating to a handler class.Why it matters: Triggers with inline logic are difficult to test, maintain, and extend. The Salesforce best practice is to keep trigger bodies thin and delegate all logic to a dedicated handler class.Fix: Create a trigger handler class and call it from the trigger body.
    // ❌ Before
    trigger AccountTrigger on Account (before insert) {
        for (Account acc : Trigger.new) {
            acc.Name = acc.Name.toUpperCase();
        }
    }

    // ✅ After
    trigger AccountTrigger on Account (before insert) {
        AccountTriggerHandler.handleBeforeInsert(Trigger.new);
    }
This rule has a Low Confidence rating — refactoring trigger logic into a handler class is a structural change that requires careful testing. MoxyCode will explain the pattern but review the suggested fix thoroughly before applying.

Info

Rule ID: apex-system-debugA System.debug() statement is present in the code.Why it matters: Debug statements left in production code clutter debug logs, add minor performance overhead, and can expose sensitive data in logs.Fix: Remove all System.debug() statements before deploying to production.

LWC JavaScript rules

Critical

Rule ID: lwc-inner-htmlA component is using direct innerHTML assignment to render content.Why it matters: Direct innerHTML assignment is an XSS vulnerability — if the content includes user-supplied data, it can be used to inject malicious scripts. Lightning Locker Service also blocks this pattern.Fix: Use LWC template directives (lwc:if, for:each) to render dynamic content safely.
Rule ID: lwc-api-mutatedA component is directly mutating an @api property inside the component.Why it matters: @api properties are owned by the parent component. Mutating them directly breaks the unidirectional data flow pattern in LWC and can cause unpredictable rendering behaviour.Fix: Copy the @api value to a tracked internal property and mutate the internal copy instead.

Warning

Rule ID: lwc-window-accessThe component is accessing the global window object directly.Why it matters: Direct window access is blocked by Lightning Locker Service in production Salesforce orgs. Code that works in local development will fail when deployed.Fix: Use LWC-supported alternatives. For timers, use setTimeout and setInterval without the window. prefix.
Rule ID: lwc-document-accessThe component is using document.querySelector() or similar DOM APIs directly.Why it matters: Direct document access is blocked by Lightning Locker Service. Each component only has access to its own DOM — not the full page document.Fix: Use this.template.querySelector() to access elements within the component’s own shadow DOM.
    // ❌ Before
    const input = document.querySelector('.my-input');

    // ✅ After
    const input = this.template.querySelector('.my-input');
Rule ID: lwc-wire-error-uncheckedA wire adapter result is being used without checking the .error property first.Why it matters: Wire adapters return both data and error. If the wire call fails and .error is not handled, the component will silently fail or throw a runtime error when trying to access .data.Fix: Always check both .data and .error when using wire adapters.
    // ❌ Before
    @wire(getContacts, { accountId: '$accountId' })
    contacts;

    // ✅ After
    @wire(getContacts, { accountId: '$accountId' })
    wiredContacts({ error, data }) {
        if (data) {
            this.contacts = data;
        } else if (error) {
            this.error = error;
        }
    }

Info

Rule ID: lwc-track-unnecessaryA @track decorator is being used on a primitive value (string, number, boolean).Why it matters: Since API version 46, all component properties are reactive by default. Using @track on primitives is unnecessary and adds noise to the code.Fix: Remove the @track decorator from primitive properties.
    // ❌ Before
    @track title = 'Hello';

    // ✅ After
    title = 'Hello';
Rule ID: lwc-event-target-valueThe component is using event.target.value to read input values.Why it matters: For Lightning base components (lightning-input, lightning-combobox, etc.), the correct property is event.detail.value. Using event.target.value returns undefined for these components.Fix: Use event.detail.value for Lightning base components.
    // ❌ Before
    handleChange(event) {
        this.value = event.target.value;
    }

    // ✅ After
    handleChange(event) {
        this.value = event.detail.value;
    }
Rule ID: console-log-productionA console.log() or console.error() statement is present in the component JavaScript.Why it matters: Console statements left in production code clutter browser developer tools and can expose sensitive data.Fix: Remove all console.log() and console.error() statements before deploying to production.
Rule ID: lwc-missing-catchAn imperative Apex method call uses .then() without a .catch() block.Why it matters: Without a .catch() handler, any error thrown by the Apex method will be silently swallowed — the component will stop working with no visible error.Fix: Always add a .catch() block to handle errors from imperative Apex calls.
    // ❌ Before
    getAccountData({ accountId: this.recordId })
        .then(result => {
            this.account = result;
        });

    // ✅ After
    getAccountData({ accountId: this.recordId })
        .then(result => {
            this.account = result;
        })
        .catch(error => {
            this.error = error;
        });

LWC HTML rules

Warning

Rule ID: lwc-string-onclickAn onclick attribute is using string function call syntax.Why it matters: String-based event handlers like onclick="handleClick()" are not supported in LWC templates. This is a common mistake for developers coming from Aura components or standard HTML.Fix: Use curly brace expression syntax to reference the handler method.
    <!-- ❌ Before -->
    <button onclick="handleClick()">Click me</button>

    <!-- ✅ After -->
    <button onclick={handleClick}>Click me</button>
Rule ID: lwc-for-each-no-keyA for:each directive is missing a key attribute on the repeated element.Why it matters: LWC requires a unique key attribute on elements rendered with for:each to efficiently track and re-render list items. Without it, LWC will throw a template rendering error.Fix: Add a key attribute with a unique value — typically the record ID.
    <!-- ❌ Before -->
    <template for:each={contacts} for:item="contact">
        <p>{contact.Name}</p>
    </template>

    <!-- ✅ After -->
    <template for:each={contacts} for:item="contact">
        <p key={contact.Id}>{contact.Name}</p>
    </template>

Shared rules (Apex + LWC)

Warning

Rule ID: empty-catchA catch block is present but empty — exceptions are being silently swallowed.Why it matters: Empty catch blocks hide errors completely. When something goes wrong, there is no log, no user feedback, and no way to diagnose the problem.Fix: Always handle caught exceptions — at minimum, log them or surface an error message.
    // ❌ Before
    try {
        update account;
    } catch (Exception e) {
    }

    // ✅ After
    try {
        update account;
    } catch (Exception e) {
        System.debug('Error updating account: ' + e.getMessage());
        throw e;
    }

Code Analysis

Learn how MoxyCode surfaces these rules in your editor