8.8 KiB
Elixir Code Smell Analysis — bDS2
Generated by static analysis of the codebase. Do not delete; use as a reference for refactoring sprints.
Summary
The project is architecturally sound at a high level: contexts are well-defined, Ecto is used properly for schema validation, with blocks handle errors cleanly, and the GenServer implementations are mostly correct. However, the codebase suffers from severe module bloat, excessive duplication of helper functions, risky process-dictionary usage, and several side-effect-in-transaction anti-patterns that become maintenance liabilities in a desktop application expected to run for long sessions.
Critical Anti-Patterns
1. Massive Module Bloat (Most Serious)
Several modules are far too large and violate the single-responsibility principle.
| Module | Lines | Issue |
|---|---|---|
BDS.Desktop.ShellLive |
~2,600 | Handles UI events, routing, overlays, menus, project switching, tab management, translations, task polling, and native menu integration |
BDS.Posts |
~1,700 | Handles CRUD, publishing, file I/O, translations, auto-translation scheduling, embeddings sync, search sync, link rebuilding, and validation |
BDS.Generation |
~1,500 | Site generation, validation, sitemap building, archive pagination, feed rendering, and file hashing |
BDS.MCP |
~670 | MCP tools, resources, proposals, post detail serialization, search, and category counting |
Why this is an anti-pattern: Giant modules reduce compiler concurrency, make refactoring dangerous, obscure test coverage, and increase merge conflicts. Elixir/OTP best practice is to keep modules under ~400 lines and split by responsibility.
Fix: Extract sub-domains into dedicated modules (e.g., BDS.Posts.Publisher, BDS.Posts.Translations, BDS.Generation.SitemapBuilder, BDS.Desktop.ShellLive.EventRouter).
2. Process Dictionary for I18n State
In BDS.Desktop.ShellLive:
def render(assigns) do
Process.put(:bds_ui_locale, assigns.page_language)
index(assigns)
end
And later:
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
Why this is an anti-pattern: The process dictionary is implicit global state. It makes functions harder to test in isolation and can leak state between concurrent operations in the same process. Elixir strongly discourages Process.put/get for business logic.
Fix: Pass locale explicitly through assigns to all translation call sites, or use a dedicated context struct.
3. Side Effects Inside Database Transactions
In BDS.Media.import_media/1:
Repo.transaction(fn ->
media = %Media{} |> Media.changeset(...) |> Repo.insert!()
:ok = File.mkdir_p(Path.dirname(destination))
:ok = File.cp(source_path, destination)
:ok = write_sidecar(project, media)
:ok = ensure_thumbnails(project, media)
:ok = Search.sync_media(media)
media
end)
Why this is an anti-pattern: If the transaction retries (or the DB connection times out in SQLite), filesystem and thumbnail side effects are duplicated or orphaned. Ecto transactions should contain only database operations.
Fix: Perform the DB insert in the transaction, then do filesystem work after the transaction succeeds.
4. String.to_atom/1 on Untrusted Input
In BDS.MCP:
defp atomize_keys(map) when is_map(map) do
Map.new(map, fn {key, value} -> {String.to_atom(key), value} end)
end
Why this is an anti-pattern: Atoms are not garbage collected. If MCP receives large or arbitrary JSON keys, the VM atom table can fill up and crash the BEAM.
Fix: Use String.to_existing_atom/1 (with a safe fallback) or keep keys as strings.
5. Bang Functions (File.read!) in Business Logic Without Rescue
Found in BDS.Posts, BDS.Media, BDS.MCP, BDS.Generation:
defp parse_rebuild_file(project, path) do
contents = File.read!(path)
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
...
end
Why this is an anti-pattern: Missing files or permission errors raise unhandled exceptions that crash the caller. In a GenServer context (e.g., background tasks), this brings down the worker.
Fix: Use File.read/1 and handle {:error, reason} explicitly, returning {:error, reason} tuples to callers.
6. Duplicate Helper Functions Across Contexts
The same private helpers are copy-pasted in BDS.Posts, BDS.Media, BDS.Search, BDS.Generation, BDS.Publishing, BDS.MCP:
attr/2(atom/string key normalization)maybe_put/3blank_to_nil/1progress_callback/1report_rebuild_started/3,report_rebuild_progress/4
Why this is an anti-pattern: Violates DRY. When behavior needs to change (e.g., adding float progress support), every copy must be updated.
Fix: Extract to a shared utility module such as BDS.MapUtils, BDS.AttrUtils, or BDS.ProgressReporter.
7. Direct Repo Access in LiveView
BDS.Desktop.ShellLive directly queries the repo:
case Repo.get(Post, post_id) do
%Post{} = post -> ...
end
Why this is an anti-pattern: LiveViews are the web layer; they should call context functions (BDS.Posts.get_post/1). Direct Repo access leaks persistence details into the UI and makes testing harder.
Fix: Replace all Repo.get calls in ShellLive with context function calls.
8. String.to_existing_atom/1 on User Input Without Whitelist
In ShellLive:
def handle_event("select_view", %{"view" => view_id}, socket) do
workbench = Workbench.click_activity(socket.assigns.workbench, String.to_existing_atom(view_id))
...
end
Why this is an anti-pattern: If a client sends a non-existent atom string, it raises ArgumentError. While safer than to_atom, it is still a crash vector.
Fix: Validate against a whitelist or map of known string-to-atom conversions before conversion.
9. Nested Exception Handling for Control Flow
In ShellLive:
defp safe_existing_atom(action) when is_binary(action) do
String.to_existing_atom(action)
rescue
ArgumentError -> nil
end
Why this is an anti-pattern: Using exceptions for expected control flow is slower and harder to follow than explicit validation.
Fix: Maintain an explicit mapping of allowed actions, or use a case with :erlang.binary_to_existing_atom/2 and catch the error tuple.
10. Jason.decode! Without Error Handling
In BDS.AI.OpenAICompatibleRuntime:
defp normalize_response(body) do
payload = Jason.decode!(body)
...
end
Why this is an anti-pattern: Malformed JSON from an external API will crash the caller.
Fix: Use Jason.decode/1 and propagate {:error, reason}.
Moderate Concerns
11. Missing @spec Typespecs
Very few public functions have @spec declarations. For a project of this size, this makes Dialyzer less effective and the API surface harder to understand for new contributors.
12. Tests Run Synchronously
All tests visible use use ExUnit.Case, async: false. For a large suite, this unnecessarily slows CI and local feedback loops. Use async: true where tests are isolated.
13. Raw SQL Overuse
While SQLite FTS necessitates some raw SQL, many queries in BDS.Search, BDS.Media, and BDS.Posts use Repo.query!/2 for standard operations that Ecto could express portably (e.g., DELETE FROM ... WHERE ...). This reduces type safety and database portability.
14. Atom/String Key Duality
Many functions normalize between atom and string keys repeatedly:
Map.get(assigns, :language, Map.get(assigns, "language", ...))
This suggests data isn't normalized at boundaries. Prefer atoms for internal structs and strings only at external boundaries (JSON, HTTP params).
15. GenServer State Growth (Tasks)
BDS.Tasks stores all tasks forever in a single map. The clear_completed/clear_finished functions exist, but if the UI doesn't trigger them regularly in a long-running desktop app, memory grows unbounded. Consider TTL-based eviction.
What the Project Does Well
- Contexts are clearly separated (
Posts,Media,Projects,Search, etc.). - Ecto changesets are used consistently for validation.
withblocks handle multi-step error flows cleanly.- Pattern matching is idiomatic and pervasive.
- Task.Supervisor is used correctly for background work.
- Atomic file writes (
Persistence.atomic_write) show good filesystem hygiene. - PubSub is used appropriately for CLI sync notifications.
Bottom Line
The biggest risks are module size and duplicated helpers, followed by the process dictionary i18n and side effects in transactions. Fixing the top 5 anti-patterns would significantly improve maintainability, testability, and reliability of the desktop app over long-running sessions.