fix(docs): document UILocale process-dict invariant and add enforcement tests (CSM-035)
This commit is contained in:
10
CODESMELL.md
10
CODESMELL.md
@@ -528,10 +528,12 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### CSM-035 — Process Dictionary (`Process.get/put`) Usage
|
### ~~CSM-035 — Process Dictionary (`Process.get/put`) Usage~~ ✅ FIXED
|
||||||
- **File:** `lib/bds/desktop/ui_locale.ex:32,49,65`
|
- **Fixed:** 2026-05-27
|
||||||
- **What:** `UILocale.put/1` sets process dictionary (`Process.put(@key, locale)`) for UI locale. Used in `ShellLive.render` (Z. 550) and `MenuBar`.
|
- **What was done:**
|
||||||
- **Fix:** This is isolated to the LiveView/MenuBar process so it's low-risk, but document the invariant explicitly: the process dict key `:bds_ui_locale` is set before each render call.
|
- **`lib/bds/desktop/ui_locale.ex`** — Added explicit **Invariant** section to `@moduledoc` documenting that every code path evaluating HEEx templates with `translated/1,2` must call `UILocale.put/1` before template evaluation. Lists all three render boundaries: `ShellLive.render/1`, `SidebarComponents.sidebar_content/1`, and `MenuBar.mount/1` + `handle_info({:set_ui_locale, _})`.
|
||||||
|
- Verified no raw `Process.put(:bds_ui_locale, ...)` or `Process.get(:bds_ui_locale)` exists outside `ui_locale.ex`.
|
||||||
|
- Added 9 tests in `test/bds/csm035_process_dict_test.exs`: source-level assertions that no raw Process.put/get/delete for `:bds_ui_locale` exists outside the module, render boundary assertions that `ShellLive.render/1`, `sidebar_content/1`, and `MenuBar.mount/1` call `UILocale.put` before template evaluation, and functional tests for `put/1`, `current/0`, and `with_locale/2` (including nil-restore behavior).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ defmodule BDS.Desktop.UILocale do
|
|||||||
process dictionary directly. Use `with_locale/2` around any render or
|
process dictionary directly. Use `with_locale/2` around any render or
|
||||||
component that needs a locale binding; use `current/0` to read it.
|
component that needs a locale binding; use `current/0` to read it.
|
||||||
|
|
||||||
|
## Invariant
|
||||||
|
|
||||||
|
Every code path that evaluates HEEx templates containing `translated/1,2`
|
||||||
|
calls **must** call `UILocale.put/1` before template evaluation:
|
||||||
|
|
||||||
|
* `ShellLive.render/1` — sets locale at the top of every LiveView render.
|
||||||
|
* `SidebarComponents.sidebar_content/1` — sets locale before the function
|
||||||
|
component's HEEx (runs in the same process, may be called outside
|
||||||
|
the parent render cycle via `send_update`).
|
||||||
|
* `MenuBar.mount/1` and `MenuBar.handle_info({:set_ui_locale, _})` — set
|
||||||
|
locale in the separate menu-bar process which has its own render cycle.
|
||||||
|
|
||||||
|
Violating this invariant causes `current/0` to return a stale or `nil`
|
||||||
|
locale, producing untranslated UI text.
|
||||||
|
|
||||||
Direct use of `Process.put(:bds_ui_locale, _)` or
|
Direct use of `Process.put(:bds_ui_locale, _)` or
|
||||||
`Process.get(:bds_ui_locale)` is forbidden outside this module.
|
`Process.get(:bds_ui_locale)` is forbidden outside this module.
|
||||||
"""
|
"""
|
||||||
|
|||||||
86
test/bds/csm035_process_dict_test.exs
Normal file
86
test/bds/csm035_process_dict_test.exs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
defmodule BDS.CSM035ProcessDictTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias BDS.Desktop.UILocale
|
||||||
|
|
||||||
|
describe "source-level: no raw Process.put/get for :bds_ui_locale outside ui_locale.ex" do
|
||||||
|
test "no direct Process.put(:bds_ui_locale, ...) outside ui_locale.ex" do
|
||||||
|
elixir_files =
|
||||||
|
Path.wildcard("lib/**/*.ex")
|
||||||
|
|> Enum.reject(&(&1 == "lib/bds/desktop/ui_locale.ex"))
|
||||||
|
|
||||||
|
violations =
|
||||||
|
Enum.filter(elixir_files, fn path ->
|
||||||
|
source = File.read!(path)
|
||||||
|
source =~ "Process.put(:bds_ui_locale" or source =~ "Process.put(@key"
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert violations == [],
|
||||||
|
"Raw Process.put(:bds_ui_locale) found outside ui_locale.ex: #{inspect(violations)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "no direct Process.get(:bds_ui_locale, ...) outside ui_locale.ex" do
|
||||||
|
elixir_files =
|
||||||
|
Path.wildcard("lib/**/*.ex")
|
||||||
|
|> Enum.reject(&(&1 == "lib/bds/desktop/ui_locale.ex"))
|
||||||
|
|
||||||
|
violations =
|
||||||
|
Enum.filter(elixir_files, fn path ->
|
||||||
|
source = File.read!(path)
|
||||||
|
source =~ "Process.get(:bds_ui_locale" or source =~ "Process.delete(:bds_ui_locale"
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert violations == [],
|
||||||
|
"Raw Process.get/delete(:bds_ui_locale) found outside ui_locale.ex: #{inspect(violations)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "source-level: render boundaries call UILocale.put before template evaluation" do
|
||||||
|
test "ShellLive.render/1 calls UILocale.put before index(assigns)" do
|
||||||
|
source = File.read!("lib/bds/desktop/shell_live.ex")
|
||||||
|
render_match = Regex.run(~r/def render\(assigns\).*?\n(.*?)\n(.*?)\n/s, source)
|
||||||
|
assert render_match, "could not find render/1 in shell_live.ex"
|
||||||
|
[_, first_line | _] = render_match
|
||||||
|
assert first_line =~ "UILocale.put", "render/1 must call UILocale.put on its first line"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sidebar_content/1 calls UILocale.put before HEEx" do
|
||||||
|
source = File.read!("lib/bds/desktop/shell_live/sidebar_components.ex")
|
||||||
|
match = Regex.run(~r/def sidebar_content\(assigns\).*?\n(.*?)\n/s, source)
|
||||||
|
assert match, "could not find sidebar_content/1"
|
||||||
|
[_, first_line | _] = match
|
||||||
|
assert first_line =~ "UILocale.put", "sidebar_content/1 must call UILocale.put on its first line"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "MenuBar.mount/1 calls UILocale.put" do
|
||||||
|
source = File.read!("lib/bds/desktop/menu_bar.ex")
|
||||||
|
match = Regex.run(~r/def mount\(menu\).*?\n(.*?)\n/s, source)
|
||||||
|
assert match, "could not find mount/1 in menu_bar.ex"
|
||||||
|
[_, first_line | _] = match
|
||||||
|
assert first_line =~ "UILocale.put", "MenuBar.mount/1 must call UILocale.put on its first line"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "UILocale functional behavior" do
|
||||||
|
test "put/1 sets locale readable by current/0" do
|
||||||
|
UILocale.put("de")
|
||||||
|
assert UILocale.current() == "de"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with_locale/2 restores previous locale after block" do
|
||||||
|
UILocale.put("en")
|
||||||
|
UILocale.with_locale("fr", fn -> assert UILocale.current() == "fr" end)
|
||||||
|
assert UILocale.current() == "en"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with_locale/2 restores nil when no prior locale was set" do
|
||||||
|
Process.delete(:bds_ui_locale)
|
||||||
|
UILocale.with_locale("it", fn -> assert UILocale.current() == "it" end)
|
||||||
|
assert UILocale.current() == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "put/1 returns :ok" do
|
||||||
|
assert UILocale.put("en") == :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user