如何获取Elixir模块内使用的原子?解决原子拼写错误运行时问题
这个问题我太有共鸣了——原子拼写错误真的是Elixir开发中很隐蔽的坑,往往要等到运行时崩溃才发现,折腾人得很!下面我就从实现思路、工具方案两个方面给你梳理一下解决方案。
一、实现你想要的AtomEnforcer:编译时捕获违规原子
你设想的通过use AtomEnforcer来限制模块内可用原子的思路完全可行,核心是在编译阶段收集模块内的所有原子,再和你声明的允许列表对比。这里有两种实现方式:
1. 遍历AST收集硬编码原子
在__using__宏中注入编译前的检查,遍历模块的AST提取所有原子节点,这种方式简单直接,能覆盖绝大多数硬编码原子的场景:
defmodule AtomEnforcer do defmacro __using__(opts) do allowed_atoms = Keyword.get(opts, :atoms, []) quote do @before_compile unquote(__MODULE__) @allowed_atoms unquote(allowed_atoms) end end defmacro __before_compile__(env) do allowed_atoms = Module.get_attribute(env.module, :allowed_atoms) # 拿到模块的AST结构 ast = Module.get_attribute(env.module, :__ast__) used_atoms = collect_atoms(ast) # 找出不在允许列表里的原子 forbidden_atoms = used_atoms -- allowed_atoms if forbidden_atoms != [] do # 抛出编译错误,直接阻断编译 quote do raise CompileError, message: "发现未授权的原子:#{inspect(unquote(forbidden_atoms))}", file: __ENV__.file, line: __ENV__.line end else nil end end # 递归遍历AST,把所有原子节点捞出来 defp collect_atoms({:__block__, _, exprs}), do: Enum.flat_map(exprs, &collect_atoms/1) defp collect_atoms({atom, _, nil}) when is_atom(atom), do: [atom] defp collect_atoms({_func, _, args}), do: Enum.flat_map(args, &collect_atoms/1) defp collect_atoms({:., _, [_mod, func]}), do: collect_atoms(func) defp collect_atoms(list) when is_list(list), do: Enum.flat_map(list, &collect_atoms/1) defp collect_atoms(_), do: [] end
使用的时候就像你设想的那样:
defmodule SomeGenServer do use AtomEnforcer, atoms: [:delayed_action] def delayed_action do Process.send_after(self(), :delayed_action, 5_000) end # 这里的:delayed_acton不在允许列表,编译时直接报错 def handle_info(:delayed_acton, state), do: handle_delayed_action(state) end
2. 解析字节码获取所有原子
如果AST遍历有遗漏(比如某些动态生成但编译到字节码的原子),可以用__after_compile__回调结合Erlang的:beam_lib模块解析编译后的字节码,这种方式能捕获所有编译进模块的原子:
defmodule AtomEnforcer do defmacro __using__(opts) do allowed_atoms = Keyword.get(opts, :atoms, []) quote do @after_compile {unquote(__MODULE__), :check_atoms} @allowed_atoms unquote(allowed_atoms) end end def check_atoms(env, _bytecode) do allowed_atoms = Module.get_attribute(env.module, :allowed_atoms) # 读取编译后的beam文件,提取原子表 {:ok, {_mod, [{:atoms, atoms}]}} = :beam_lib.chunks(env.module, [:atoms]) # 过滤掉系统内置的原子(比如:__MODULE__、:self这类) used_atoms = Enum.filter(atoms, fn {atom, _} -> not is_system_atom(atom) end) |> Keyword.keys() forbidden_atoms = used_atoms -- allowed_atoms if forbidden_atoms != [] do raise CompileError, message: "发现未预期的原子:#{inspect(forbidden_atoms)}", file: env.file, line: env.line end end defp is_system_atom(atom) do atom_str = Atom.to_string(atom) String.starts_with?(atom_str, "__") or Enum.member?([:self, :nil, :true, :false], atom) end end
二、社区现成的解决方案,不用自己造轮子
其实你不需要从零开始写工具,社区已经有成熟的方案来应对这类问题:
1. Credo静态代码分析
Credo的Credo.Check.Warning.UnusedAtom规则可以检测未被引用的原子。如果你的:delayed_acton是拼写错误,那它大概率不会被任何地方引用,Credo会直接给出警告。你只需要在.credo.exs中启用这个规则:
%{ configs: [ %{ name: "default", checks: [ {Credo.Check.Warning.UnusedAtom, []} ] } ] }
2. Dialyzer类型检查
给函数添加类型标注后,Dialyzer能检测出不匹配的原子分支。比如给handle_info加个类型:
@spec handle_info(:delayed_action, any()) :: {:noreply, any()} def handle_info(:delayed_action, state), do: {:noreply, handle_delayed_action(state)}
当你写了:delayed_acton的分支时,Dialyzer会提示这个分支永远不会被调用,因为类型不匹配,间接帮你发现拼写错误。
3. 模块属性集中管理原子
最省事的方法其实是用模块属性来定义所有要使用的原子,从源头避免硬编码拼写错误:
defmodule SomeGenServer do @delayed_action :delayed_action def delayed_action do Process.send_after(self(), @delayed_action, 5_000) end def handle_info(@delayed_action, state), do: handle_delayed_action(state) end
这样如果拼写错误,会直接触发“未定义模块属性”的编译错误,根本到不了运行时。
总结
- 要是你需要严格的原子白名单,可以自己实现
AtomEnforcer,AST遍历够用,字节码解析更全面; - 日常开发更推荐用模块属性集中管理原子,简单高效;
- Credo和Dialyzer可以作为辅助工具,帮你查漏补缺。
内容的提问来源于stack exchange,提问作者Chris Meyer

