用途
本技能是一份修复 Motoko 编译器警告(moc)的指南。当被要求修复、抑制或清理来自 dfx build --check 或 moc 的 Motoko 编译器警告时应使用它。
如何运行构建检查
针对 DFX 项目(仅构建 Motoko canister):
dfx build --check 2>&1 | tee /tmp/dfx_build_output.txt
该命令在不部署的情况下对所有 canister 进行类型检查,并重定向 stderr 以捕获警告。大型项目的构建需要数分钟。
针对 MOPS 包:
find src -type f -name "*.mo" -print0 | xargs -0 -n1 $(mops toolchain bin moc) --check $(mops sources) 2>&1 | tee /tmp/moc_check_output.txt
该命令对每个文件单独使用 moc --check,以避免产生过多警告,通常比完整的 dfx build --check 更快,适合独立的 Motoko 包。
针对 ICP-CLI 项目:
icp build 2>&1 | tee /tmp/icp_build_output.txt
该命令使用较新的 icp-cli 工具对项目进行类型检查。
按类型统计警告数量:
grep -o 'M[0-9][0-9][0-9][0-9]' /tmp/*_output.txt | sort | uniq -c | sort -rn
警告代码参考
M0194 — 未使用标识符
含义:声明了变量、参数、导入或绑定,但从未使用。
修复方式:在标识符前加 _ 前缀(例如 caller → _caller)。对于 catch 绑定,使用纯 _(例如 catch (e) → catch (_)),不要用 _e。
关键陷阱:
- 函数参数解构中未使用的记录字段:应整体移除该字段。Motoko 中记录类型是结构化的并支持宽度子类型,期望
{ response : ... }的函数会接受{ context : Blob; response : ... },因为调用方的记录字段多于所需。因此正确的修复是从参数记录中删除未使用的字段:
// ❌ 错误 — 重命名字段会改变记录类型,导致 M0096 类型错误
public query func transform({
_context : Blob;
response : { ... };
}) : async { ... } { ... }
// ❌ 同样错误 — 保留它会触发 M0194 警告
public query func transform({
context : Blob;
response : { ... };
}) : async { ... } { ... }
// ✅ 正确 — 移除未使用的字段;子类型会接受调用方传入的额外字段
public query func transform({
response : { ... };
}) : async { ... } { ... }
IC 运行时传入 { context = <blob>; response = <http_response> }。由于 Motoko 使用结构化子类型,期望仅含 { response : ... } 的函数会接受同时带有 context 字段的记录——额外字段会被忽略。
规则:如果 M0194 在函数参数的记录字段上触发,应从解构模式中移除该字段,不要用 _ 前缀重命名。
- 词边界正则可能重命名方法调用。如果变量
liquidity未使用,且与poolActor.liquidity()在同一行,简单的\b正则会同时重命名两者。应使用排除点号的负向后顾/负向前瞻:
# ❌ 错误 — 同时会重命名 poolActor.liquidity()
pattern = rf'\b{identifier}\b'
# ✅ 正确 — 不会匹配点号之后的内容
pattern = rf'(?<![._a-zA-Z0-9]){re.escape(identifier)}(?![_a-zA-Z0-9])'
未使用的导入加前缀是安全的(例如 import Time "mo:base/Time" → import _Time "mo:base/Time"),但如果确实不需要,可考虑直接删除该导入。
可安全重命名的类别:
catch (e)→catch (_)(catch 绑定——始终使用纯_,绝不用_e或_err)let foo = ...→let _foo = ...(let 绑定,仅在确实未使用时)func bar(caller : Principal)→func bar(_caller : Principal)(函数参数中的简单名称,不是记录字段)- 函数参数解构中未使用的记录字段 → 整体移除该字段(由子类型处理)
import Foo "..."→import _Foo "..."(未使用的导入,或直接删除)
catch 块约定:始终使用纯通配符 catch (_),而不是 catch (_e) 或 catch (_err)。由于错误值是被有意丢弃的,_ 能清晰地表达这一点,且不会创建具名标识符。
批量修复方法:从构建输出中提取所有 M0194 警告,解析 file:line:identifier,通过 Python 脚本应用修复。示例:
import re
def fix_m0194(filepath, line_num, identifier):
"""Prefix an unused identifier with _ on a specific line."""
with open(filepath, 'r') as f:
lines = f.readlines()
line = lines[line_num - 1] # 1-indexed
# Careful regex: don't match after dots (method calls)
pattern = rf'(?<![._a-zA-Z0-9]){re.escape(identifier)}(?![_a-zA-Z0-9])'
new_line = re.sub(pattern, f'_{identifier}', line, count=1)
lines[line_num - 1] = new_line
with open(filepath, 'w') as f:
f.writelines(lines)
M0244 — 变量可改为 let
含义:var 绑定从未被重新赋值,因此可以改为 let(不可变)。
修复方式:将 var 改为 let:
// 修改前
var foo = computeSomething();
// 修改后
let foo = computeSomething();
也适用于 transient var → transient let 和 stable var → stable let(不过 stable let 有限制——仅当该值是常量或不需要可升级性的初始化器时才使用)。
批量修复方法:从构建输出解析警告(file:line:varname),然后对每一行用 let 替换 \bvar\b(count=1)。这是安全的,因为编译器已确认该变量从未被重新赋值。
可安全批量修复。除非该变量是有意为将来使用而保留可变,否则这些修改始终安全。
通用策略
关键规则:除非编译器针对该具体行和问题明确产生了警告或错误(Mxxxx),否则不要修改任何代码。绝不要对编译器并未实际报错的“疑似问题”应用“修复”。例如,除非编译器明确在该行报告警告或错误,否则不要将点号表示法改为模块调用。
重要:分阶段推进。一次只修复一种错误/警告类型,然后重新运行测试/基准测试,再进入下一种错误类型。
- 运行构建检查(见上文)并捕获输出。
- 按代码统计警告数量以确定优先级。
- 一次修复一种警告类型(例如先全部 M0194,再全部 M0244)。
- 每阶段修复后,始终重新构建/重新检查,以确认未引入新错误。
- 运行测试和基准测试,确保没有行为回归。
注意重命名后出现的类型错误(M0096)——它们表明破坏了类型签名。IC 接口函数中的记录字段重命名会导致级联类型错误。
环境注意事项
- 构建时间随仓库规模和 canister 数量而变化。
- 如果仓库存在与编译器警告无关的既有构建失败,应单独记录,避免将其视为警告修复导致的回归。
- 编译器警告可能随 moc 版本变化,因此在复现或排查结果时,如相关应记录当前使用的编译器版本。