Infer 静态分析器 NO_MATCHING_FUNCTION_CLAUSE 检查项全解:Erlang 函数子句失配检测原理与实践
2026/9/23 7:41:24 网站建设 项目流程

Infer 静态分析器 NO_MATCHING_FUNCTION_CLAUSE 检查项全解:Erlang 函数子句失配检测原理与实践

【免费下载链接】inferA static analyzer for Java, C, C++, and Objective-C项目地址: https://gitcode.com/gh_mirrors/infer/infer

本篇技术指南聚焦 Meta 开源静态分析器 Infer 对 Erlang 语言的检查项NO_MATCHING_FUNCTION_CLAUSE(对应 Erlang 运行时function_clause错误),围绕该检查项的定义、触发条件、Pulse 抽象解释器底层的实现链路,以及仓库测试中的真实用例展开讲解。读完本文,你将掌握该检查项在 Infer 中如何建模函数子句失配、如何复现与复跑对应测试,以及如何用infer --pulse-only -- erlc在真实 Erlang 工程中捕捉这类潜在崩溃。

检查项定义:什么会被报告

按照 NO_MATCHING_FUNCTION_CLAUSE.md 的定义:当一次函数调用的实参无法匹配该函数的任意一个子句(clause)时,Infer 就会报告NO_MATCHING_FUNCTION_CLAUSE。它对应的是 Erlang 运行时(BEAM)的function_clause错误,属于运行时异常(RuntimeException)范畴。

造成"没有子句可以匹配"的原因有两种,缺一不可地需要被分析器识别:

  1. 模式(pattern)失配:所有子句的模式(参数位置上的模式)都不匹配调用实参;
  2. 守卫(guard)失配:即使模式匹配成功,但所有子句的守卫表达式(when子句)求值为false,导致该子句同样不能选用。

原文给出的例子非常精炼:若tail的完整定义只有一个子句tail([_|Xs]) -> Xs.,那么调用tail([])就会因模式失配而报告此错误——空列表无法匹配非空列表模式[_|Xs]

该检查项在源码中的注册位置位于 IssueType.ml:

let no_matching_function_clause = register_with_latent ~category:RuntimeException ~id:"NO_MATCHING_FUNCTION_CLAUSE" Error Pulse ~user_documentation:[%blob "./documentation/issues/NO_MATCHING_FUNCTION_CLAUSE.md"]

这里有几个关键信息值得展开:

  • ~id:"NO_MATCHING_FUNCTION_CLAUSE":报告输出中的稳定问题类型标识,与文档文件名一一对应;
  • Error:默认严重级别为 Error(错误),意味着被分析路径上该异常可能触发;
  • Pulse:该检查项由 Pulse 抽象解释器产生(区别于 BufferOverrun、Cost 等其他分析器);
  • register_with_latent:说明该检查项同时支持 latent(潜在)变体,报告 ID 为NO_MATCHING_FUNCTION_CLAUSE_LATENT。所谓 latent 是指该函数本身的调用路径上实参类型尚不明确、无法当场断定必然失配,但一旦调用方传入了不兼容的实参,就会升级为确定的错误报告。

Pulse 如何建模:从 Builtin 到诊断的完整调用链

前端翻译:为失配路径插入崩溃节点

Infer 的 Erlang 前端将 Erlang AST 翻译为中间表示(CFG)时,在 ErlangTranslator.ml 的translate_function_clauses函数中处理函数的所有子句:先把形参加载到新标识符中,再将每个子句翻译成匹配 case,最后通过add_crash_node在所有子句都匹配失败的控制流路径上接入一个"崩溃节点":

let clauses_blocks = let match_cases = List.map ~f:(translate_case_clause env idents) clauses in add_crash_node env (Block.any env match_cases) BuiltinDecl.__erlang_error_function_clause

add_crash_node的定义在同文件的 ErlangTranslator.ml:它把代表"匹配失败出口"的exit_failure节点后接到一个以__erlang_error_function_clause命名的崩溃节点上,从而把"没有任何子句匹配"这一条语义路径显式建模为一次异常。

与函数子句配套的还有一整套 Erlang 运行时错误 builtin,集中定义在 BuiltinDecl.ml,包括__erlang_error_badmatch__erlang_error_case_clause__erlang_error_else_clause__erlang_error_if_clause__erlang_error_try_clause__erlang_error_function_clause等,声明见 BuiltinDecl.mli。它们分别对应不同的运行时异常族,而本检查项只负责其中function_clause一族。

值得注意的是,这一机制不仅覆盖顶层函数,也覆盖 lambda/匿名函数与fun表达式(translate_function_clauses同时被顶层函数翻译与闭包翻译复用),因此对匿名函数子句失配同样能检测。

Pulse 模型:把崩溃节点映射为 ErlangError 诊断

进入 Pulse 抽象解释器后,这些 builtin 崩溃节点被模型化为对应的错误。在 PulseModelsErlang.ml 的 builtin 模型中可以看到映射表:

; +BuiltinDecl.(match_builtin __erlang_error_function_clause) <>--> Errors.function_clause |> with_non_disj

Errors.function_clause会构造一个Function_clause诊断,见同文件 PulseModelsErlang.ml:

let function_clause : model_no_non_disj = fun {location} astate -> error (Function_clause {calling_context= []; location}) astate

error会产出FatalError(致命错误),表示"一旦走到这条路径程序必然崩溃"。

这里还有一处重要的工程细节:Errors模块的选择受配置开关Config.erlang_reliability控制(见 PulseModelsErlang.ml):

module Errors : ERRORS = (val if Config.erlang_reliability then (module ErrorsReport) else (module ErrorsSilent) : ERRORS)
  • 启用 Erlang 可靠性分析时,使用ErrorsReport,真实报告错误;
  • 未启用时,使用ErrorsSilent(所有错误模型退化为stuck,即"卡住"而不报告),避免在不关注可靠性的场景下引入噪声。

诊断到问题类型的最终映射

最后,Pulse 的诊断需要映射为 IssueType 才能进入最终报告。在 PulseDiagnostic.ml 中:

| ErlangError (Function_clause _), _ -> IssueType.no_matching_function_clause ~latent

至此,完整的链路是:

Erlang AST → ErlangTranslator.translate_function_clauses(插入崩溃节点) → BuiltinDecl.__erlang_error_function_clause → PulseModelsErlang 的 builtin 模型(Errors.function_clause) → PulseDiagnostic(Function_clause → IssueType.no_matching_function_clause) → 报告 NO_MATCHING_FUNCTION_CLAUSE / NO_MATCHING_FUNCTION_CLAUSE_LATENT

触发场景与真实测试用例

仓库在 infer/tests/codetoanalyze/erlang/pulse/nonmatch/ 目录下集中放置了大量针对失配检测的测试文件,从原子、整数、列表、元组、map、record、字符串、守卫到 try/catch 表达式全覆盖。下面选取几个与原文例子直接对应的用例,结合期望输出(.exp)解读。

列表模式失配:tail([]) 的完整再现

nonmatch_lists.erl 中正是原文示例的完整版:

tail([_ | Xs]) -> Xs. assert_empty([]) -> ok. assert_second_is_nil([_, [] | _]) -> ok. test_tail1_Ok() -> tail([1, 2]). test_tail2_Ok() -> tail([1]). test_tail3_Bad() -> tail([]). test_empty1_Ok() -> assert_empty([]). test_empty2_Bad() -> assert_empty([1]). test_empty3_Bad() -> assert_empty([1, 2]).
  • test_tail1_Oktest_tail2_Ok:实参是非空列表,匹配唯一子句,不报告;
  • test_tail3_Badtail([])空列表无法匹配[_ | Xs],报告;
  • test_empty2_Bad/test_empty3_Badassert_empty([1])assert_empty([1,2])均无法匹配assert_empty([]),同样报告。

期望输出记录在 pulse/issues.exp,格式为文件, 函数/arity, 位置, 问题类型, bucket, ERROR, 消息

codetoanalyze/erlang/pulse/nonmatch/nonmatch_lists.erl, tail/1, 0, NO_MATCHING_FUNCTION_CLAUSE_LATENT, no_bucket, ERROR, [no matching function clause here] codetoanalyze/erlang/pulse/nonmatch/nonmatch_lists.erl, test_tail3_Bad/0, 1, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `tail/1`,no matching function clause here]

注意两种形态同时出现:

  • tail/1自身的定义处报告NO_MATCHING_FUNCTION_CLAUSE_LATENT(潜在错误,因为tail/1本身可能有合法调用者);
  • 在调用点test_tail3_Bad处报告确定的NO_MATCHING_FUNCTION_CLAUSE,并携带调用上下文:[calling context starts here, in call to tail/1, no matching function clause here]

原子与布尔值模式失配

nonmatch_atoms.erl 展示了原子模式与布尔表达式:

matches_ok(ok) -> ok. test_match3_Bad() -> matches_ok(not_ok). matches_true(true) -> ok. test_match6_Bad() -> matches_true(false). test_match7_Bad() -> matches_true(1 == 0).

matches_ok(not_ok)无法匹配matches_ok(ok)matches_true(false)matches_true(1 == 0)无法匹配matches_true(true),三者均被报告(见 pulse/issues.exp)。注意test_match5_Ok -> matches_true(1 == 1)这类常量折叠后为true的调用不会被误报。

守卫失配:when 子句导致无子句可用

nonmatch_function_guards.erl 专门验证"模式匹配但守卫拒绝"的场景:

accepts_positive(X) when X > 0 -> ok. accepts_positive2(X) when 1 =:= 1, 1 =:= 0; X > 0 -> ok. accepts_all_basic(X) when X > 0 -> ok; accepts_all_basic(_) -> ok. accepts_all_tricky2(X) when X > 0 -> ok; accepts_all_tricky2(X) when not (X > 0) -> ok. possible_exception(X) when 1 div X =:= 1 -> ok.
  • accepts_positive(0):模式X通配一切值,但守卫X > 00为假,唯一子句被拒,报告(见 pulse/issues.exp);
  • accepts_positive2(0):守卫是(1 =:= 1, 1 =:= 0) ; (X > 0),两个分支均为假,同样报告;
  • accepts_all_basic:第二个子句accepts_all_basic(_)无守卫兜底,任何输入都能匹配,不报告;
  • accepts_all_tricky2:两个子句守卫互补(X > 0not (X > 0)),任意整数都能匹配,不报告;
  • possible_exception(2):守卫1 div X =:= 12为假,报告。测试注释也标注了一个已知漏报(FN,possible_exception(0),守卫内部会先抛除零异常,见代码内注释 T95472386)。

这组用例说明:Infer 对守卫的处理不仅仅是"符号上能否满足",还会考虑守卫表达式的常量折叠与布尔逻辑(and/or/;/,),并能识别"多个子句守卫互补时不会失配"的情形。

与 catch 表达式、map/record/元组等类型的组合

  • features_catch_expr.erl 验证了与catch表达式交互时的行为:accepts_one(1) -> ok.下,accepts_one(catch 2)会报告,因为catch 2的结果类型无法匹配1这个字面量模式(见 pulse/issues.exp);
  • map 模式匹配(如#{key := _})、record 元组模式、二元组/三元组大小匹配、字符串字面量匹配等场景在nonmatch_maps.erlnonmatch_records.erlnonmatch_tuples.erlnonmatch_strings.erl中均有完整覆盖,问题 ID 全部统一为NO_MATCHING_FUNCTION_CLAUSE

如何在本地复现与运行测试

编译开启 Erlang 支持

Erlang 前端并非默认构建目标。按 erlang/README.md 的说明,需要:

  1. 按 INSTALL.md 从源码安装 Infer;
  2. 使用./build-infer.sh erlang构建 Erlang 支持;
  3. 确保系统安装了 Erlang 编译器erlc

对单个文件进行分析

对任意 Erlang 文件运行 Pulse 分析并启用可靠性检查:

infer --pulse-only -- erlc ex1.erl

--pulse-only让 Infer 只运行 Pulse 分析器(NO_MATCHING_FUNCTION_CLAUSE 由 Pulse 产生);-- erlc ex1.erl表示通过 Erlang 编译器驱动捕获,这是 Infer 对 Erlang 的标准捕获方式。原文示例tail([])即可用如下最小文件复现:

-module(ex1). -export([bad/0]). tail([_|Xs]) -> Xs. bad() -> tail([]).

运行后在报告中会看到ex1.erlbad/0处的NO_MATCHING_FUNCTION_CLAUSE错误,并附带调用上下文in call to tail/1

复跑仓库自带测试

仓库的 Erlang Pulse 测试期望文件位于 infer/tests/codetoanalyze/erlang/pulse/issues.exp,配套的构建规则在 infer/tests/erlc.make。修改测试用例或自行添加用例后,可用 Infer 自带的差异测试框架比对.exp期望输出;测试命名约定中_Bad后缀表示应当报告、_Ok后缀表示不应报告、fp_/fn_前缀分别表示当前已知的误报(FP)与漏报(FN)标注。

与其他 Erlang 失配类检查项的对比

Infer 的 Erlang 可靠性检查是一整套运行时异常检测体系,NO_MATCHING_FUNCTION_CLAUSE 只是其中一员,与之并列的还有(同一份 PulseModelsErlang.ml 映射表):

检查项Erlang 运行时错误触发场景
NO_MATCHING_FUNCTION_CLAUSEfunction_clause函数调用的实参不匹配任何子句(模式或守卫)
NO_MATCHING_CASE_CLAUSEcase_clausecase表达式的值不匹配任何分支
NO_MATCHING_ELSE_CLAUSEmaybe等表达式的 else 分支缺失
NO_TRUE_BRANCH_IN_IFif_clauseif表达式所有守卫均为假
NO_MATCHING_BRANCH_IN_TRYtry ... of的所有of分支失配
NO_MATCH_OF_RHSbadmatch匹配表达式=右侧无法匹配左侧模式
BAD_MAP/BAD_KEY/BAD_RECORDbadmap/badkey/badrecordmap 上调用非 map、键不存在、record 字段非法

这些错误族的建模方式完全同构:前端翻译阶段通过add_crash_node在各语言构造的失败路径上插入对应 builtin(ErlangTranslator.ml 中caseif表达式同样处理),Pulse 侧统一映射为ErlangError诊断并最终分发到各自的 IssueType。理解这一体系后,只要掌握了 NO_MATCHING_FUNCTION_CLAUSE 一条链路,就能顺藤摸瓜理解其余所有 Erlang 运行时错误检查项的实现。

小结

  • NO_MATCHING_FUNCTION_CLAUSE 是 Infer 对 Erlangfunction_clause运行时错误的静态预演,模式与守卫任一环节导致"无子句可用"都会触发;
  • 前端在 ErlangTranslator.ml 中为失配路径插入__erlang_error_function_clause崩溃节点,Pulse 在 PulseModelsErlang.ml 中将其映射为诊断,最终在 PulseDiagnostic.ml 落到 IssueType;
  • 报告同时存在确定形态(NO_MATCHING_FUNCTION_CLAUSE)与潜在形态(NO_MATCHING_FUNCTION_CLAUSE_LATENT),后者在调用方实参可判定时升级为前者;
  • 仓库 nonmatch/ 目录提供了原子、整数、列表、元组、map、record、字符串、守卫等全部维度的正反用例,是理解该检查项行为边界的最佳教材;
  • 实战中使用infer --pulse-only -- erlc 文件.erl即可在 CI 或本地对 Erlang 代码持续守护这类潜在崩溃。

【免费下载链接】inferA static analyzer for Java, C, C++, and Objective-C项目地址: https://gitcode.com/gh_mirrors/infer/infer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询