Skip to content

Fix && and || not short-circuiting in CSP evaluator#4823

Merged
calebporzio merged 1 commit intomainfrom
josh/fix-csp-short-circuit
Apr 30, 2026
Merged

Fix && and || not short-circuiting in CSP evaluator#4823
calebporzio merged 1 commit intomainfrom
josh/fix-csp-short-circuit

Conversation

@joshhanley
Copy link
Copy Markdown
Collaborator

@joshhanley joshhanley commented Apr 22, 2026

The Scenario

In an app running Livewire's CSP build, a single Flux::toast(...) call renders two toasts instead of one when the page uses a grouped toast setup.

Screenshot 2026-04-23 at 07 46 01AM
<flux:toast.group position="top end">
    <flux:toast />
</flux:toast.group>

One toast appears correctly inside the group; a second appears separately, originating from the inner <ui-toast> element. Swapping the CSP build for the regular Livewire build makes it behave correctly.

The Problem

Flux's inner <ui-toast> guards against handling the toast-show event when nested inside a group with a short-circuit expression:

<ui-toast x-on:toast-show.document="! $el.closest('ui-toast-group') && $el.showToast($event.detail)" ...>

Under the regular Alpine build this works correctly: when $el.closest('ui-toast-group') returns the group, the expression short-circuits and showToast is never called. Under the CSP build it doesn't, because the BinaryExpression handler in packages/csp/src/parser.js evaluates both operands up front before applying the operator:

case 'BinaryExpression':
    const left = this.evaluate({ node: node.left, ... });
    const right = this.evaluate({ node: node.right, ... });

    switch (node.operator) {
        // ...
        case '&&': return left && right;
        case '||': return left || right;
    }

By the time left && right is computed, the right side has already run. Any && or || expression with a side effect on the right leaks that side effect, regardless of the left value.

The Solution

Wrap the right-hand evaluation in a function and only call it when short-circuiting doesn't apply:

case 'BinaryExpression':
    const left = this.evaluate({ node: node.left, ... });

    // Wrapped in a function so && and || can skip it when short-circuiting.
    const evalRight = () => this.evaluate({ node: node.right, ... });

    // Short-circuit && and || so side-effects on the right aren't evaluated when the result is already determined.
    if (node.operator === '&&') return left && evalRight();
    if (node.operator === '||') return left || evalRight();

    const right = evalRight();

    switch (node.operator) { ... }
Screenshot 2026-04-23 at 07 47 38AM

Four new tests in tests/vitest/csp-evaluator.spec.js cover both the happy path (right side runs when needed) and the short-circuit path (right side does not run) for both && and ||.

Fixes livewire/flux#2571

The BinaryExpression handler always evaluated both sides, so guards like
`a && b()` and `a || b()` ran `b()` unconditionally.
@joshhanley joshhanley changed the title Short-circuit && and || in CSP evaluator Fix && and || not short-circuiting in CSP evaluator Apr 22, 2026
@codeldev
Copy link
Copy Markdown

Any ETA on when this may get merged? I have a client live project waiting on this fix :) Thanks!

@calebporzio calebporzio merged commit 91973c6 into main Apr 30, 2026
2 checks passed
@calebporzio calebporzio deleted the josh/fix-csp-short-circuit branch April 30, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two toasts displayed for a single Flux::toast() call when using flux:toast.group with flux:toast inside

3 participants