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)范畴。
造成"没有子句可以匹配"的原因有两种,缺一不可地需要被分析器识别:
- 模式(pattern)失配:所有子句的模式(参数位置上的模式)都不匹配调用实参;
- 守卫(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_clauseadd_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_disjErrors.function_clause会构造一个Function_clause诊断,见同文件 PulseModelsErlang.ml:
let function_clause : model_no_non_disj = fun {location} astate -> error (Function_clause {calling_context= []; location}) astateerror会产出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_Ok、test_tail2_Ok:实参是非空列表,匹配唯一子句,不报告;test_tail3_Bad:tail([])空列表无法匹配[_ | Xs],报告;test_empty2_Bad/test_empty3_Bad:assert_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 > 0对0为假,唯一子句被拒,报告(见 pulse/issues.exp);accepts_positive2(0):守卫是(1 =:= 1, 1 =:= 0) ; (X > 0),两个分支均为假,同样报告;accepts_all_basic:第二个子句accepts_all_basic(_)无守卫兜底,任何输入都能匹配,不报告;accepts_all_tricky2:两个子句守卫互补(X > 0与not (X > 0)),任意整数都能匹配,不报告;possible_exception(2):守卫1 div X =:= 1对2为假,报告。测试注释也标注了一个已知漏报(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.erl、nonmatch_records.erl、nonmatch_tuples.erl、nonmatch_strings.erl中均有完整覆盖,问题 ID 全部统一为NO_MATCHING_FUNCTION_CLAUSE。
如何在本地复现与运行测试
编译开启 Erlang 支持
Erlang 前端并非默认构建目标。按 erlang/README.md 的说明,需要:
- 按 INSTALL.md 从源码安装 Infer;
- 使用
./build-infer.sh erlang构建 Erlang 支持; - 确保系统安装了 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.erl中bad/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_CLAUSE | function_clause | 函数调用的实参不匹配任何子句(模式或守卫) |
NO_MATCHING_CASE_CLAUSE | case_clause | case表达式的值不匹配任何分支 |
NO_MATCHING_ELSE_CLAUSE | 无 | maybe等表达式的 else 分支缺失 |
NO_TRUE_BRANCH_IN_IF | if_clause | if表达式所有守卫均为假 |
NO_MATCHING_BRANCH_IN_TRY | 无 | try ... of的所有of分支失配 |
NO_MATCH_OF_RHS | badmatch | 匹配表达式=右侧无法匹配左侧模式 |
BAD_MAP/BAD_KEY/BAD_RECORD | badmap/badkey/badrecord | map 上调用非 map、键不存在、record 字段非法 |
这些错误族的建模方式完全同构:前端翻译阶段通过add_crash_node在各语言构造的失败路径上插入对应 builtin(ErlangTranslator.ml 中case、if表达式同样处理),Pulse 侧统一映射为ErlangError诊断并最终分发到各自的 IssueType。理解这一体系后,只要掌握了 NO_MATCHING_FUNCTION_CLAUSE 一条链路,就能顺藤摸瓜理解其余所有 Erlang 运行时错误检查项的实现。
小结
- NO_MATCHING_FUNCTION_CLAUSE 是 Infer 对 Erlang
function_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),仅供参考