mcpField
M
motoko-core-code-improvements

motoko-core-code-improvements

github.com/research-ag开发060 次安装10 周前更新
代码仓库

关于 motoko-core-code-improvements

面向 Motoko 项目的开发技能,在测试通过后进行不改变行为的可读性与一致性改进。它按步骤指导整理导入、采用点表示法及其他机械式修改,并要求分阶段构建和测试。

主要功能

  1. 01分组并排序导入
  2. 02谨慎清理未使用的导入
  3. 03采用受支持的点表示法
  4. 04简化单表达式函数的 return
  5. 05通过构建和测试验证修改

适用场景

  1. 01测试通过后整理 Motoko 代码
  2. 02统一项目文件中的导入格式
  3. 03审查安全且不改变行为的重构

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/research-ag/motoko-skills --skill motoko-core-code-improvements

安装后在 Claude Code 中输入/motoko-core-code-improvements确认已加载。

详细介绍

目的与范围

在测试通过后使用此技能,提升代码可读性和一致性,同时不改变行为。

此技能专注于保持语义不变的机械性改进:

  • 将导入聚合为几个区块:(1)mo:core/...;(2)来自 mops 或类似第三方来源的其他 mo:*/...;(3)本地项目模块;每个文件内的各区块按字母顺序排序
  • 在 mo:core 中可用时,优先使用点表示法
  • 清理确实未使用的 import 行,同时注意点表示法带来的隐式需求
  • 在单表达式函数中移除多余的 return
  • 适当将常量 ASCII 字符串直接赋值给 Blob

安全第一:

  • 每类改进单独执行;每完成一类就提交,以隔离差异
  • 优先使用脚本化且便于审查的更改;使用下方提供的审计检查
  • 每完成一类都重新构建;如有测试,也要运行

AI 快速检查清单(请勿跳过)

  1. 前置条件
  • 项目已基于 mo:core 编译通过(参见迁移技能)。仅当仍有引用时才保留 mo:base;否则应已移除 base 依赖。
  • 确保 Motoko 和 dfx 版本符合迁移技能要求(moc ≥ 1.3.0,dfx ≥ 0.31)。
  1. 改进顺序(建议)
  • A. 移除单表达式函数中的 return
  • B. 在可用处改用点表示法 — 参见 Motoko 点表示法迁移技能(skills/dot-notation-migration/SKILL.md)
  • C. 确保为点表示法导入必要的 mo:core 模块 — 参见 Motoko 点表示法迁移技能(导入映射)
  • D. 清理未使用的导入(对点表示法相关导入持谨慎态度)
  • E. 缩短本地(同级)导入路径(适用时省略 ./ 前缀)
  • F. 将导入聚合为三个区块,并在每个文件内按字母顺序排列:(1)mo:core/...;(2)来自 mops/第三方的其他 mo:*/...;(3)本地项目模块
  • G. 适当将常量 ASCII 字符串直接赋值给 Blob
  1. 每一步之后进行验证
  • 构建所有 canister 或软件包
  • 使用提供的命令进行 grep/审计
  • 保持差异精简且易读

验收标准

  • 不引入编译错误或警告。运行 find src -type f -name "*.mo" -print0 | xargs -0 -n1 $(mops toolchain bin moc) --check $(mops sources) 进行验证(尤其检查是否缺少导入)。
  • 不改变行为;除风格调整外,公共接口保持不变
  • 导入已聚合为三个区块——(1)mo:core/...;(2)来自 mops/第三方的其他 mo:*/...;(3)本地项目模块——且各区块均按字母顺序排列;没有确实未使用的 import
  • 在直接支持点表示法的地方,一致地使用点表示法
  • 常量 Text 字符串直接赋值给 Blob,不再冗余调用 Text.encodeUtf8

A)在单表达式函数中移除 return

模式

// Before
func f(x : T) : U { return <expr>; };

// After
func f(x : T) : U { <expr> };

说明

  • 仅当函数体由单条 return <expr>; 语句组成时应用。
  • 不要转换多语句函数体,也不要转换包含 try、label、switch 或会导致控制流不同的 await 的函数体。
  • 函数有多条 return 语句时(例如 switch 分支中的提前返回,如 return null),不得移除任何 return。
  • 单条非终结 return(提前/条件退出): 如果函数恰好有一条 return,但它不是函数体的最后一条语句(例如,if 或 switch 分支中的提前返回,后面还有一个顺延执行的最终表达式),则函数有两条不同的返回路径。不要移除提前返回语句。相反,也要为最终表达式添加显式的 return,这样函数中总共有两个 return 关键字——每条退出路径各一个。这能让两条控制流退出路径都明确且一致。
    // Before — one explicit return, one implicit fall-through return
    func lookup(k : Key) : ?V {
      if (cache.contains(k)) { return cache.get(k) };
      table.find(k)
    };
    
    // After — both exit paths use `return`
    func lookup(k : Key) : ?V {
      if (cache.contains(k)) { return cache.get(k) };
      return table.find(k);
    };
    
  • 函数末尾的 return switch (...) { ... } 没问题。 每个分支块都以要返回的表达式结尾(分支内没有 return 关键字)。当所有分支都正常产生值时,这是首选写法。
    // OK — all cases produce values, no traps or throws
    return switch (decode(data)) {
      case (#ok key) { (key.x, key.y) };
      case (#err msg) { #err(msg) };
    };
    
  • 例外:如果某个分支包含等同于返回的语句(Runtime.trap 或 throw),且该语句代表真正的函数退出,则将 return 放入分支块中。 移除 switch 前的 return,然后只在产生值的分支块中添加 return。包含 Runtime.trap 或 throw 的分支不需要 return。这样可以清楚地表明哪些分支会返回,哪些分支会中止:
    // Avoid — trap is hidden inside return switch
    return switch (decode(data)) {
      case (#ok key) { (key.x, key.y) };
      case (#err msg) { Runtime.trap(msg) };
    };
    
    // Prefer — return only in value-producing cases
    switch (decode(data)) {
      case (#ok key) { return (key.x, key.y) };
      case (#err msg) { Runtime.trap(msg) };
    };
    
  • 不可达的 trap 不等同于返回。 如果使用了 Runtime.trap("unreachable"),或者注释或消息表明该分支只有在出现 Bug 时才会执行,那么该 trap 就不是正常的函数退出,而是一种防御性断言。在这种情况下,使用 return switch 没问题,不应将 return 放入分支块中:
    // OK — the trap just guards an impossible case
    return switch (Jacobi.fromNat(x, y, 1, curve)) {
      case (null) Runtime.trap("unreachable");
      case (?point) point;
    };
    

自动化(示例)

  • 搜索候选项:grep -rn "func \\w\\+(.*) *:.*{ *return .*; *};" . --include="*.mo" | grep -v \.mops
  • 检查匹配项;然后使用编辑器或脚本替换。

脚本安全要求(根据实际迁移经验总结)

  • 简单的逐行 return 计数器并不足够。必须通过基于字符的括号深度解析来追踪函数边界。
  • 嵌套函数:当函数体内包含嵌套的 func 声明时,跳过嵌套函数的整个函数体——只统计直接位于外层函数作用域中的 return。
  • 准确检测函数边界:使用基于字符的扫描,处理字符串字面量(跳过包括 \" 转义在内的 "...")、注释(// 行注释和 /* ... */ 块注释),并追踪括号深度,以找到每个函数真正的闭合 }。
  • 计数规则:统计外层函数体中的所有 return,包括嵌套控制流块(如 switch、if、for 和 while)中的 return,但排除嵌套 func 函数体中的 return。只有当总数恰好为 1,且这条 return 是函数体直接作用域中的终结语句(在它之后到闭合 } 之前只能有空白、注释和可选的 ;)时,函数才可以安全地改写。如果唯一的 return 是提前/条件退出,则函数还存在一条通过顺延执行最终表达式的隐式返回路径——保留两者不动(脚本不会移除该 return;请按照上方的风格说明,手动为顺延表达式添加显式的 return)。

经过实战检验的 Python 脚本

将其保存为项目根目录下的 remove_returns.py,运行 python3 remove_returns.py,然后删除该脚本。

#!/usr/bin/env python3
"""
Remove terminal `return` from Motoko functions that have exactly one return
statement in their direct body (excluding nested functions).

Usage:
  1. Set src_dirs to match your project layout.
  2. Run: python3 remove_returns.py
  3. Run tests: npx mops test
  4. Delete this script after confirming all tests pass.

Safety:
  - Tracks function boundaries via character-level brace-depth parsing.
  - Skips string literals ("..." with \" escapes) and comments (// and /* */).
  - Detects nested `func` declarations and skips their bodies entirely.
  - Only removes the `return` keyword (+ trailing space) from functions with
    exactly 1 return at any depth within the direct body.
"""

import re
import glob
import os

# ── Configuration ──────────────────────────────────────────────────────
# Directories to process (relative to script location or cwd).
SRC_DIRS = ["src", "test", "bench"]
# ──────────────────────────────────────────────────────────────────────


def find_func_bodies(text):
    """
    Yield (body_start, body_end) for every top-level and nested function
    found in `text`. body_start is the index of the opening '{' of the
    function body; body_end is the index of the matching closing '}'.

    Handles:
      - String literals (skips content inside "...")
      - Line comments (// ...)
      - Block comments (/* ... */)
      - Nested braces
    """
    i = 0
    n = len(text)
    while i < n:
        # Skip string literals
        if text[i] == '"':
            i += 1
            while i < n and text[i] != '"':
                if text[i] == '\\':
                    i += 1  # skip escaped char
                i += 1
            i += 1  # skip closing "
            continue

        # Skip line comments
        if text[i] == '/' and i + 1 < n and text[i + 1] == '/':
            i += 2
            while i < n and text[i] != '\n':
                i += 1
            continue

        # Skip block comments
        if text[i] == '/' and i + 1 < n and text[i + 1] == '*':
            i += 2
            while i < n and not (text[i] == '*' and i + 1 < n and text[i + 1] == '/'):
                i += 1
            i += 2  # skip */
            continue

        # Look for 'func' keyword at a word boundary
        if text[i:i+4] == 'func' and (i == 0 or not text[i-1].isalnum() and text[i-1] != '_'):
            after = text[i+4:i+5] if i + 4 < n else ''
            if after == '' or not (after.isalnum() or after == '_'):
                # Found a func keyword. Scan forward to find the opening '{'.
                j = i + 4
                while j < n:
                    if text[j] == '"':
                        j += 1
                        while j < n and text[j] != '"':
                            if text[j] == '\\':
                                j += 1
                            j += 1
                        j += 1
                        continue
                    if text[j] == '{':
                        # Found the opening brace of the function body.
                        brace_start = j
                        depth = 1
                        j += 1
                        while j < n and depth > 0:
                            if text[j] == '"':
                                j += 1
                                while j < n and text[j] != '"':
                                    if text[j] == '\\':
                                        j += 1
                                    j += 1
                                j += 1
                                continue
                            if text[j] == '/' and j + 1 < n and text[j + 1] == '/':
                                j += 2
                                while j < n and text[j] != '\n':
                                    j += 1
                                continue
                            if text[j] == '/' and j + 1 < n and text[j + 1] == '*':
                                j += 2
                                while j < n and not (text[j] == '*' and j + 1 < n and text[j + 1] == '/'):
                                    j += 1
                                j += 2
                                continue
                            if text[j] == '{':
                                depth += 1
                            elif text[j] == '}':
                                depth -= 1
                            j += 1
                        brace_end = j - 1  # index of closing '}'
                        yield (brace_start, brace_end)
                        i = j
                        break
                    if text[j] == '=' or text[j] == ';':
                        # func ... = expr; (no body) or forward decl
                        i = j + 1
                        break
                    j += 1
                else:
                    i = j
                continue
        i += 1


def count_returns_in_direct_body(text, body_start, body_end):
    """
    Count `return` statements that are directly inside this function body
    (not inside nested functions). Returns list of (return_keyword_start,
    return_keyword_end) positions.
    """
    body = text[body_start + 1 : body_end]  # content between { and }
    offset = body_start + 1

    # First, find all nested func bodies within this body so we can skip them.
    nested_ranges = []
    for ns, ne in find_func_bodies(body):
        # Adjust to absolute positions
        nested_ranges.append((ns + offset, ne + offset))

    def is_inside_nested(pos):
        for ns, ne in nested_ranges:
            if ns <= pos <= ne:
                return True
        return False

    # Now scan for `return` keywords in the body, skipping nested funcs.
    returns = []
    i = 0
    while i < len(body):
        # Skip strings
        if body[i] == '"':
            i += 1
            while i < len(body) and body[i] != '"':
                if body[i] == '\\':
                    i += 1
                i += 1
            i += 1
            continue

        # Skip line comments
        if body[i] == '/' and i + 1 < len(body) and body[i + 1] == '/':
            i += 2
            while i < len(body) and body[i] != '\n':
                i += 1
            continue

        # Skip block comments
        if body[i] == '/' and i + 1 < len(body) and body[i + 1] == '*':
            i += 2
            while i < len(body) and not (body[i] == '*' and i + 1 < len(body) and body[i + 1] == '/'):
                i += 1
            i += 2
            continue

        # Check for 'return' keyword
        if body[i:i+6] == 'return' and (i == 0 or not body[i-1].isalnum() and body[i-1] != '_'):
            after = body[i+6:i+7] if i + 6 < len(body) else ''
            if after == '' or not (after.isalnum() or after == '_'):
                abs_pos = i + offset
                if not is_inside_nested(abs_pos):
                    returns.append((abs_pos, abs_pos + 6))
                i += 6
                continue
        i += 1

    return returns


def process_file(filepath):
    with open(filepath, 'r') as f:
        text = f.read()

    original = text
    removals = 0

    # Collect all function bodies
    func_bodies = list(find_func_bodies(text))

    # For each function, check if it has exactly 1 return in its direct body
    # Process in reverse order to preserve indices when editing
    edits = []  # list of (start, end) of "return " to remove

    for body_start, body_end in func_bodies:
        returns = count_returns_in_direct_body(text, body_start, body_end)
        if len(returns) == 1:
            ret_start, ret_end = returns[0]
            # Verify this return is the terminal statement of the direct body:
            # scan past the return's expression (tracking brackets/strings/comments)
            # to its terminating ';' (or body_end), then ensure only whitespace,
            # comments, and optional semicolons remain before body_end.
            n = len(text)
            j = ret_end
            depth = 0
            stmt_end = body_end  # position after the return statement
            while j < body_end:
                c = text[j]
                if c == '"':
                    j += 1
                    while j < body_end and text[j] != '"':
                        if text[j] == '\\':
                            j += 1
                        j += 1
                    j += 1
                    continue
                if c == '/' and j + 1 < body_end and text[j + 1] == '/':
                    j += 2
                    while j < body_end and text[j] != '\n':
                        j += 1
                    continue
                if c == '/' and j + 1 < body_end and text[j + 1] == '*':
                    j += 2
                    while j < body_end and not (text[j] == '*' and j + 1 < body_end and text[j + 1] == '/'):
                        j += 1
                    j += 2
                    continue
                if c in '({[':
                    depth += 1
                elif c in ')}]':
                    depth -= 1
                elif c == ';' and depth == 0:
                    stmt_end = j + 1
                    break
                elif c == '\n' and depth == 0:
                    # Statement terminated by newline (no semicolon)
                    stmt_end = j
                    break
                j += 1
            else:
                stmt_end = body_end

            # Now skip whitespace, comments, and stray semicolons after the return
            k = stmt_end
            terminal = True
            while k < body_end:
                c = text[k]
                if c.isspace() or c == ';':
                    k += 1
                    continue
                if c == '/' and k + 1 < body_end and text[k + 1] == '/':
                    k += 2
                    while k < body_end and text[k] != '\n':
                        k += 1
                    continue
                if c == '/' and k + 1 < body_end and text[k + 1] == '*':
                    k += 2
                    while k < body_end and not (text[k] == '*' and k + 1 < body_end and text[k + 1] == '/'):
                        k += 1
                    k += 2
                    continue
                # Found other code after the return — not terminal.
                terminal = False
                break

            if not terminal:
                continue

            # Remove "return " (keyword + trailing space)
            if ret_end < len(text) and text[ret_end] == ' ':
                edits.append((ret_start, ret_end + 1))
            else:
                edits.append((ret_start, ret_end))

    # Apply edits in reverse order to preserve positions
    edits.sort(key=lambda x: x[0], reverse=True)
    for start, end in edits:
        text = text[:start] + text[end:]
        removals += 1

    if text != original:
        with open(filepath, 'w') as f:
            f.write(text)

    return removals


def main():
    total = 0
    files_modified = 0
    for src_dir in SRC_DIRS:
        for filepath in sorted(glob.glob(os.path.join(src_dir, "**/*.mo"), recursive=True)):
            r = process_file(filepath)
            if r > 0:
                print(f"  {filepath}: {r} returns removed")
                total += r
                files_modified += 1
    print(f"\nTotal: {total} returns removed in {files_modified} files")


if __name__ == '__main__':
    main()

B) 点表示法转换

有关所有 Motoko 点表示法规则、自动化脚本和注意事项,请参阅专门的技能文档:

  • skills/dot-notation-migration/SKILL.md

本文件有意不重复这些说明。请使用专门的技能文档应用点表示法更改,然后继续执行此处的导入清理(D 节)和导入排序(F 节)。


C) 点表示法的导入要求

有关点表示法相关的导入映射和规则,请使用专门的技能文档:

  • skills/dot-notation-migration/SKILL.md

本文件有意不重复导入映射。按照该技能文档应用点表示法更改后,请继续执行 D 节(清理未使用的导入)和 F 节(导入排序)。


D) 安全清理未使用的导入

目标

  • 移除之前重构后确实未使用的导入,但不要移除点表示法隐式依赖的模块。

实际情况

  • 编辑器工具(VSCode Motoko 扩展)能正确标记未使用的导入,并会考虑点表示法。CLI 检测可能更棘手。

常见误报(看似未使用但实际必需的导入)

  • Blob — 当对 Blob 值调用 .toArray()、.size()、.isEmpty()、.hash() 时需要(例如,Sha256.fromArray(...).toArray()、hmac.sum().toArray())
  • Array — 当对 [T] 值调用 .flatten()、.foldLeft()、.sliceToArray()、.map()、.filter() 等方法时需要(例如,[arr1, arr2].flatten())
  • Nat — 当对来自 .size() 的 Nat 值调用 .toText() 时需要(例如,arr.size().toText())
  • VarArray — 当对 [var T] 值调用 .toArray() 时需要
  • 规则:如果对某个模块类型的值调用了任何点表示法方法,那么即使模块名称从未在代码中显式出现,也必须导入该模块。

处理方法

  1. 通过编辑器处理

    • 在 VSCode 中打开工作区。对每个 *.mo 文件,接受快速修复以移除标记为未使用的导入。检查差异。
  2. 借助编译器/LSP 批量处理

    • 通过 VSCode 扩展使用 Motoko 语言服务器显示所有诊断;在支持的情况下批量应用代码操作。
  3. 借助脚本进行保守移除

    • 编写一个简单脚本,以便:
      • 解析每个 import ... "mo:core/XYZ";
      • 搜索文件中是否存在 XYZ.,或是否存在映射到 XYZ 的已知点表示法模式(参见 Dot-Notation Migration Skill 的导入映射)
      • 如果两者都不存在,则标记该行可移除
    • 删除前手动检查标记的行

审查辅助方法

  • 清理后,搜索模块名称从未出现且没有映射点表示法模式的 import 行。
  • 构建项目。如果移除了必需模块,点表示法调用将在编译时失败——恢复导入并完善规则。

E) 缩短本地(同级)导入路径

  • 尽可能对本地导入使用裸模块名称:"Bech32",而不是 "./Bech32"。两者都能正确解析,但裸名称更简洁,也更符合惯用写法。

    // Good
    import Bech32 "Bech32";
    import Script "Script";
    import Types "Types";
    
    // Acceptable (cross-directory)
    import ByteUtils "../ByteUtils";
    import Curves "../ec/Curves";
    
    // Avoid (unnecessary ./ prefix for siblings)
    import Bech32 "./Bech32";
    
  • 对于跨目录导入,必须使用相对路径 ../,这是可以接受的。


F) 按分区汇总并按字母顺序排列导入项

原因

  • 统一的排序方式可减少合并冲突,加快审查速度。清晰的分组便于浏览,也能避免将外部模块与本地模块混在一起。

分区(按以下顺序排列,每个分区之间空一行)

  1. mo:core 导入项
    • 所有路径以 "mo:core/..." 开头的导入项(包括 mo:core/Types)。
  2. 其他 mo:* 第三方导入项(mops 或类似项)
    • 所有不是 mo:core/... 的 mo:... 导入项(例如,mo:uuid/UUID、mo:sha2/SHA256 等)。
  3. 本地项目模块
    • 裸模块名称导入项,如 "Bech32"、"Common"(优先),或相对路径导入项,如 "../ByteUtils"、"./Script"。
    • 同级导入项优先使用不带 ./ 前缀的裸模块名称(例如,使用 "Bech32" 而不是 "./Bech32")。两种方式都可用,但裸名称更简洁。
    • 按导入项所使用的本地名称排序本地项目模块,而不是按导入路径中的模块名称排序。

排序规则(分别应用于每个分区)

  • 按模块导入时使用的本地名称按字母顺序排序
  • 保留导入样式(模块导入与命名类型导入)。
  • 同一路径的多个命名类型导入项保持在同一行,不作更改。
  • 如果代码库风格偏好,可在每个分区上方添加注释标题(Core、Third-party、Local)。

示例

// Before (mixed)
import Runtime "mo:core/Runtime";
import { type Result } "mo:core/Types";
import SHA256 "mo:sha2/SHA256";
import Map "mo:core/Map";
import BitVec "mo:bitvec/BitVec";
import Utils "../lib/Utils";
import Logger "./Logger";

// After (aggregated and sorted per section)
//// Core
import Map "mo:core/Map";
import Runtime "mo:core/Runtime";
import { type Result } "mo:core/Types";

//// Third‑party (mops)
import BitVec "mo:bitvec/BitVec";
import SHA256 "mo:sha2/SHA256";

//// Local
import Logger "Logger";
import Utils "../lib/Utils";

轻量级自动化思路(逐文件)

  • 收集文件顶部的所有导入行。
  • 按路径前缀将其划分为三个分区。
  • 按导入项所使用的本地名称,对每个分区按字母顺序排序
  • 按 Core → Third-party → Local 的顺序重新输出各分区,并在分区之间空一行。
  • 保留所有非导入注释的原有位置,除非它们明显属于某个分区标题。

G) 常量直接将字符串赋值给 Blob

模式

// Before
let b : Blob = Text.encodeUtf8("hello");

// After
let b : Blob = "hello";

原因

  • 对于常量 Text 字符串,Motoko 编译器允许直接赋值给 Blob 类型。
  • 结果与 Text.encodeUtf8 相同,但代码更简洁,也避免了显式调用函数。

示例

// Good: direct assignment
let blobs = [
  "strategy",
  Text.encodeUtf8(Nat.toText(slot)),
];

// Avoid: redundant encoding for constant string
let blobs = [
  Text.encodeUtf8("strategy"),
  Text.encodeUtf8(Nat.toText(slot)),
];

实用自动化方案(可选)

这些是可选的起点。有可用的编辑器集成重构功能时,优先使用。务必检查差异。

  1. 查找单行返回函数
rg -n --glob '!**/.mops/**' --glob '**/*.mo' "func [A-Za-z_][A-Za-z0-9_]*\(.*\) *:.*\{ *return .*; *};"
  1. 点表示法转换与候选项检测
  • 完整的自动化与 grep 方案请参阅专用技能:
    • skills/dot-notation-migration/SKILL.md
  1. 标记可能未使用的核心导入项(保守处理)
# Rough heuristic: list imports, then search for name or dot‑patterns
rg -n --glob '!**/.mops/**' --glob '**/*.mo' '^import .*"mo:core/([A-Za-z/]+)";' -o -r '$1'
# For each file, ensure presence of module references OR mapped dot‑patterns before removal
  1. 将导入项汇总并按分区排序(编辑器宏)
  • 选中文件顶部的所有 import 行 → 分成三个分区(Core、Third-party mo:*、Local)→ 按路径对每组按字母顺序排序 → 在分区之间插入空行 → 保持导入样式不变。

Agent 策略(面向 AI 助手)

  1. 开始改进前,确认项目可基于 mo:core 构建。
  2. 逐个处理文件。对于每个文件:
    • A. 移除单表达式 return 形式
    • B. 按 skills/dot-notation-migration/SKILL.md 应用点表示法
    • C. 为引入的点表示法确保具备所需导入项(参见 skills/dot-notation-migration/SKILL.md 中的导入映射)
    • D. 移除确实未使用的导入项(遵循专用技能中的点表示法导入映射)
    • E. 缩短本地(同级)导入路径(适用时移除 ./ 前缀)
    • F. 将导入项汇总到三个分区,并按字母顺序排列各分区(Core → Third-party mo:* → Local)
    • G. 当目标类型为 Blob 时,将 Text.encodeUtf8("<literal>") 替换为 "<literal>"
  3. 每处理完一个文件后:编译;如果因缺少导入项而失败,则恢复并标记映射
  4. 每完成代码库中的一个类别后:运行完整构建(对于 MOPS 软件包,使用 find src -type f -name "*.mo" -print0 | xargs -0 -n1 $(mops toolchain bin moc) --check $(mops sources)),并可选择运行测试
  5. 简要报告所做的更改,以及为人工审查而暂缓处理的边界情况

H) 将 Array.fromVarArray(x) 转换为 x.toArray()

模式

// Before
Array.fromVarArray(buf)
Array.fromVarArray<Nat8>(buf)

// After
buf.toArray()

说明

  • Array.fromVarArray 是一个工厂函数(第一个参数不是 self),因此点表示法迁移脚本不会自动转换它。这需要单独转换。
  • VarArray.toArray() 是对应的点表示法形式,定义在 [var T] 值上。
  • 转换后,检查是否可以移除 Array 导入(它可能仍用于 Array.tabulate、[T] 上的 Array.flatten 点表示法等)。
  • 移除可选的类型参数:Array.fromVarArray<Nat8>(buf) → buf.toArray()(类型会根据变长数组推断)。

I) Array.tabulate 通常需要类型注解

  • 不要移除 Array.tabulate<T>(...) 调用中的类型注解。
  • Motoko 编译器通常无法推断元素类型,尤其是在回调使用 fromNat、算术运算或其他可能返回多种数值类型的表达式时。
  • 在实际项目中,移除注解导致 24 个测试文件中有 13 个失败,错误类似于 expression of type [Any] cannot produce expected type [Nat8]。
  • 保留注解:Array.tabulate<Nat8>(n, func i { ... })。

边缘情况与注意事项

  • 有关所有点表示法的行为、方法可用性、工厂函数与方法的区别,以及可变性说明,请参阅:
    • skills/dot-notation-migration/SKILL.md
  • 汇总导入时,将来自 mo:core/Types 的具名类型导入保留在 mo:core 分组中;排序规则请参阅 F 节。
  • 本地导入:同级文件优先使用裸模块名("Bech32"),而非相对路径("./Bech32")。两者都能正确解析,但裸模块名更简洁。
  • 在 src/ 中使用 "../src/Bech32" 这样的导入路径是不正确的——同级文件应使用 "Bech32",跨目录引用应使用 "../SubDir/Module"。

验证与签核

  • 更改后成功构建所有 canister。
  • 运行静态审计:
    • 确认没有 import 行被编辑器或启发式脚本标记为未使用(考虑点表示法的需求后再判断)。
    • 抽查数组、映射、集合和文本操作是否在适合的地方使用点表示法。
  • 差异应保持机械式;不得更改公共 API 或行为。

附录:点表示法参考

完整且持续维护的点表示法目录、自动化脚本和导入映射,请参阅:

  • skills/dot-notation-migration/SKILL.md

评论

支持 Markdown