motoko-core-code-improvements について
テスト合格後の Motoko プロジェクトを対象に、動作を変えず可読性と一貫性を整える開発スキルです。インポート整理やドット記法などの機械的な変更を順序立てて案内し、分類ごとにビルドとテストを求めます。
主な機能
- 01インポートの分類と並べ替え
- 02未使用インポートの慎重な削除
- 03対応するドット記法の適用
- 04単一式関数の return の簡素化
- 05ビルドとテストによる変更確認
活用シーン
- 01テスト合格後の Motoko コード整理
- 02プロジェクト内のインポート形式統一
- 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 向けクイックチェックリスト(省略禁止)
- 前提条件
- プロジェクトが
mo:coreでコンパイルできること(Migration Skill を参照)。引き続き参照されている場合に限りmo:baseを残します。それ以外の場合は、base への依存関係をすでに削除しておきます。 - Migration Skill に従い、Motoko と dfx のバージョンを統一します(moc ≥ 1.3.0、dfx ≥ 0.31)。
- 改善の順序(推奨)
- A. 単一式の関数から
returnを削除します - B. 使用できる箇所をドット記法に変更します — Motoko Dot-Notation Migration Skill(
skills/dot-notation-migration/SKILL.md)を参照 - C. ドット記法に必要な
mo:coreのインポートを確保します — Motoko Dot-Notation Migration Skill のインポート対応表を参照 - D. 未使用のインポートを整理します(ドット記法との関係に注意し、慎重に行ってください)
- E. ローカル(兄弟)インポートのパスを短縮します(該当する場合は
./の接頭辞を削除します) - F. インポートを3つのセクションにまとめ、各ファイル内で各セクションをアルファベット順に並べます。(1)
mo:core/...、(2)mops/サードパーティーのその他のmo:*/...、(3)ローカルのプロジェクトモジュール - G. 適切な場合は、定数の ASCII 文字列を直接
Blobに代入します
- 各手順の後に確認
- すべての canister またはパッケージをビルドします
- 指定されたコマンドで grep/監査を行います
- 差分を最小限に抑え、読みやすく保ちます
受け入れ基準
- 変更によるコンパイラーエラーや警告が発生していないこと。確認のため、
find src -type f -name "*.mo" -print0 | xargs -0 -n1 $(mops toolchain bin moc) --check $(mops sources)を実行します(特にインポート漏れを確認してください)。 - 動作が変わっていないこと。スタイル上の変更を除き、公開インターフェースが変更されていないこと
- インポートが3つのセクション(1)
mo:core/...、(2)mops/サードパーティーのその他のmo:*/...、(3)ローカルのプロジェクトモジュール)にまとめられ、各セクションがアルファベット順に並んでいること。本当に未使用のimportが残っていないこと - 直接サポートされる箇所で、一貫してドット記法が使用されていること
- 定数の Text 文字列が、冗長な
Text.encodeUtf8呼び出しなしで直接Blobに代入されていること
A) 単一式関数から return を削除する
パターン
// Before
func f(x : T) : U { return <expr>; };
// After
func f(x : T) : U { <expr> };
注意事項
- 関数本体が単一の
return <expr>;文で構成されている場合にのみ適用してください。 - 複数の文からなる本体や、異なる制御フローにつながる
try、label、switch、awaitを含む本体は変換しないでください。 - 複数の
return文がある関数(例:return nullのようなswitchケース内の早期リターン)では、どのreturnも削除してはいけません。 - 終端ではない単一の
return(早期/条件付き終了): 関数にreturnがちょうど1つだけあり、それが本体の最後の文ではない場合(例:ifまたはswitchケース内の早期リターンの後に、フォールスルーする最終式が続く場合)、関数には2つの異なる戻り経路があります。早期のreturnは削除しないでください。代わりに、最終式にも明示的なreturnを追加し、関数内のreturnキーワードを合計2つにしてください。これにより、両方の制御フロー終了が明示され、一貫した形になります。// 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) }; }; - 到達不能なトラップは戻り値相当ではありません。
Runtime.trap("unreachable")が使用されている場合、またはコメントやメッセージで、その分岐にはバグがある場合にしか到達しないことが示されている場合、そのトラップは通常の関数終了ではなく、防御的なアサーションです。この場合は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 で検索します:
grep -rn "func \\w\\+(.*) *:.*{ *return .*; *};" . --include="*.mo" | grep -v \.mops - 一致した箇所を確認してから、エディターまたはスクリプトによる置換で適用します。
スクリプトの安全要件(実際の移行から得た知見)
- 行単位の単純な
returnカウンターでは不十分です。文字単位で波括弧の深さを解析し、関数の境界を追跡する必要があります。 - ネストされた関数: 関数本体にネストされた
func宣言がある場合は、そのネストされた関数の本体全体をスキップし、外側の関数スコープ直下にあるreturnのみを数えてください。 - 正確な関数境界の検出: 文字列リテラル(
\"エスケープを含む"..."をスキップ)、コメント(//の行コメントおよび/* ... */のブロックコメント)を処理し、波括弧の深さを追跡して、各関数の正しい閉じ}を見つける文字単位のスキャンを使用してください。 - カウント規則: ネストされた
func本体内のreturnを除き、switch、if、for、whileなどのネストされた制御フローブロック内も含め、外側の関数本体内にあるすべてのreturnを数えてください。書き換えが安全なのは、合計がちょうど1つで、そのreturnが本体直下の終端文である場合のみです(そのreturnの後から閉じ}までに、空白、コメント、任意の;以外があってはいけません)。単一のreturnが早期/条件付き終了の場合、フォールスルーする最終式による暗黙の戻り経路がもう1つあります。その場合はどちらも変更しないでください(スクリプトは削除しません。上記のスタイル注記に従って、フォールスルー式に明示的な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 つでも呼び出している場合は、コード内にモジュール名が明示的に記載されていなくても、そのインポートが必要です。
方法
1)エディターを使った方法
- VSCode でワークスペースを開きます。各 *.mo ファイルで、未使用とマークされたインポートを削除するクイックフィックスを適用します。差分を確認してください。
2)コンパイラー/LSP を使った一括処理 - VSCode 拡張機能から Motoko 言語サーバーを使用して、すべての診断を表示します。サポートされている場合は、コードアクションを一括適用します。
3)スクリプトを使った保守的な削除
- 次の処理を行う簡単なスクリプトを作成します。
- 各 import ... "mo:core/XYZ"; を解析する
- ファイル内で XYZ. または XYZ に対応する既知のドット記法パターン(ドット記法移行スキルのインポートマッピングを参照)を検索する
- どちらも見つからない場合、その行を削除候補としてフラグ付けする
- フラグ付けされた行を手動で確認してから削除する
監査用の確認
- 整理後、モジュール名が出現せず、対応するドット記法パターンもない
"import"行を検索します。 - プロジェクトをビルドします。必要なモジュールを削除した場合、ドット記法の呼び出しがコンパイル時に失敗するため、インポートを復元してルールを調整してください。
E)ローカル(同階層)インポートパスを短くする
-
可能であれば、ローカルインポートにはモジュール名のみを使用します。たとえば、
"./Bech32"ではなく"Bech32"を使用します。どちらも正しく解決されますが、モジュール名のみのほうが簡潔で慣例に沿っています。// 良い例 import Bech32 "Bech32"; import Script "Script"; import Types "Types"; // 許容される例(ディレクトリをまたぐ場合) import ByteUtils "../ByteUtils"; import Curves "../ec/Curves"; // 避ける例(同階層への不要な ./ 接頭辞) import Bech32 "./Bech32"; -
ディレクトリをまたぐインポートでは、
../を使った相対パスが必要であり、問題ありません。
F) インポートをセクションごとにまとめ、アルファベット順に並べる
理由
- 順序を統一すると、マージの競合が減り、レビューが速くなります。明確にグループ分けすることで見通しがよくなり、外部モジュールとローカルモジュールが混在するのを防げます。
セクション(次の順序で並べ、それぞれを空行1行で区切ります)
- mo:core のインポート
- パスが
"mo:core/..."で始まるすべてのインポート(mo:core/Typesを含む)。
- パスが
- その他の mo:* サードパーティーインポート(mops など)
"mo:core/..."以外のmo:...インポート(例:mo:uuid/UUID、mo:sha2/SHA256など)。
- ローカルプロジェクトモジュール
"Bech32"、"Common"などのモジュール名をそのまま指定するインポート(推奨)、または"../ByteUtils"、"./Script"などの相対パスによるインポート。- 同階層のインポートでは、
"./"プレフィックスを付けずにモジュール名をそのまま使うことを推奨します(例:"./Bech32"ではなく"Bech32")。どちらも機能しますが、モジュール名をそのまま使うほうがすっきりします。 - ローカルプロジェクトモジュールのインポートは、インポート元のパスにあるモジュール名ではなく、インポート時に付けられるローカル名で並べ替えます。
並べ替えルール(各セクション内で個別に適用)
- インポート時に付けられるローカル名のアルファベット順に並べます
- インポート形式(モジュールインポートまたは名前付き型インポート)を維持します。
- 同じパスから複数の名前付き型をインポートする場合は、現在のように1行にまとめます。
- リポジトリのスタイルに合わせて、各セクションの上にコメント見出し(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";
軽量な自動化のアイデア(ファイルごと)
- ファイル先頭にあるすべてのインポート行を集めます。
- パスのプレフィックスに従って、3つのセクションに分けます。
- 各セクション内で、インポート時に付けられるローカル名のアルファベット順に並べ替えます。
- 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行の return 関数を検索する
rg -n --glob '!**/.mops/**' --glob '**/*.mo' "func [A-Za-z_][A-Za-z0-9_]*\(.*\) *:.*\{ *return .*; *};"
- ドット記法への変換と候補の検出
- 自動化と grep のレシピについては、専用スキルを参照してください:
- skills/dot-notation-migration/SKILL.md
- 未使用の可能性がある core インポートにフラグを付ける(慎重に)
# 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
- インポートをセクションにまとめて並べ替える(エディターマクロ)
- ファイル先頭のすべての
import行を選択 → 3つのセクション(Core、Third-party mo:*、Local)にグループ化 → 各グループをパスのアルファベット順に並べ替え → セクション間に空行を挿入 → インポート形式はそのまま維持。
エージェントの戦略(AI アシスタント向け)
- 改善に着手する前に、プロジェクトが
mo:coreを使ってビルドできることを確認します。 - ファイルごとに作業します。各ファイルについて:
- A. 単一式の
return形式を削除します - B. skills/dot-notation-migration/SKILL.md に従ってドット記法を適用します
- C. 導入したドット記法で必要になるインポートを確認します(skills/dot-notation-migration/SKILL.md のインポート対応表を参照)
- D. 実際に未使用のインポートを削除します(専用スキルのドット記法インポート対応表に従います)
- E. ローカル(同階層)インポートのパスを短縮します(該当する場合は
./プレフィックスを削除) - F. インポートを3つのセクションにまとめ、各セクション内をアルファベット順に並べます(Core → Third-party mo:* → Local)
- G. 対象の型が
Blobの場合、Text.encodeUtf8("<literal>")を"<literal>"に置き換えます。
- A. 単一式の
- 各ファイルの作業後にコンパイルします。インポート不足が原因で失敗した場合は、インポートを復元して対応表に記録します。
- リポジトリ内の各カテゴリの作業後に、全体ビルドを実行します(MOPS パッケージでは
find src -type f -name "*.mo" -print0 | xargs -0 -n1 $(mops toolchain bin moc) --check $(mops sources)を使用)。必要に応じてテストも実行します。 - 変更内容と、手動レビューのために保留したエッジケースを簡潔に報告します
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()(型は var array から推論されます)。
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