Skip to content

MOB-5: ~MOB @foo raises a clear error when assigns is not in scope#56

Merged
GenericJam merged 1 commit into
masterfrom
mob-5-sigil-assigns-guard
Jul 1, 2026
Merged

MOB-5: ~MOB @foo raises a clear error when assigns is not in scope#56
GenericJam merged 1 commit into
masterfrom
mob-5-sigil-assigns-guard

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Fixes MOB-5. Follow-up to the 0.7.11 LiveView-ergonomics release.

Problem

0.7.11 added @fooassigns.foo sugar in ~MOB. But the idiomatic way to factor mob UI — including the guide's own example — is plain functions with positional args:

def card(title, children), do: ~MOB"<Text text={title} />"

There's no assigns in such a function. The moment a user follows the new "@assigns shorthand" docs and writes @title inside one, it expands to assigns.title → cryptic "undefined variable assigns". (Same trap if a render param is named socket instead of assigns.) A user hit exactly this: a wave of "no assigns" errors, then "unused assigns" warnings while trying to bridge the two styles.

What Phoenix does

Phoenix.Component.sigil_H guards with Macro.Env.has_var?(__CALLER__, {:assigns, nil}) and raises "~H requires a variable named assigns..." — a clear compile error instead of a raw undefined-variable. mob's sigil has __CALLER__ too, so it can do the same.

Fix

  • Mob.Sigil: before rewriting @foo, check Macro.Env.has_var?(caller, {:assigns, nil}). If assigns isn't bound, raise a CompileError naming the fix — "@title requires a variable named assigns in scope … use {title} instead of @title." Applies to attrs, {expr} children, and :if/:for. Only @-using templates trigger it; a static ~MOB(<Text text="hi"/>) in a positional-arg helper still compiles (unlike ~H, which always needs assigns).
  • Docs: scope @ to render(assigns), steer composites to positional {var}, and add the common-mistake note (including "name the render param assigns").

Tests

@foo with no assigns raises (asserts the message names {title}); :if={@flag} with no assigns raises; a static template compiles without assigns; a positional-arg helper ({title}) compiles + runs without assigns. Existing @assign tests (which bind assigns locally) stay green. Full suite: 964 pass; format + credo clean.

Not included

No version bump — release timing is the maintainer's call (would be 0.7.12).

🤖 Generated with Claude Code

0.7.11 added @foo -> assigns.foo sugar. In an ordinary helper function
(positional args — the idiomatic composite pattern), there is no
`assigns`, so @foo compiled to a cryptic "undefined variable assigns".

Mirror Phoenix's ~H guard: before rewriting @foo, check
Macro.Env.has_var?(caller, {:assigns, nil}) and raise a CompileError that
names the fix ("use {title} instead of @title") when assigns isn't bound.
Only @-using templates trigger it — a static ~MOB(<Text text="hi"/>) in a
positional-arg helper still compiles fine (unlike ~H, which always needs
assigns).

Docs: scope @ to render(assigns), steer composites to positional {var},
and add the common-mistake note (incl. render params must be named
`assigns`). Tests cover the raise, the :if={@x} raise, and both no-assigns
positives (static template + positional-arg helper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@GenericJam GenericJam left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff, tests, and CI. Recommend merge.

Correctness: the guard sits inside the prewalk, so it only fires when a @name node is actually present — static templates and positional-arg helpers using {var} compile untouched. caller is threaded correctly from the sigil __CALLER__, and Macro.Env.has_var?(caller, {:assigns, nil}) is the same check Phoenix.Component.sigil_H uses, so it is proven.

Tests: targeted and real — @foo without assigns raises (asserting the message names {title}), :if={@flag} raises, a static template compiles, and a positional helper both compiles and runs. Guard + happy path both covered.

CI: fully green (Elixir 1.19/OTP 28, native formatters, mix_audit, GitGuardian); mergeable/CLEAN.

Minor, non-blocking: the guard also fires for a genuine module attribute @title used inside a sigil in a non-assigns function. That is pre-existing @-shadowing (same as HEEx) and the new error is still clearer than the old undefined-variable crash, so not worth holding the PR.

Coordination note (docs anchor): the new blockquote links to [function composites](#pure-elixir-composite-components). PR #57 renames that section heading to "Defining your own components", which removes that anchor. I am updating #57 to keep the #pure-elixir-composite-components anchor alive so this link does not dangle regardless of merge order. Suggest merging this (#56) first, then #57 rebases on top.

(Posting as a comment rather than an approval — GitHub blocks approving your own PR.)

GenericJam added a commit that referenced this pull request Jul 1, 2026
The section was renamed to 'Defining your own components', which drops the
old auto-generated anchor. PR #56 links to #pure-elixir-composite-components
from the @assigns note; add an explicit anchor so that link resolves
regardless of merge order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@GenericJam GenericJam merged commit fccd1c4 into master Jul 1, 2026
4 checks passed
GenericJam added a commit that referenced this pull request Jul 1, 2026
MOB-5: ~MOB raises a helpful CompileError when @foo is used without an
`assigns` in scope (guard via Macro.Env.has_var?, mirroring ~H), instead
of a cryptic "undefined variable assigns". Plus worked component-authoring
examples in the Components guide. (#56, #57)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@GenericJam GenericJam deleted the mob-5-sigil-assigns-guard branch July 1, 2026 05:12
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.

1 participant