mcpField
M
motoko-doc-strings

motoko-doc-strings

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

关于 motoko-doc-strings

为 Motoko 包的开发者提供公共 API 文档注释规范与工作流程,帮助完善 mo-doc 生成的文档。指南强调按代码位置确定注释详略,并检查参数语义、错误条件和示例。

主要功能

  1. 01为公共声明添加三斜线注释
  2. 02区分内部模块与公共 API 的详略
  3. 03说明模块级与声明级注释格式
  4. 04使用脚本盘点并检查注释
  5. 05指导生成及审阅 mo-doc 文档

适用场景

  1. 01发布前补齐 Motoko 包的 API 文档
  2. 02为新增公共符号撰写文档注释
  3. 03检查文档中的参数、错误和边界说明

安装

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

$npx skills add https://github.com/research-ag/motoko-skills --skill motoko-doc-strings

安装后在 Claude Code 中输入/motoko-doc-strings确认已加载。

详细介绍

Motoko 文档字符串

用途

为 Motoko 源文件中的每个公共声明添加三斜杠(///)文档注释,让 mo-doc 生成有意义的文档页面。本指南总结了 mo:core(caffeinelabs/motoko-core)采用的约定,以及在 motoko-bitcoin 库中应用这些约定时积累的经验。

何时使用

  • 用户要求为 Motoko 包“添加文档字符串”“编写公共 API 文档”或“生成文档”。
  • 在 MOPS 上发布前,审查或完善库。
  • 添加新的公共符号后,发现生成的 docs/ 中缺少条目。

什么算作“公共对象”

为所有会显示在生成文档中的声明编写文档。在典型的 module { ... } 文件中,这些声明包括:

  • public type ...
  • public let ...
  • public func ...
  • public class ...
  • 模块本身(module { ... } 或 module Name { ... })
  • 对于 public class,类体内的每个 public let、public var 和 public func 成员。
  • 对于嵌套的 public module,递归应用相同规则。

私有声明(不带 public 的 func、let、class)和辅助类型不应添加 /// 注释——它们不会出现在 mo-doc 输出中,增加这些注释会影响可读性。

不同位置的详尽程度

文档字符串需要多详细,取决于文件所在的位置:

  • 位于 src/internal/ 下(某些包使用 src/private/ 表示相同用途,也按相同规则处理)——文档字符串可以简短。这些模块属于实现细节,最终用户不需要直接调用,因此通常用一句话说明声明的作用即可。除非行为会令其他维护者感到意外,否则可以省略陷阱或错误说明。
  • 位于 src/ 下的其他位置(即 src/internal/ 之外)——文档字符串必须详尽。这些是用户会调用的公共 API,因此需要涵盖所有细节:参数的单位和格式、大小或范围限制、返回值语义、完整的失败行为(Traps / Errors 段落),以及有帮助时提供可运行的示例。对这里的每个声明,都要应用本指南末尾的完整“从用户视角通读”检查清单。

文档字符串格式

mo-doc 会将以 /// 开头的行解析为 Markdown 文档,并将其附加到下一个声明。

/// Brief one-line description ending in a period.
///
/// Optional longer paragraph(s) describing semantics, edge cases,
/// and return values. Refer to parameters by `name` in backticks.
///
/// Example:
/// ```motoko include=import
/// let result = Module.func(arg);
/// ```
public func func(arg : T) : U { ... };

mo:core 中采用的约定:

  • 第一行是简短的祈使句式摘要(“返回……”“计算……”“解码……”),以句号结尾。
  • 段落之间用一个空白的 /// 行分隔。
  • 参数名、类型和简短代码用单反引号括起。
  • 代码块使用 ```motoko 围栏。使用 ```motoko include=import annotation when the snippet relies on the module's import header (defined elsewhere with ```motoko name=import).
  • For module-level docs, define a named import block once at the top so all example snippets can reference it:
    /// ```motoko name=import
    /// import Hash "mo:bitcoin/Hash";
    /// ```
    module { ... }
    
  • The name=import / include=import annotations are NOT consumed by mo-doc itself — they are directives for an external doctest runner (used by mo:core's CI and by Docusaurus-based doc sites) that prepends the named snippet before compiling each example. mo-doc passes them through verbatim into the rendered code-fence info string. If the project does not run doctests, you can omit them and just use plain ```motoko fences.
Where to put the module-level doc string

The module-level /// block must sit at the very top of the file, before the import statements, with one blank line separating it from the imports and one blank line between the imports and the module { ... } line. This matches the layout used in dfinity/motoko-core and is the only placement that mo-doc actually attaches to the module page — a doc block placed between the imports and module { ... } is silently ignored and the rendered module page will have no description.

All .mo files MUST have a module-level /// doc string at the very top of the file (before the import statements). This applies to every Motoko source file, not just modules with public APIs — having a doc block at the top ensures mo-doc renders a meaningful module page and helps maintainers quickly understand the file's purpose.

Special case for Mixins: This rule is especially important for files using the mixin keyword. Even though mo-doc currently skips files with the mixin keyword due to syntax differences, the doc string MUST still be placed at the very top of the file to ensure compatibility with future tool updates and maintainer readability.

/// One-line module summary.
///
/// Longer description.
///
/// ```motoko name=import
/// import Foo "mo:pkg/Foo";
/// ```

import Bar "mo:core/Bar";
import Baz "mo:core/Baz";

module {
  // ...
}

For named modules (module Name { ... }, e.g. src/bitcoin/Script.mo) the same rule applies: doc block at the top of the file, then a blank line, then imports, then a blank line, then module Name { ... }.

Where to put per-declaration comments

Place /// lines immediately above the declaration with no blank line between them. If there is an existing legacy // ... comment, put the /// block above the legacy comment (the legacy comment can stay as implementation notes).

/// Public API description goes here.
// Legacy implementation notes can stay below the doc string.
public func encode(input : [Nat8]) : Text { ... };

Common Pitfalls

1. apply_patch and literal \n

When inserting multi-line content, write actual newlines in the patch body (one + per real line). NEVER use the escape sequence \n inside inserted text — it will be written as the literal two characters and break the file.

2. Don't accidentally insert into the middle of a function

apply_patch matches on context. When the surrounding context is too short or appears multiple times, the inserted block may land inside a function body. After inserting docs, run a quick syntax check (e.g. mo-doc or moc --check) to catch this. A telltale sign is a syntax error [M0001], unexpected token 'import' message — that means a new module-level block was placed inside a loop/func body.

3. Class member documentation order

mo-doc renders class fields and methods in source order, but the class's own description block is shown after the member list when the class doc appears above the constructor. To keep the class summary at the top of the rendered class page, place the /// block on the line directly preceding public class Name(...).

4. Re-exported types

Re-exported types like public type Signature = Types.Signature; still need a one-line /// description so they are not rendered as "(no description)".

5. Skip private helpers and constants

Don't add /// to let, func, or type declarations that lack public. They never appear in the output and the comments add visual noise.

6. module Name { ... } named modules

Files like src/bitcoin/Script.mo use the form module Script { ... } instead of the bare module { ... }. The same top-of-file placement rule applies — the module-level /// block goes at the very start of the file (before the imports), not on the line directly preceding module Script {.

7. Preserve unfamiliar parameter syntax verbatim

If a function signature uses syntax you don't recognise — e.g. self : [var T] (Motoko's method-dispatch hook that enables x.func(...) call sites) or key : (implicit : T -> Nat32) (the implicit parameter feature) — do NOT rewrite it. These shapes are load-bearing: they affect how call sites in examples work (users.bucketSort<User>(...) only compiles because the first parameter is named self), and a doc pass that "cleans them up" can silently invalidate every example you just wrote.

8. Module doc must be at the top of the file

mo-doc only treats a /// block as the module description when it appears at the very beginning of the file, ahead of the import statements. A doc block placed between the imports and module { ... } compiles fine but produces an empty module description in the rendered HTML. If you find an existing project with module docs adjacent to module { ... }, relocate them to the top of the file (a small Python script that finds the trailing /// block before module and prepends it to the file works well for batch migration).

8. Documenting Record Types

For record types with multiple members, documenting each member with a per-line /// can make the code and the rendered documentation very verbose. Instead, prefer documenting all members in a single block above the type declaration. Use backticks for member names.

/// An HTTP request.
/// `method`: HTTP method (e.g., "GET", "POST").
/// `url`: Request URL.
/// `headers`: List of headers.
/// `body`: Request body.
public type Request = {
  method : Text;
  url : Text;
  headers : [(Text, Text)];
  body : Blob;
};

Workflow

  1. Inventory public declarations:

    grep -RInE "public (type|func|class|let)" src
    
  2. For each .mo file:

    • Add a module-level /// block (with a name=import example) at the beginning of the file, even before the block of import statements.
    • Add /// blocks above every public declaration inside the top-level modules.
    • Recurse into nested public declarations. For instance public members of public classes need doc strings. Public members of public modules need doc strings. And so on.
  3. Re-scan to catch anything missed:

    awk '
      FNR==1{prev=""}
      {
        if ($0 ~ /^[ \t]*public[ \t]+(type|func|class|let)([ \t(]|$)/) {
          p=prev; gsub(/^[ \t]+|[ \t]+$/, "", p);
          if (p !~ /^\/{3}/) printf "%s:%d:%s\n", FILENAME, FNR, $0;
        }
        if ($0 !~ /^[ \t]*$/) prev=$0;
      }
    ' src/*.mo src/**/*.mo
    

    Empty output = all public declarations are preceded by a /// line.

    Portability: use [ \t]+ (and explicit [ \t(] boundaries) instead of [[:space:]]+ / \b. BSD/macOS awk silently fails to match POSIX classes here — the script returns empty output and looks like a pass. The cost of this miss is a full iteration to diagnose.

    Known false positives (don't waste edits "fixing" these — mo-doc itself handles both cases correctly):

    • A legacy // ... comment between the /// block and the public ... line. The skill's format guidance explicitly allows this layout, but the audit awk only looks at the immediately previous non-blank line. Verify by reading the rendered HTML before assuming a real miss.
    • A public func / public let inside a /* ... */ block comment. The awk has no notion of block comments, so commented-out code shows up as missing. Cross-check by grep-ing the file for /* near the flagged line.
  4. Generate docs:

    # mo-doc will NOT create nested output subdirectories on its own.
    # If src/ has subdirectories, mkdir -p the mirrored paths under
    # docs/ first or the run aborts on the first file it can't write.
    mkdir -p docs $(find src -mindepth 1 -type d | sed 's|^src|docs|')
    mo-doc --source src --output docs --format html
    

    No output = success. Two failure modes to watch for:

    • A Fatal error: exception Sys_error("docs/.../X.html: No such file or directory") means an output subdirectory is missing.
    • Any Skipping ... line indicates a syntax error that must be fixed (often a stray apply_patch corruption).
  5. Spot-check the rendered output:

    • docs/index.html — every module should appear in the listing.
    • Each docs/<Module>.html — module description, types, functions, and class members should all show their text.

Tips for Writing Useful Descriptions

  • Describe what the function does and what it returns, not how it is implemented.
  • For low-level helpers (readBE32, writeLE64, etc.) a one-liner stating the byte order, width, and offset semantics is sufficient.
  • For domain types (SighashType, WitnessProgram, OutPoint) name the spec or BIP that defines the format and link to it.
  • Keep examples short and self-contained; prefer literal byte arrays over reading from external sources.
Files with many similar public lets (constants tables)

Each public let needs its own /// line — there is no syntax for a shared doc block. For files like FIPS round-constant tables (e.g. SHA256's K00..K63, SHA512's K00..K79), doing N individual Edit calls is slow and noisy. Read the file once and rewrite it whole with Write, emitting one /// <name> round constant K<NN>. line per entry. A module-level /// at the top can carry the shared context (spec section, purpose) so the per-entry lines stay terse.

Documenting Error and Trap Behavior (REQUIRED)

Every public function doc string MUST describe its full failure behavior. Readers cannot tell from a type signature alone whether a function traps, returns null, returns #err, or simply produces a wrong-but-defined result on bad input — the doc string is the only place this contract is recorded.

What to look for in the implementation

Scan the function body (and every helper it calls) for:

  • Runtime.trap(...) / Debug.trap(...) / Prim.trap(...) calls.
  • assert ...; statements (a failed assert traps).
  • Pattern matches that are non-exhaustive in practice (e.g. a switch on ?T whose null branch traps, or a case (#err _) Runtime.trap ...).
  • Implicit traps from the standard library: out-of-bounds array indexing (a[i] when i >= a.size()), Nat subtraction underflow, division by zero, Option.unwrap on null.
  • Explicit error returns: ?T returning null, Result<T, E> returning #err, variant returns like { #ok; #err }.
What to write

For each failure mode, state in the doc:

  1. The condition — what input or state triggers it, in user-facing terms ("when input contains a character outside the Base58 alphabet", not "when mapBase58[c] == 255").
  2. The outcome — traps, returns null, returns #err(...), etc.
  3. For Result/option returns, list every distinct error case separately when the variants carry meaning.

Use a dedicated Traps and/or Errors paragraph (or both) at the end of the doc, after the example and before the runtime/space notes.

CRITICAL: Use the wording "Traps" (capitalised, present tense) for unrecoverable failures (e.g., assert, trap). This makes the failure modes highly visible in rendered docs and easily greppable.

/// Decodes a Base58-encoded string into the original byte array.
///
/// ```motoko include=import
/// let bytes = Base58.decode("StV1DL6CwTryKyV");
/// ```
///
/// Traps if `encoded` contains any character that is not in the Base58
/// alphabet (i.e. not in
/// `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`).
public func decode(encoded : Text) : [Nat8] { ... };

For graceful errors:

/// Parses a WIF-encoded private key.
///
/// Returns `#err(msg)` when:
/// - the input is not valid Base58Check (bad checksum or alphabet),
/// - the decoded payload has an unexpected length (must be 33 or 34 bytes),
/// - the version byte does not match `network`.
public func decode(wif : Text, network : Network) : Result<PrivateKey, Text> { ... };
Pure / total functions

If a function genuinely cannot fail (e.g. Array.size, a pure arithmetic helper that uses fixed-width types and total operators), say so explicitly with a one-liner like "Never traps." or omit the failure section entirely — but only after auditing the body to confirm.

Worked examples from motoko-bitcoin
  • Base58.decode — traps on any character outside the alphabet (not a Result, no graceful fallback).
  • Base58Check.decode — returns ?[Nat8]; null on bad alphabet, bad length, or checksum mismatch. Document each.
  • Bech32.decode — returns Result<_, Text> with distinct messages for invalid characters, mixed case, bad checksum, length out of range, invalid HRP. Mention each error category, not just "returns #err".
  • Bip32.derivePath — traps on hardened derivation from a public key.
  • Fp.inverse (and the / operator) — traps when the value is zero.
  • Affine.add / Jacobi.add — document behaviour at the point at infinity and for equal/opposite inputs.
  • Transaction serialization (Transaction.toBytes, TxInput.toBytes, etc.) — note any size limits that would cause Nat32/Nat64 conversion traps.
Audit workflow for an existing file
  1. List every public func and public class member.
  2. For each, read the body and follow the call graph one level into private helpers.
  3. Note every trap, assert, Result/Option return, and any implicit trap source (subtraction, indexing, division).
  4. Update the doc string to enumerate the conditions.
  5. Re-run mo-doc and visually scan the rendered HTML for sections that still lack a "Traps" / "Errors" paragraph on a non-trivial function.

Final Step: User-Perspective Read-Through (REQUIRED)

After every public declaration has a doc string, do one more pass. Read each doc string from the perspective of a first-time user of the API who has not seen the implementation. For every doc, ask:

  • What is the unit / format of each argument and the return value? (bytes vs. bits, big- vs. little-endian, satoshis vs. BTC, raw vs. DER-encoded, compressed vs. uncompressed, 0-based vs. 1-based, …)
  • For Motoko array parameters, is [T] (immutable) vs. [var T] (mutable) clear? Examples that mix Array.tabulate (returns [T]) with VarArray.repeat (returns [var T]) routinely confuse newcomers — spell out which shape the function expects.
  • What are the size or range constraints on each input?
  • Which BIP / RFC / spec defines the format, and is it linked?
  • For mutating methods, what state changes? Is the receiver still usable afterward?
  • For functions that take a callback or proxy (e.g. an ECDSA signer), what is the expected input/output shape of the callback?
  • For variant returns, what does each variant mean semantically (not just what tag it carries)?
  • Are domain-specific terms ("witness program", "tap leaf", "sighash", "scriptPubKey") used without a one-line explanation or link?
  • For constants (EMPTY_WITNESS, dustThreshold), what is the value and why does it have the value it has?
  • For re-exported types, where is the actual definition (and is the link there)?
  • If you removed the function name, would the description still be unambiguous? If two near-identical functions exist (e.g. toBytes vs. toBytesIgnoringWitness, encode vs. encodeUncompressed), is the difference between them spelled out?

If any question remains unanswered, extend the doc string to address it. Prefer one extra sentence in the doc over forcing the user to read the source. Do this pass file by file; it usually surfaces 2–5 missing facts per non-trivial module.

评论

支持 Markdown