You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取Elixir模块内使用的原子?解决原子拼写错误运行时问题

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:50:11