查看源代码 代码 (Elixir v1.16.2)
用于管理代码编译、代码评估和代码加载的实用程序。
此模块补充了 Erlang 的 :code
模块,以添加特定于 Elixir 的行为。有关操作 Elixir 的 AST(而不是评估它)的函数,请参阅 Macro
模块。
处理文件
此模块包含三个用于编译和评估文件的函数。以下是它们的摘要及其行为
require_file/2
- 编译文件并跟踪其名称。如果文件已先前被要求,它不会再次编译文件。compile_file/2
- 编译文件而不跟踪其名称。多次调用时,多次编译文件。eval_file/2
- 评估文件内容而不跟踪其名称。它返回文件最后表达式的结果,而不是文件中定义的模块。评估的文件不会触发下一节中描述的编译跟踪器。
简而言之,当您要跟踪系统处理的文件以避免同一个文件被多次编译时,必须使用第一个。这在脚本中很常见。
当您对文件中定义的模块感兴趣但不想跟踪时,必须使用 compile_file/2
。当您对评估文件的結果感兴趣而不是它定义的模块时,应使用 eval_file/2
。
以上函数适用于 Elixir 源代码。如果您想使用编译为字节码的模块,这些模块具有 .beam
扩展名,通常位于 Mix 项目的 _build 目录下,请参阅 Erlang 的 :code
模块中的函数。
Erlang VM 上的代码加载
Erlang 具有两种加载代码的模式:交互式和嵌入式。
默认情况下,Erlang VM 以交互模式运行,其中模块按需加载。在嵌入模式下,情况相反,因为所有模块都需要预先加载或显式加载。
您可以使用 ensure_loaded/1
(以及 ensure_loaded?/1
和 ensure_loaded!/1
)来检查模块是否已加载并在使用前采取行动。
ensure_compiled/1
和 ensure_compiled!/1
Elixir 还包含 ensure_compiled/1
和 ensure_compiled!/1
函数,它们是 ensure_loaded/1
的超集。
由于 Elixir 的编译是并行进行的,因此在某些情况下,您可能需要使用尚未编译的模块,因此甚至无法加载它。
调用时,ensure_compiled/1
和 ensure_compiled!/1
会停止调用者的编译,直到该模块可用。请注意,ensure_compiled/1
和 ensure_compiled!/1
之间的区别很重要:如果您使用的是 ensure_compiled!/1
,则表示您向编译器表明只有在该模块可用时才能继续。
如果您使用的是 Code.ensure_compiled/1
,则表示您可以不使用该模块继续,因此 Elixir 可能会返回 {:error, :unavailable}
,用于模块尚未可用(但可能稍后可用)的情况。
出于这些原因,开发人员通常必须使用 Code.ensure_compiled!/1
。特别是,不要这样做
case Code.ensure_compiled(module) do
{:module, _} -> module
{:error, _} -> raise ...
end
最后,请注意您只需要 ensure_compiled!/1
来检查同一项目中定义的模块。它不适用于依赖项中的模块,因为依赖项始终在预先编译。
在大多数情况下,ensure_loaded/1
就足够了。ensure_compiled!/1
必须在极少数情况下使用,通常涉及需要调用模块以获取回调信息的宏。使用 ensure_compiled/1
的可能性更小。
编译跟踪器
Elixir 支持编译跟踪器,它允许模块观察 Elixir 编译器在编译文件时处理的结构。跟踪器是一个实现了 trace/2
函数的模块。该函数接收事件名称作为第一个参数,接收 Macro.Env
作为第二个参数,并且必须返回 :ok
。对于跟踪器来说,尽可能少地进行同步工作并将大部分工作分派到单独的进程非常重要。**缓慢的跟踪器会减慢编译速度**。
您可以通过 put_compiler_option/2
配置跟踪器列表。以下事件可供跟踪器使用
:start
- (自 v1.11.0 起)每当编译器开始跟踪新的词法上下文时调用。编译新文件或在函数内定义模块时,会启动词法上下文。请注意,评估的代码不会启动新的词法上下文(因为它们不跟踪未使用的别名、导入等),但在评估的代码内定义模块会。请注意,此事件可能并行发出,其中多个文件/模块调用
:start
并同时运行。宏环境的lexical_tracker
值虽然不透明,但可用于唯一标识环境。:stop
- (自 v1.11.0 起)每当编译器停止跟踪新的词法上下文(例如新文件)时调用。{:import, meta, module, opts}
- 每当导入module
时跟踪。meta
是导入 AST 元数据,opts
是导入选项。{:imported_function, meta, module, name, arity}
和{:imported_macro, meta, module, name, arity}
- 每当调用导入的函数或宏时跟踪。meta
是调用 AST 元数据,module
是导入来自的模块,后面是导入的函数/宏的name
和arity
。对于导入的模块/名称/元组,仍然可以发出 :remote_function/:remote_macro 事件。{:alias, meta, alias, as, opts}
- 每当alias
被别名为as
时跟踪。meta
是别名 AST 元数据,opts
是别名选项。{:alias_expansion, meta, as, alias}
每当先前定义的alias
存在别名扩展时跟踪,即当用户写入as
扩展为alias
时。meta
是别名扩展 AST 元数据。{:alias_reference, meta, module}
- 每当代码中存在别名时跟踪,即当用户在代码中写入MyModule.Foo.Bar
时,无论它是否已扩展。{:require, meta, module, opts}
- 每当要求module
时跟踪。meta
是 require AST 元数据,opts
是 require 选项。如果meta
选项包含:from_macro
,则模块是从宏中调用的,因此必须将其视为编译时依赖项。{:struct_expansion, meta, module, keys}
- 每当扩展module
的结构时跟踪。meta
是结构 AST 元数据,keys
是扩展使用的键{:remote_function, meta, module, name, arity}
和{:remote_macro, meta, module, name, arity}
- 每当引用远程函数或宏时跟踪。meta
是调用 AST 元数据,module
是调用的模块,后面是name
和arity
。{:local_function, meta, name, arity}
和{:local_macro, meta, name, arity}
- 每当引用本地函数或宏时跟踪。meta
是调用 AST 元数据,后面是name
和arity
。{:compile_env, app, path, return}
- 每当调用Application.compile_env/3
或Application.compile_env!/2
时跟踪。app
是一个原子,path
是一个列表,用于遍历应用程序环境中的键,return
或者是{:ok, value}
或者是:error
。:defmodule
- (自 v1.16.2 起)在模块定义开始时跟踪。这是在模块生命周期的早期调用的,Module.open?/1
对于此类跟踪仍然返回false
{:on_module, bytecode, _ignore}
- (自 v1.13.0 起)每当定义模块时跟踪。这等效于@after_compile
回调,并在给定模块中的任何@after_compile
之后调用。第三个元素当前是:none
,但将来可能会提供更多元数据。目前最好忽略它。请注意,Module
函数期望尚未编译的模块(例如Module.definitions_in/1
)在发出此事件时仍然可用。
:tracers
编译器选项可以与 :parser_options
编译器选项结合使用,以丰富上面跟踪事件的元数据。
将来可能会随时添加新的事件,因此建议 trace/2
函数具有“catch-all”子句。
以下是一个打印所有远程函数调用的示例跟踪器
defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok
end
def trace(_event, _env) do
:ok
end
end
摘要
函数
将路径追加到 Erlang VM 代码路径列表中。
将 paths
列表追加到 Erlang VM 代码路径列表中。
返回一个包含所有可用编译器选项的列表。
如果当前进程可以等待模块编译,则返回 true
。
编译引用的表达式。
编译给定的字符串。
从代码服务器获取所有编译选项。
存储所有给定的编译选项。
从 Erlang VM 代码路径列表中删除路径。
从 Erlang VM 代码路径列表中删除路径列表。
确保加载给定的模块。
与 ensure_all_loaded/1
相同,但如果任何模块无法加载,则会引发异常。
类似于 ensure_compiled!/1
,但表示您可以继续,即使没有该模块。
确保给定的模块已编译并加载。
确保给定的模块已加载。
与 ensure_loaded/1
相同,但如果模块无法加载,则会引发异常。
确保给定的模块已加载。
返回用于评估的环境。
评估给定的文件。
使用 binding
和 env
评估给定的 quoted
内容。
评估由 string
给出的内容。
返回给定模块或 .beam
文件路径的文档。
格式化给定的代码 string
。
返回给定编译器选项的值。
如果模块已加载,则返回 true
。
将路径添加到 Erlang VM 代码路径列表的开头。
将 paths
的列表添加到 Erlang VM 代码路径列表的开头。
将诊断信息打印到标准错误流。
清除编译器模块。
存储编译选项。
使用 Elixir 的格式化规则将引用的表达式转换为代数文档。
需要给定的 file
。
列出所有所需文件。
将给定的字符串转换为其引用形式。
将给定的字符串转换为其引用形式。
将给定的字符串转换为其引用形式以及注释列表。
将给定的字符串转换为其引用形式以及注释列表。
从所需文件列表中删除文件。
执行给定的 fun
并捕获所有诊断信息。
类型
包含所有变量及其值的列表。
绑定键通常是原子,但它们也可能是定义在不同上下文的变量的元组。
@type diagnostic(severity) :: %{ :source => Path.t() | nil, :file => Path.t() | nil, :severity => severity, :message => String.t(), :position => position(), :stacktrace => Exception.stacktrace(), :span => {line :: pos_integer(), column :: pos_integer()} | nil, optional(:details) => term(), optional(any()) => any() }
编译器和代码评估返回的诊断信息。
文件和位置与应该显示诊断信息的位置有关。如果存在文件和位置,则诊断信息是精确的,您可以使用给定的文件和位置生成代码片段、IDE 注解等。可以使用可选的范围,其中包含诊断信息结束的行号和列号。
否则,可能会给出堆栈跟踪,您可以使用自己的启发式方法来提供更好的报告。
源字段指向编译器跟踪错误的源文件。例如,文件 lib/foo.ex
可能嵌入来自 lib/foo/bar.eex
的 .eex
模板。EEx 模板上的语法错误将指向文件 lib/foo/bar.eex
,但源文件是 lib/foo.ex
。
@type line() :: non_neg_integer()
行号。0 表示没有行。
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
诊断信息的位置。
可以是行号或 {line, column}
。行号和列号从 1 开始。位置 0
表示未知。
函数
将路径追加到 Erlang VM 代码路径列表中。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
路径在被附加之前将使用 Path.expand/1
进行扩展。它要求路径存在。返回一个布尔值,指示路径是否已成功添加。
示例
Code.append_path(".")
#=> true
Code.append_path("/does_not_exist")
#=> false
选项
:cache
- (自 v1.15.0 起)当为 true 时,代码路径在首次遍历时会被缓存,以减少文件系统操作。它需要 Erlang/OTP 26,否则将无效。
将 paths
列表追加到 Erlang VM 代码路径列表中。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
所有路径在被附加之前都将使用 Path.expand/1
进行扩展。只附加存在的路径。此函数始终返回 :ok
,无论附加了多少路径。如果您需要更多控制,请使用 append_path/1
。
示例
Code.append_paths([".", "/does_not_exist"])
#=> :ok
选项
:cache
- 当为 true 时,代码路径在首次遍历时会被缓存,以减少文件系统操作。它需要 Erlang/OTP 26,否则将无效。
@spec available_compiler_options() :: [atom()]
返回一个包含所有可用编译器选项的列表。
有关所有选项的说明,请参见 put_compiler_option/2
。
示例
Code.available_compiler_options()
#=> [:docs, :debug_info, ...]
@spec can_await_module_compilation?() :: boolean()
如果当前进程可以等待模块编译,则返回 true
。
当通过 Kernel.ParallelCompiler
编译 Elixir 代码时,它由 Mix 和 elixirc
使用,调用尚未编译的模块将阻塞调用者,直到模块可用为止。执行 Elixir 脚本,例如将文件名传递给 elixir
,不会等待。
编译给定的文件。
接受 relative_to
作为参数,以告知文件的位置。
返回元组列表,其中第一个元素是模块名称,第二个元素是其字节码(作为二进制文件)。与 require_file/2
相反,它不跟踪编译文件的名称。
如果您想获取评估文件的結果,而不是文件中定义的模块,请参见 eval_file/2
。
要并行编译多个文件,请参见 Kernel.ParallelCompiler.compile/2
。
编译引用的表达式。
返回元组列表,其中第一个元素是模块名称,第二个元素是其字节码(作为二进制文件)。可以用第二个参数指定一个 file
,用于报告警告和错误。
@spec compile_string(List.Chars.t(), binary()) :: [{module(), binary()}]
编译给定的字符串。
返回元组列表,其中第一个元素是模块名称,第二个元素是其字节码(作为二进制文件)。可以用第二个参数指定一个 file
,用于报告警告和错误。
警告:string
可以是任何 Elixir 代码,代码可以以与 Erlang VM 相同的权限执行:这意味着此类代码可能危及机器(例如通过执行系统命令)。不要使用 compile_string/2
处理不受信任的输入(例如来自网络的字符串)。
@spec compiler_options() :: map()
从代码服务器获取所有编译选项。
要获取单个选项,请参见 get_compiler_option/1
。有关所有选项的说明,请参见 put_compiler_option/2
。
示例
Code.compiler_options()
#=> %{debug_info: true, docs: true, ...}
@spec compiler_options(Enumerable.t({atom(), term()})) :: %{ optional(atom()) => term() }
存储所有给定的编译选项。
更改编译选项会影响在给定 Erlang VM 节点上运行的所有进程。要存储单个选项以及所有选项的说明,请参见 put_compiler_option/2
。
返回包含先前值的映射。
示例
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
从 Erlang VM 代码路径列表中删除路径。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
路径在被删除之前将使用 Path.expand/1
进行扩展。如果路径不存在,此函数将返回 false
。
示例
Code.prepend_path(".")
Code.delete_path(".")
#=> true
Code.delete_path("/does_not_exist")
#=> false
@spec delete_paths([Path.t()]) :: :ok
从 Erlang VM 代码路径列表中删除路径列表。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
路径在被删除之前将使用 Path.expand/1
进行扩展。如果路径不存在,此函数将返回 false
。
@spec ensure_all_loaded([module()]) :: :ok | {:error, [{module(), reason}]} when reason: :badfile | :nofile | :on_load_failure
确保加载给定的模块。
类似于 ensure_loaded/1
,但它接受模块列表而不是单个模块,并加载所有模块。
如果所有模块都成功加载,则返回 :ok
。否则,返回 {:error, errors}
,其中 errors
是一个元组列表,包含模块及其加载失败的原因。
示例
iex> Code.ensure_all_loaded([Atom, String])
:ok
iex> Code.ensure_all_loaded([Atom, DoesNotExist])
{:error, [{DoesNotExist, :nofile}]}
@spec ensure_all_loaded!([module()]) :: :ok
与 ensure_all_loaded/1
相同,但如果任何模块无法加载,则会引发异常。
@spec ensure_compiled(module()) :: {:module, module()} | {:error, :embedded | :badfile | :nofile | :on_load_failure | :unavailable}
类似于 ensure_compiled!/1
,但表示您可以继续,即使没有该模块。
虽然 ensure_compiled!/1
表示对 Elixir 编译器来说,只有在该模块可用时才能继续,但此函数表示即使没有该模块,也可以继续编译。
如果它成功加载了模块,它将返回 {:module, module}
。如果没有,则返回 {:error, reason}
,其中包含错误原因。如果正在检查的模块当前处于编译器死锁状态,此函数将返回 {:error, :unavailable}
。不可用并不一定意味着模块不存在,而仅仅是它当前不可用,但它可能在将来变得可用(也可能不会)。
因此,如果您只有在模块可用时才能继续,请使用 ensure_compiled!/1
。特别是,不要这样做
case Code.ensure_compiled(module) do
{:module, _} -> module
{:error, _} -> raise ...
end
有关代码加载的更多信息,请参见模块文档。
确保给定的模块已编译并加载。
如果模块已加载,它将无操作。如果模块尚未编译,ensure_compiled!/1
将停止调用者的编译,直到传递给 ensure_compiled!/1
的模块变为可用,或者当前项目的所有文件都被编译。如果编译完成,并且模块不可用或处于死锁状态,则会引发错误。
由于此函数会停止编译,因此请谨慎使用。特别是,避免使用它来猜测系统中有哪些模块。过度使用此函数也可能导致死锁,其中两个模块同时检查对方是否已编译。这将返回一个特定的不可用错误代码,在该代码中,我们无法成功验证模块是否可用。
有关代码加载的更多信息,请参见模块文档。
@spec ensure_loaded(module()) :: {:module, module()} | {:error, :embedded | :badfile | :nofile | :on_load_failure}
确保给定的模块已加载。
如果模块已加载,此函数将无操作。如果模块尚未加载,它将尝试加载它。
如果它成功加载了模块,它将返回 {:module, module}
。如果没有,则返回 {:error, reason}
,其中包含错误原因。
有关代码加载的更多信息,请参见模块文档。
示例
iex> Code.ensure_loaded(Atom)
{:module, Atom}
iex> Code.ensure_loaded(DoesNotExist)
{:error, :nofile}
与 ensure_loaded/1
相同,但如果模块无法加载,则会引发异常。
确保给定的模块已加载。
类似于 ensure_loaded/1
,但如果模块已加载或已成功加载,则返回 true
。否则返回 false
。
示例
iex> Code.ensure_loaded?(String)
true
返回用于评估的环境。
它接受 Macro.Env
(随后会被修剪和准备)或一个选项列表。它返回一个准备评估的环境。
此模块中的大多数函数会自动为给定环境准备评估,因此您无需显式调用此函数,除了 eval_quoted_with_env/3
,该函数被设计成在循环中调用,以实现交互式 shell 或其他具有多个评估的功能。
选项
如果未给出 env,则选项可以是
:file
- 评估中要考虑的文件:line
- 脚本开始的行号
评估给定的文件。
接受 relative_to
作为参数,以告知文件的位置。
虽然 require_file/2
和 compile_file/2
返回加载的模块及其字节码,但 eval_file/2
仅评估文件内容并返回评估结果及其绑定(与 eval_string/3
的返回值完全相同)。
评估引用的内容。
警告:在宏内调用此函数被认为是不良实践,因为它会在编译时尝试评估运行时值。宏参数通常通过取消引用它们来转换为返回的引号表达式(而不是被评估)。
有关 binding
和 opts
的描述,请参见 eval_string/3
。
示例
iex> contents = quote(do: var!(a) + var!(b))
iex> {result, binding} = Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2]
为了方便起见,您可以将 __ENV__/0
作为 opts
参数传递,所有选项将自动从当前环境中提取
iex> contents = quote(do: var!(a) + var!(b))
iex> {result, binding} = Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2]
@spec eval_quoted_with_env(Macro.t(), binding(), Macro.Env.t(), keyword()) :: {term(), binding(), Macro.Env.t()}
使用 binding
和 env
评估给定的 quoted
内容。
此函数旨在在循环中调用,以实现交互式 shell 或其他具有多个评估的功能。因此,您第一次调用此函数时,必须使用 env_for_eval/1
计算初始环境。其余调用必须传递此函数返回的环境。
选项
:prune_binding
- (自 v1.14.2 起) 修剪绑定以仅保留已读取或写入的变量。请注意,模块使用的变量始终会被修剪,即使后来被模块使用。您可以提交给:on_module
跟踪器事件,并从其环境中访问模块使用的变量。
@spec eval_string(List.Chars.t(), binding(), Macro.Env.t() | keyword()) :: {term(), binding()}
评估由 string
给出的内容。
binding
参数是所有变量及其值的列表。 opts
参数是一个包含环境选项的关键字列表。
警告:string
可以是任何 Elixir 代码,并且将以与 Erlang VM 相同的权限执行:这意味着此类代码可能危及机器(例如通过执行系统命令)。不要在不受信任的输入(例如来自网络的字符串)上使用 eval_string/3
。
选项
选项可以是
:file
- 评估中要考虑的文件:line
- 脚本开始的行号
此外,您还可以将环境作为第二个参数传递,以便在该环境中进行评估。
返回一个形式为 {value, binding}
的元组,其中 value
是从评估 string
返回的值。如果在评估 string
时发生错误,将引发异常。
binding
是一个列表,包含所有变量名称及其在评估 string
后的值。绑定键通常是原子,但它们可能是不同上下文中定义的变量的元组。名称没有特定顺序。
示例
iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2]
iex> {result, binding} = Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2, c: 3]
iex> {result, binding} = Code.eval_string("a = a + b", [a: 1, b: 2])
iex> result
3
iex> Enum.sort(binding)
[a: 3, b: 2]
为了方便起见,您可以将 __ENV__/0
作为 opts
参数传递,当前环境中定义的所有导入、要求和别名将自动被继承
iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2]
@spec fetch_docs(module() | String.t()) :: {:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata, docs :: [doc_element]} | {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary()}} when annotation: :erl_anno.anno(), beam_language: :elixir | :erlang | atom(), doc_content: %{optional(binary()) => binary()} | :none | :hidden, doc_element: {{kind :: atom(), function_name :: atom(), arity()}, annotation, signature, doc_content, metadata}, format: binary(), signature: [binary()], metadata: map()
返回给定模块或 .beam
文件路径的文档。
给定模块名称时,它会找到其 BEAM 代码并从中读取文档。
给定 .beam
文件的路径时,它会直接从该文件加载文档。
它返回存储在文档块中的术语,该术语的格式由 EEP 48 定义,或者如果文档块不可用,则返回 {:error, reason}
。
示例
# Module documentation of an existing module
iex> {:docs_v1, _, :elixir, _, %{"en" => module_doc}, _, _} = Code.fetch_docs(Atom)
iex> module_doc |> String.split("\n") |> Enum.at(0)
"Atoms are constants whose values are their own name."
# A module that doesn't exist
iex> Code.fetch_docs(ModuleNotGood)
{:error, :module_not_found}
格式化文件。
有关代码格式和可用选项的更多信息,请参见 format_string!/2
。
格式化给定的代码 string
。
格式化程序接收一个表示 Elixir 代码的字符串,并返回根据预定义规则格式化的代码的 iodata。
选项
:file
- 包含字符串的文件,用于错误报告:line
- 字符串开始的行号,用于错误报告:line_length
- 格式化文档时要达到的行长。默认为 98。请注意,此值用作指导,但在某些情况下不会强制执行。有关详细信息,请参见下面的“行长”部分:locals_without_parens
- 一个包含名称和元组对的关键字列表,这些名称和元组对应尽可能保持不带括号。元组可以是原子:*
,这意味着该名称的所有元组。格式化程序已包含函数列表,此选项会扩充该列表。:force_do_end_blocks
(自 v1.9.0 起) - 当true
时,将所有内联使用do: ...
、else: ...
等转换为do
-end
块。默认为false
。请注意,此选项是收敛的:一旦您将其设置为true
,所有关键字都将被转换。如果您稍后将其设置为false
,do
-end
块将不会转换回关键字。:normalize_bitstring_modifiers
(自 v1.14.0 起) - 当true
时,将删除已知位串 修饰符 中不必要的括号,例如<<foo::binary()>>
变成<<foo::binary>>
,或者为自定义修饰符添加括号,其中<<foo::custom_type>>
变成<<foo::custom_type()>>
。默认为true
。此选项会更改 AST。:normalize_charlists_as_sigils
(自 v1.15.0 起) - 当true
时,将字符列表格式化为~c
标识,例如'foo'
变成~c"foo"
。默认为true
。此选项会更改 AST。
设计原则
格式化程序是在三个原则下设计的。
首先,格式化程序永远不会改变代码的语义。这意味着输入 AST 和输出 AST 几乎总是等效的。格式化程序更改 AST 的唯一情况是输入 AST 会导致编译器警告,而输出 AST 不会。如果需要,可以通过格式化选项禁用格式化程序更改 AST 的情况。
第二个原则是提供尽可能少的配置。这通过消除争论点,简化了格式化程序的采用,同时确保整个社区始终遵循单一风格。
格式化程序不会硬编码名称。格式化程序不会因为函数名为 defmodule
、def
等而表现出特殊行为。此原则反映了 Elixir 的目标,即成为一种可扩展的语言,开发人员可以在其中扩展语言,就像它们是语言的一部分一样。当绝对有必要根据名称更改行为时,这种行为应该是可配置的,例如 :locals_without_parens
选项。
运行格式化程序
格式化程序尝试在单行中尽可能多地容纳,并在无法容纳时尽可能引入换行符。
在某些情况下,这可能会导致不希望的格式。因此,格式化程序生成的某些代码可能在美学上不令人满意,可能需要开发人员明确干预。这就是我们不建议在现有代码库中盲目运行格式化程序的原因。相反,您应该格式化每个格式化的文件并进行完整性检查。
例如,格式化程序可能会将一个长函数定义拆分为多个子句
def my_function(
%User{name: name, age: age, ...},
arg1,
arg2
) do
...
end
虽然上面的代码完全有效,但您可能更愿意在函数体内部对结构变量进行匹配,以便将定义保留在单行中
def my_function(%User{} = user, arg1, arg2) do
%{name: name, age: age, ...} = user
...
end
在某些情况下,您可以利用格式化程序不会生成优雅代码这一事实作为重构的提示。以这段代码为例
def board?(board_id, %User{} = user, available_permissions, required_permissions) do
Tracker.OrganizationMembers.user_in_organization?(user.id, board.organization_id) and
required_permissions == Enum.to_list(MapSet.intersection(MapSet.new(required_permissions), MapSet.new(available_permissions)))
end
上面的代码行数非常长,运行格式化程序不会解决这个问题。事实上,格式化程序可能会使您复杂的表达式更加明显
def board?(board_id, %User{} = user, available_permissions, required_permissions) do
Tracker.OrganizationMembers.user_in_organization?(user.id, board.organization_id) and
required_permissions ==
Enum.to_list(
MapSet.intersection(
MapSet.new(required_permissions),
MapSet.new(available_permissions)
)
)
end
将此类情况视为对代码进行重构的建议
def board?(board_id, %User{} = user, available_permissions, required_permissions) do
Tracker.OrganizationMembers.user_in_organization?(user.id, board.organization_id) and
matching_permissions?(required_permissions, available_permissions)
end
defp matching_permissions?(required_permissions, available_permissions) do
intersection =
required_permissions
|> MapSet.new()
|> MapSet.intersection(MapSet.new(available_permissions))
|> Enum.to_list()
required_permissions == intersection
end
总结一下:由于格式化程序无法改变代码的语义,因此有时需要调整或重构代码才能获得最佳格式。为了帮助您更好地了解如何控制格式化程序,我们在后面的部分中描述了格式化程序保留用户编码的情况以及如何控制多行表达式。
行长
关于格式化程序的另一个要点是,:line_length
配置只是一个指导。在很多情况下,格式化程序无法将代码拆分,这意味着它会超出行长。例如,如果你有一个很长的字符串
"this is a very long string that will go over the line length"
格式化程序不知道如何在不改变底层代码语法表示的情况下将其拆分,因此需要你自己进行干预。
"this is a very long string " <>
"that will go over the line length"
字符串连接使得代码能够在一行内显示,同时也为格式化程序提供了更多选择。
这也可能出现在 do/end 块中,其中 do
关键字(或 ->
)可能超出行长,因为格式化程序没有机会以可读的方式引入换行符。例如,如果你执行
case very_long_expression() do
end
并且只有 do
关键字超过行长,Elixir **不会**发出此警告
case very_long_expression()
do
end
因此,它更倾向于完全不修改该行,并保留 do
超出行长限制。
保留用户的格式
格式化程序在某些情况下会尊重输入格式。这些列在下面
数字中不重要的数字将保持原样。但是,格式化程序始终会为超过 5 位数的十进制数插入下划线,并将十六进制数字转换为大写
字符串、字符列表、原子和标识符将保持原样。不会自动转义或取消转义任何字符。分隔符的选择也尊重来自输入的设置
块内部的换行符将保持与输入中的相同,除了
- 跨越多行的表达式前后始终有一个空行,并且空行始终被压缩成一个空行
使用
:do
关键字和do
-end
块的选择由用户决定列表、元组、位串、映射、结构体和函数调用将被拆分为多行,如果它们在起始括号后跟随换行符,并在结束括号前以换行符开头
某些运算符(例如管道运算符)之前的换行符,以及其他运算符(例如比较运算符)之前的换行符
上述行为并非绝对保证。我们可能会在将来删除或添加新的规则。记录这些规则是为了让你更好地了解对格式化程序的预期。
多行列表、映射、元组等
你可以通过在起始括号后添加换行符,并在结束括号行前添加换行符,强制列表、元组、位串、映射、结构体和函数调用每行包含一个条目。例如
[
foo,
bar
]
如果括号周围没有换行符,那么格式化程序将尝试将所有内容都放在一行中,例如,下面的代码段
[foo,
bar]
将被格式化为
[foo, bar]
你也可以通过将每个条目都放在单独的行上,强制函数调用和关键字在多行上显示
defstruct name: nil,
age: 0
上面的代码将由格式化程序保留为每行一个关键字条目。为了避免这种情况,只需将所有内容压缩到一行即可。
函数调用中的括号和无括号
Elixir 具有两种函数调用语法。使用括号和不使用括号。默认情况下,Elixir 会为所有调用添加括号,除了
- 包含
do
-end
块的调用 - 没有括号的本地调用,其中本地调用的名称和参数个数也在
:locals_without_parens
中列出(除了参数个数为 0 的调用,编译器始终要求使用括号)
括号和无括号的选择也会影响缩进。当带括号的函数调用无法在一行内显示时,格式化程序会在括号周围引入换行符,并将参数缩进两个空格
some_call(
arg1,
arg2,
arg3
)
另一方面,没有括号的函数调用始终按函数调用本身的长度缩进,如下所示
some_call arg1,
arg2,
arg3
如果最后一个参数是数据结构(例如映射和列表),并且数据结构的开头与函数调用位于同一行,则不会进行缩进,这样就可以写出这样的代码
Enum.reduce(some_collection, initial_value, fn element, acc ->
# code
end)
some_function_without_parens %{
foo: :bar,
baz: :bat
}
代码注释
格式化程序还以一种方式处理代码注释,以确保在注释开头(#)和下一个字符之间始终添加一个空格。
格式化程序还会将所有尾随注释提取到它们的前一行。例如,下面的代码
hello #world
将被重写为
# world
hello
由于代码注释与代码表示(AST)分开处理,因此在某些情况下,代码注释被代码格式化程序视为模棱两可。例如,下面匿名函数中的注释
fn
arg1 ->
body1
# comment
arg2 ->
body2
end
以及这个
fn
arg1 ->
body1
# comment
arg2 ->
body2
end
被认为是等效的(嵌套与大多数用户格式化一起被丢弃)。在这种情况下,代码格式化程序将始终格式化为后者。
换行符
格式化程序将代码中的所有换行符从 \r\n
转换为 \n
。
返回给定编译器选项的值。
有关所有选项的说明,请参见 put_compiler_option/2
。
示例
Code.get_compiler_option(:debug_info)
#=> true
如果模块已加载,则返回 true
。
此函数不会尝试加载模块。对于此类行为,可以使用 ensure_loaded?/1
。
示例
iex> Code.loaded?(Atom)
true
iex> Code.loaded?(NotYetLoaded)
false
将路径添加到 Erlang VM 代码路径列表的开头。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
该路径在追加之前使用 Path.expand/1
进行扩展。它要求路径存在。返回一个布尔值,表示路径是否已成功添加。
示例
Code.prepend_path(".")
#=> true
Code.prepend_path("/does_not_exist")
#=> false
选项
:cache
- (自 v1.15.0 起)当为 true 时,代码路径在首次遍历时会被缓存,以减少文件系统操作。它需要 Erlang/OTP 26,否则将无效。
将 paths
的列表添加到 Erlang VM 代码路径列表的开头。
这是 Erlang VM 用于查找模块代码的目录列表。文件列表在每个 Erlang VM 节点上进行管理。
所有路径在追加之前使用 Path.expand/1
进行扩展。仅追加存在的路径。此函数始终返回 :ok
,无论追加了多少路径。如果需要更多控制,请使用 prepend_path/1
。
示例
Code.prepend_paths([".", "/does_not_exist"])
#=> :ok
选项
:cache
- 当为 true 时,代码路径在首次遍历时会被缓存,以减少文件系统操作。它需要 Erlang/OTP 26,否则将无效。
@spec print_diagnostic( diagnostic(:warning | :error), keyword() ) :: :ok
将诊断信息打印到标准错误流。
诊断要么由 Kernel.ParallelCompiler
返回,要么由 Code.with_diagnostics/2
返回。
选项
:snippet
- 是否读取诊断位置的代码片段。由于它可能会影响性能,因此不建议在运行时使用。默认值为true
。
@spec purge_compiler_modules() :: {:ok, non_neg_integer()}
清除编译器模块。
编译器使用临时模块来编译代码。例如,elixir_compiler_1
、elixir_compiler_2
等等。如果编译后的代码存储对匿名函数或类似内容的引用,Elixir 编译器可能无法回收这些模块,从而在内存中保留不必要的代码量,最终导致出现类似 elixir_compiler_12345
的模块。
此函数会清除编译器当前保留的所有模块,从而允许重复使用旧的编译器模块名称。如果任何进程正在运行来自这些模块的任何代码,它们也会被终止。
此函数仅供长时间运行的节点(不断评估代码)调用。
它返回 {:ok, number_of_modules_purged}
。
存储编译选项。
更改编译选项会影响在给定 Erlang VM 节点中运行的所有进程。
可用选项为
:docs
- 当为true
时,在编译后的模块中保留文档。默认值为true
。:debug_info
- 当为true
时,在编译后的模块中保留调试信息。默认值为true
。这将启用静态分析工具,因为它允许开发者部分重建原始源代码。因此,不建议禁用:debug_info
,因为它会删除 Elixir 编译器和其他工具提供反馈的能力。如果你想在部署时删除:debug_info
,mix release
之类的工具默认情况下会这样做。此外,mix test
通过:test_elixirc_options
项目配置选项禁用它。此选项也可以使用@compile
指令在每个模块的基础上进行覆盖。:ignore_already_consolidated
(自 v1.10.0 起) - 当为true
时,当协议已合并并添加了新的实现时,不会发出警告。默认值为false
。:ignore_module_conflict
- 当为true
时,当模块已定义时,不会发出警告。默认值为false
。:relative_paths
- 当为true
时,在编译器生成的引号节点、警告和错误中使用相对路径。请注意,禁用此选项不会影响运行时警告和错误。默认值为true
。:warnings_as_errors
- 当生成警告时,导致编译失败。默认值为false
。:no_warn_undefined
(自 v1.10.0 起) - 模块和{Mod, fun, arity}
元组的列表,它们不会在编译时发出模块或函数不存在的警告。传递原子:all
以跳过对所有未定义函数的警告。这在进行动态编译时非常有用。默认值为[]
。:tracers
(自 v1.10.0 起) - 在编译期间要使用的跟踪器(模块)列表。有关更多信息,请参阅模块文档。默认值为[]
。:parser_options
(从 v1.10.0 开始) - 一个包含选项的关键字列表,在编译文件时传递给解析器。它接受与string_to_quoted/2
相同的选项(除了那些更改 AST 本身的选项)。这可以与跟踪器一起使用,以便在编译期间检索有关事件的本地化信息。默认值为[columns: true]
。此选项仅影响代码编译函数,例如compile_string/2
和compile_file/2
,但不影响string_to_quoted/2
及其同类函数,因为后者用于除编译之外的其他目的。:on_undefined_variable
(从 v1.15.0 开始) - 可以是:raise
或:warn
。当设置为:raise
(默认值)时,未定义的变量将触发编译错误。如果希望未定义的变量发出警告并扩展为对同名零元函数的本地调用(例如,node
将扩展为node()
),可以将其设置为:warn
。这种:warn
行为仅出于与旧依赖项兼容的原因而存在。
它始终返回 :ok
。对于无效的选项,将引发错误。
示例
Code.put_compiler_option(:debug_info, true)
#=> :ok
@spec quoted_to_algebra( Macro.t(), keyword() ) :: Inspect.Algebra.t()
使用 Elixir 的格式化规则将引用的表达式转换为代数文档。
可以通过调用以下代码将代数文档转换为字符串:
doc
|> Inspect.Algebra.format(:infinity)
|> IO.iodata_to_binary()
对于执行相同操作的高级函数,请参见 Macro.to_string/1
.
格式化注意事项
Elixir AST 不包含用于字符串、列表或具有两个元素的元组之类的字面量的元数据,这意味着生成的代数文档不会尊重所有用户偏好,注释可能会放错位置。为了获得更好的结果,可以使用 :token_metadata
、:unescape
和 :literal_encoder
选项传递给 string_to_quoted/2
,以便为格式化程序提供更多信息
[
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
unescape: false
]
这将生成一个包含信息的 AST,例如 do
块的开始和结束行或 sigil 定界符,通过将字面量包装在块中,它们现在可以包含元数据,例如行号、字符串定界符和转义序列,或整数格式(例如 0x2a
而不是 47
)。但是,**请注意此 AST 无效**。如果您评估它,它将不会具有与常规 Elixir AST 相同的语义,因为 :unescape
和 :literal_encoder
选项。但是,如果您正在进行源代码操作,则这些选项很有用,在源代码操作中,保留用户选择和注释放置非常重要。
选项
:comments
- 与引用的表达式关联的注释列表。默认值为[]
。建议将:token_metadata
和:literal_encoder
选项传递给string_to_quoted_with_comments/2
,以便为注释获得正确的放置位置:escape
- 当设置为true
时,转义序列(如\n
)将被转义为\\n
。如果使用string_to_quoted/2
时:unescape
选项被设置为false
,则将此选项设置为false
将阻止它对序列进行两次转义。默认值为true
。:locals_without_parens
- 一个包含名称和元组对的关键字列表,这些名称和元组对应尽可能保持不带括号。元组可以是原子:*
,这意味着该名称的所有元组。格式化程序已包含函数列表,此选项会扩充该列表。:syntax_colors
- 用于对输出进行着色的颜色关键字列表。有关更多信息,请参见Inspect.Opts
.
需要给定的 file
。
接受 relative_to
作为参数,以指示文件的位置。如果文件已被引入,require_file/2
不会执行任何操作并返回 nil
。
请注意,如果 require_file/2
被不同的进程同时调用,则第一个调用 require_file/2
的进程将获取锁,其余进程将被阻塞,直到文件可用。这意味着,如果 require_file/2
被同一个文件调用多次,则该文件将仅编译一次。第一个调用 require_file/2
的进程将获得已加载模块的列表,其他进程将获得 nil
。引入文件的列表是按 Erlang VM 节点管理的。
如果您想编译文件但不跟踪其文件名,请参见 compile_file/2
。最后,如果您想获得评估文件的结果而不是其中定义的模块,请参见 eval_file/2
.
示例
如果文件尚未引入,它将返回模块列表
modules = Code.require_file("eex_test.exs", "../eex/test")
List.first(modules)
#=> {EExTest.Compiled, <<70, 79, 82, 49, ...>>}
如果文件已被引入,它将返回 nil
Code.require_file("eex_test.exs", "../eex/test")
#=> nil
@spec required_files() :: [binary()]
列出所有所需文件。
示例
Code.require_file("../eex/test/eex_test.exs")
List.first(Code.required_files()) =~ "eex_test.exs"
#=> true
@spec string_to_quoted( List.Chars.t(), keyword() ) :: {:ok, Macro.t()} | {:error, {location :: keyword(), binary() | {binary(), binary()}, binary()}}
将给定的字符串转换为其引用形式。
如果成功,将返回 {:ok, quoted_form}
,否则返回 {:error, {meta, message_info, token}}
。
选项
:file
- 在发生解析错误时要报告的文件名。默认值为"nofile"
。:line
- 被解析的字符串的起始行。默认值为 1。:column
- (从 v1.11.0 开始) 被解析的字符串的起始列。默认值为 1。:columns
- 当设置为true
时,将:column
键附加到引用的元数据。默认值为false
。:unescape
(从 v1.10.0 开始) - 当设置为false
时,保留转义序列。例如,"null byte\\t\\x00"
将保持原样,而不是被转换为比特串字面量。请注意,如果将此选项设置为 false,则生成的 AST 将不再有效,但它可能有助于分析/转换源代码,通常与quoted_to_algebra/2
一起使用。默认值为true
。:existing_atoms_only
- 当设置为true
时,如果词法分析器发现不存在的原子,则会引发错误。默认值为false
。:token_metadata
(从 v1.10.0 开始) - 当设置为true
时,在表达式 AST 中包含与令牌相关的元数据,例如do
和end
令牌的元数据,用于关闭令牌、表达式结束以及 sigil 的定界符。请参见Macro.metadata/0
。默认值为false
。:literal_encoder
(从 v1.10.0 开始) - 如何在 AST 中编码字面量。它必须是一个接受两个参数的函数,一个是字面量,另一个是其元数据,并且它必须返回{:ok, ast :: Macro.t}
或{:error, reason :: binary}
。如果您返回的term
与字面量本身不同,则 AST 将不再有效。如果您想对源代码进行文本分析,则此选项可能仍然有用。:static_atoms_encoder
- 静态原子编码器函数,请参见下面的“:static_atoms_encoder
函数”部分。请注意,此选项会覆盖静态原子的:existing_atoms_only
行为,但:existing_atoms_only
仍用于动态原子,例如包含插值的原子。:emit_warnings
(从 v1.16.0 开始) - 当设置为false
时,不会发出与词法分析/解析相关的警告。默认值为true
。
Macro.to_string/2
将字符串转换为其引用形式的反向操作是 Macro.to_string/2
,它将引用形式转换为字符串/二进制表示形式。
:static_atoms_encoder
函数
当将 static_atoms_encoder: &my_encoder/2
作为参数传递时,每次词法分析器需要创建“静态”原子时,都会调用 my_encoder/2
。静态原子是 AST 中的原子,用作别名、远程调用、本地调用、变量名、常规原子和关键字列表。
编码器函数将接收原子名称(作为二进制)和一个包含当前文件、行和列的关键字列表。它必须返回 {:ok, token :: term} | {:error, reason :: binary}
。
编码器函数应该从给定的字符串创建原子。为了生成有效的 AST,需要返回 {:ok, term}
,其中 term
是一个原子。可以返回原子以外的其他东西,但是,在这种情况下,AST 将不再“有效”,因为它不能用于编译或评估 Elixir 代码。一个用例是,如果您想在用户界面中使用 Elixir 解析器,但不想耗尽原子表。
原子编码器不会被调用用于 AST 中存在的所有原子。它不会被调用用于以下原子
运算符(
:+
、:-
等)语法关键字(
fn
、do
、else
等)包含插值的原子(
:"#{1 + 1} is two"
),因为这些原子是在运行时构造的用于表示单字母 sigil 的原子,如
:sigil_X
(但多字母 sigil,如:sigil_XYZ
,会被编码)。
@spec string_to_quoted!( List.Chars.t(), keyword() ) :: Macro.t()
将给定的字符串转换为其引用形式。
如果成功,它将返回 AST,否则会引发异常。如果令牌丢失(通常是因为表达式不完整),则异常将是 TokenMissingError
,否则为 SyntaxError
.
有关选项信息,请参见 string_to_quoted/2
.
@spec string_to_quoted_with_comments( List.Chars.t(), keyword() ) :: {:ok, Macro.t(), [map()]} | {:error, {location :: keyword(), term(), term()}}
将给定的字符串转换为其引用形式以及注释列表。
此函数在对源代码进行文本更改时很有用,同时保留注释和字面量位置等信息。
如果成功,将返回 {:ok, quoted_form, comments}
,否则返回 {:error, {line, error, token}}
。
注释是具有以下字段的映射
:line
- 源代码的行号:text
- 注释的完整文本,包括开头的#
:previous_eol_count
- 注释与前一个 AST 节点或注释之间有多少行结束符:next_eol_count
- 注释与下一个 AST 节点或注释之间有多少行结束符
有关选项信息,请参见 string_to_quoted/2
.
示例
iex> Code.string_to_quoted_with_comments("""
...> :foo
...>
...> # Hello, world!
...>
...>
...> # Some more comments!
...> """)
{:ok, :foo, [
%{line: 3, column: 1, previous_eol_count: 2, next_eol_count: 3, text: "# Hello, world!"},
%{line: 6, column: 1, previous_eol_count: 3, next_eol_count: 1, text: "# Some more comments!"},
]}
iex> Code.string_to_quoted_with_comments(":foo # :bar")
{:ok, :foo, [
%{line: 1, column: 6, previous_eol_count: 0, next_eol_count: 0, text: "# :bar"}
]}
@spec string_to_quoted_with_comments!( List.Chars.t(), keyword() ) :: {Macro.t(), [map()]}
将给定的字符串转换为其引用形式以及注释列表。
如果成功,返回 AST 和注释列表,否则抛出异常。如果缺少令牌(通常是因为表达式不完整),则异常为 TokenMissingError
,否则为 SyntaxError
。
有关选项信息,请参见 string_to_quoted/2
.
@spec unrequire_files([binary()]) :: :ok
从所需文件列表中删除文件。
文件中定义的模块不会被删除;调用此函数只会将它们从列表中删除,允许再次加载它们。
文件列表在每个 Erlang VM 节点中进行管理。
示例
# Require EEx test code
Code.require_file("../eex/test/eex_test.exs")
# Now unrequire all files
Code.unrequire_files(Code.required_files())
# Note that modules are still available
function_exported?(EExTest.Compiled, :before_compile, 0)
#=> true
@spec with_diagnostics( keyword(), (-> result) ) :: {result, [diagnostic(:warning | :error)]} when result: term()
执行给定的 fun
并捕获所有诊断信息。
诊断信息是在代码评估或单文件编译过程中以及由诸如 IO.warn/2
之类的函数发出的警告和错误。
如果使用 mix compile
或 Kernel.ParallelCompiler
,请注意它们已经捕获并返回诊断信息。
选项
:log
- 诊断信息是否应在发生时被记录。默认为false
。
处理错误
with_diagnostics/2
不会自动处理异常。您可以通过在fun
中添加try/1
来捕获它们。{result, all_errors_and_warnings} = Code.with_diagnostics(fn -> try do {:ok, Code.compile_quoted(quoted)} rescue err -> {:error, err} end end)