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

发布时间:2026/9/23 7:41:39
Infer 静态分析器 NO_MATCHING_FUNCTION_CLAUSE 检查项全解:Erlang 函数子句失配检测原理与实践 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.mllet 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.mllet function_clause : model_no_non_disj fun {location} astate - error (Function_clause {calling_context []; location}) astateerror会产出FatalError致命错误表示一旦走到这条路径程序必然崩溃。这里还有一处重要的工程细节Errors模块的选择受配置开关Config.erlang_reliability控制见 PulseModelsErlang.mlmodule 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 → PulseDiagnosticFunction_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_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 0对0为假唯一子句被拒报告见 pulse/issues.expaccepts_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为假报告。测试注释也标注了一个已知漏报FNpossible_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.expmap 模式匹配如#{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_CLAUSEfunction_clause函数调用的实参不匹配任何子句模式或守卫NO_MATCHING_CASE_CLAUSEcase_clausecase表达式的值不匹配任何分支NO_MATCHING_ELSE_CLAUSE无maybe等表达式的 else 分支缺失NO_TRUE_BRANCH_IN_IFif_clauseif表达式所有守卫均为假NO_MATCHING_BRANCH_IN_TRY无try ... of的所有of分支失配NO_MATCH_OF_RHSbadmatch匹配表达式右侧无法匹配左侧模式BAD_MAP/BAD_KEY/BAD_RECORDbadmap/badkey/badrecordmap 上调用非 map、键不存在、record 字段非法这些错误族的建模方式完全同构前端翻译阶段通过add_crash_node在各语言构造的失败路径上插入对应 builtinErlangTranslator.ml 中case、if表达式同样处理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),仅供参考