MOB-5: ~MOB @foo raises a clear error when assigns is not in scope#56
Conversation
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
left a comment
There was a problem hiding this comment.
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.)
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>
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>
Fixes MOB-5. Follow-up to the 0.7.11 LiveView-ergonomics release.
Problem
0.7.11 added
@foo→assigns.foosugar in~MOB. But the idiomatic way to factor mob UI — including the guide's own example — is plain functions with positional args:There's no
assignsin such a function. The moment a user follows the new "@assigns shorthand" docs and writes@titleinside one, it expands toassigns.title→ cryptic "undefined variable assigns". (Same trap if arenderparam is namedsocketinstead ofassigns.) 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_Hguards withMacro.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, checkMacro.Env.has_var?(caller, {:assigns, nil}). Ifassignsisn't bound, raise aCompileErrornaming the fix — "@titlerequires a variable namedassignsin 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).@torender(assigns), steer composites to positional{var}, and add the common-mistake note (including "name the render paramassigns").Tests
@foowith 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@assigntests (which bindassignslocally) 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