Skip to content

第 5 章: パッケージ管理と静的解析

5.1 はじめに

前の章では、何をコミットし何をコミットしないかを決めました。この章では、処理系の版をどう固定するかと、コードをどう検査するかを見ていきます。

なでしこ3 版の検査は 5 つです。

検査 道具 違反したときの終了コード
文法 gonako lint 1
整形 gonako format の出力と元のファイルの diff 1(diff の値)
テスト 自作のテスト補助(エラー発生) 1
テストが最後まで走ったか check.sh が出力の最後の行を読む 1(自分で決めた)
関数の網羅 自作(tools/function_coverage.nako3) 1(自分で決めた)

Haskell 版の 100・1・1・3 と違って、すべて 1 です。道具の作者が決めた値が gonako lint の 1 つしかなく、残りは diff か自分で決めた値だからです。

この章の見どころは 3 つです。

  • 依存が 1 つも無い。管理する版は処理系(gonako)とそれを入れる Go のツールチェーンの 2 つだけです
  • lint が素通りさせる誤りが多い。第 1〜3 章で見つけたものに、この章で見つけた「引数の不足」を加えて 5 つ並べます
  • カバレッジの仕組みが無い。その代わりに、テストから届く関数を静的に数える検査をなでしこ3 で自作します

5.2 依存と版の管理

依存が 1 つも無い

なでしこ3 版は外部のライブラリを使いません。CSV の読み込み(CSV取得)・JSON(JSONエンコード)・正規表現・ファイルの読み書きは、すべて処理系に組み込まれた命令です。乱数・線形代数・機械学習のアルゴリズムは自作します(ADR 015)。

言語版 依存を書くファイル ロックファイル
PHP 版 composer.json composer.lock
Haskell 版 *.cabal 無し(置かない判断)
なでしこ3 版 無し 無し(依存が無い)

処理系の版は疑似版で固定する

管理する版は、処理系の gonako だけです。tools/install-gonako.sh に書いてあります。

readonly GONAKO_VERSION="v0.0.0-20260924163730-f840295acd3d" # タグ 3.8.8
readonly GO_TOOLCHAIN="go1.26.8"

GOTOOLCHAIN="${GO_TOOLCHAIN}" GOBIN="$(pwd)/bin" \
  go install "github.com/kujirahand/nadesiko3go/cmd/gonako@${GONAKO_VERSION}"

第 1 章で見たとおり、nadesiko3go のタグは v の付かない 3.8.8 なので、Go のモジュールの版としては読めません。タグが指すコミットの疑似版で固定しています。疑似版にはコミットのハッシュ(f840295acd3d)が含まれるので、同じ名前のまま中身が変わることはありません。

Go の既定の設定(GOSUMDB=sum.golang.org)では、go install が取ってきたモジュールを公開のチェックサムのデータベースと照らし合わせます。ロックファイルを置かなくても、「同じ版なら同じ中身」は Go の仕組みが確かめてくれます。

Go のツールチェーンも固定する

nadesiko3go は Go 1.26 を要求し、Nix の Go は 1.25.5 です。GOTOOLCHAIN=go1.26.8 で、導入のときだけ 1.26.8 のツールチェーンに切り替えます。これも版の固定です。 GOTOOLCHAIN=auto にすると「1.26 以上のどれか」が選ばれ、日によって違う版でビルドされることになります。

版を上げるときは、install-gonako.sh の 2 行を書き換えて、全章のテストを通してからコミットします(build(nadesiko3): ...)。処理系は活発に更新されていて、2026 年 9 月だけで 3.8.1 から 3.8.8 まで 8 回、新しい版が出ています。

ファイルを取り込む

なでしこ3 のプログラムは、!「…」を取り込む でほかのファイルを読み込みます。

!「./csv.nako3」を取り込む
!「./dataset.nako3」を取り込む

第 1 章で見たとおり、./ を省くとパッケージの名前として探され、同じフォルダにあっても見つかりません。 なでしこ3 には gonako-package というパッケージの置き場の仕組みもありますが、この版では使っていません。取り込むのはすべて自分のファイルです。

取り込んだファイルの関数は、ファイル名の名前空間に入りますが、呼ぶ側は名前だけで呼べます。そのため、同じ名前の関数を 2 つのファイルに書くと衝突します。第 1 章の 特徴量正解分離 と第 2 章の 目的列分離 のように、関数名をファイルをまたいで一意にしています。

5.3 コードスタイル — gonako format

gonako format は、プログラムを実行せずに見た目だけを整えます。

整えるもの 例
関数定義のかっこ ●(値を桁数で)小数表示とは → ●(値を桁数で)小数表示とは
繰り返しの書き方 I を 0 から 5 まで繰り返す → Iを 0から 5まで繰り返す
字下げ 4 文字にそろえる
演算子の前後 空白を 1 つ置く

2 回かけても結果は変わりません。ただし検査用のモードがありません。 -f を付けなければ整形後の内容を表示するだけで、整形が要るかどうかを終了コードで教えてくれません。check.sh は、表示された内容と元のファイルの差分を取って判定します。

./bin/gonako format src/number_format.nako3 | diff -u src/number_format.nako3 -
--- src/number_format.nako3
+++ -
@@ -4,7 +4,7 @@
-●(値を桁数で)小数表示とは
+●(値を桁数で)小数表示とは

差分があれば diff が終了コード 1 を返します。整形の検査の終了コードは、整形の道具ではなく diff が決めています。

第 4 章で見たとおり、gonako format は CRLF をそのまま残します。改行は整形の対象外です。

5.4 静的解析 — gonako lint とその限界

lint が捕まえるもの

gonako lint は文法を検査します。関数名を打ち間違えて、かっこの外で命令として書いた場合は捕まえます。

[文法エラー]main.nako3(5行目): 未解決の単語があります: [単語『章選択実効』して]

1 回に 1 ファイルしか受け付けないので、check.sh はファイルごとに呼びます。

lint が素通りさせるもの

第 1〜3 章と、この章で見つけた「lint を通るのに誤っている」書き方を並べます。

誤り lint 実行したとき 見つけた場所
かっこの中の未定義の関数名 (「a」を未定義命令) 通る エラーにならず undefined 第 1 章
値の位置の未定義の名前 第1章実効を継続表示 通る エラーにならず undefined を表示(終了コード 0) 第 1 章
名前を 終了 にする 通る プロセスが終了コード 0 で終わる 第 3 章
の間 の条件をかっこで囲まない 通る 「『while』はまだ実行に対応していません」 第 3 章
命令の引数が足りない 通る 足りない引数が別の値で埋められる この章

最後の 1 つは、この章で関数の網羅の検査を作っている最中に踏みました。

引数が足りないと、別の値で埋められる

文字列の中から別の文字列を探す 文字検索 の書式は「AでBからCを文字検索」で、B は何文字目から探すかです。最初は「から」を省いて書きました。

(残りで名前を文字検索) > 0で戻る

テストのいくつかは通り、いくつかは落ちました。調べていくと、同じ式が、呼ぶ場所によって違う値を返していました。

他名=孤立 他長=2 名前=入口 名長=2 残り=入口を試す
最終 残り=入口を試す 名前=入口 位置=0
結果=true

位置 = 残りで名前を文字検索 は 0(見つからない)なのに、同じ式を > 0 で比べた結果は真です。足りない 1 つの引数が、その直前に計算した別の値で埋められていたようで、直前に何を計算したかで結果が変わります。lint は何も言いません。

「から」を書けば、どこで呼んでも同じ値になります。

# 文字検索 の書式は「AでBからCを文字検索」。「から」を省くと、足りない引数が直前に
# 計算した別の値で埋められ、呼ぶ場所によって結果が変わる(lint は捕まえない)。
(残りで1から名前を文字検索) > 0で戻る

同じことが起きていないか、ほかに使っている複数の引数を取る命令(配列検索・区切る・置換・正規表現置換・配列結合・最大値 など)の書式を gonako doc で 1 つずつ確かめ、単独で呼んでも正しい値を返すことを確かめました。助詞が説明と違う書き方(区切る を「の」でなく「を」で呼ぶなど)は受け付けられていましたが、引数の数が足りないものは他にありませんでした。

型の検査も無い

Haskell 版では GHC の -Wall -Werror が型と網羅性を、hlint が書き方を見ました。なでしこ3 版には、その両方がありません。

言語版 型の検査 書き方の検査 名前の検査
Haskell 版 GHC(エラーにする) hlint GHC
PHP 版 PHPStan(レベル 9) PHP-CS-Fixer PHPStan
なでしこ3 版 無し 無し(整形だけ) gonako lint(半分だけ)

第 1 章で見たとおり、!厳チェック という指定も、nadesiko3go 3.8.8 では検査に使われていません。残る守りはテストだけです。 だからこそ、次の 2 つを検査に加えました。

  • テストファイルが最後まで走ったか(第 3 章の 終了 の落とし穴への対策)
  • テストがソースのすべての関数に届いているか(この章の関数の網羅)

5.5 カバレッジの代わり — 関数の網羅

仕組みが無い

カバレッジは、テストがコードのどこを通ったかの割合です。第 3 波の言語版と並べると、なでしこ3 版だけが「計測」もできません。

言語版 計測の仕組み しきい値の機能
Elixir 版 標準 ある
PHP 版 拡張が要る(pcov) 無い(自作)
Haskell 版 標準(HPC) 無い(自作)
なでしこ3 版 無い 無い

処理系に手を入れて実行の記録を取る方法もありますが、それは処理系の改造です。この版では、ソースを読んで分かることだけで、代わりになる検査を作ります。

テストから届く関数を数える

考え方は次のとおりです。

  1. src/ のファイルから関数の定義(●(…)名前とは 〜 行頭の ここまで)を集める
  2. test/ のファイルの中に名前が現れる関数を「直接」届くと数える
  3. 届いた関数の本文の中に名前が現れる関数を「間接」に届くと数え、増えなくなるまで繰り返す
  4. どこからも届かない関数を「未到達」として報告し、1 つでもあれば失敗する

実行しない静的な検査なので、行や分岐を通ったかどうかは分かりません。代わりに、テストの無い関数と、どこからも呼ばれない関数は確実に見つかります。関数の単位の、粗い網羅です。

この検査をなでしこ3 で書きました(tools/function_coverage_lib.nako3)。テストから書きます。

定義一覧 = [{"名前": 「入口」, "本文": 「    下請けを呼ぶ」}, {"名前": 「下請け」, "本文": 「    1で戻る」}, {"名前": 「孤立」, "本文": 「    2で戻る」}]
到達結果 = 定義一覧と「入口を試す」の到達関数算出
「テストから直接呼ばれる関数」で(到達結果["直接"])と[「入口」]が一致確認
「呼ばれた関数の本文から間接に届く関数」で(到達結果["間接"])と[「下請け」]が一致確認
「どこからも届かない関数」で(到達結果["未到達"])と[「孤立」]が一致確認

短い名前が長い名前の一部として現れる

名前が現れるかを文字列の検索で見るので、1 つ落とし穴があります。第 1 章の 正解率 は、第 3 章の 決定木正解率 の一部です。テストが 決定木正解率 しか呼んでいなくても、正解率 も「現れる」と数えてしまいます。

そこで、探す名前を含むより長い名前を、先に文章から取り除いてから探します。

●(文章と名前を名前一覧で)名前出現判定とは
    残り = 文章
    Iを 0から (名前一覧の要素数) - 1まで繰り返す
        他名 = 名前一覧[I]
        もし、(他名の文字数) > (名前の文字数)ならば
            もし、(他名で1から名前を文字検索) > 0ならば
                残り = 残りの他名を「」に置換
            ここまで
        ここまで
    ここまで
    (残りで1から名前を文字検索) > 0で戻る
ここまで
名前一覧 = [「正解率」, 「決定木正解率」]
「長い名前の一部としてだけ現れる短い名前は出現と見なさない」で(「木で予測と正解の決定木正解率」と「正解率」を名前一覧で名前出現判定)と偽が一致確認
「短い名前が単独でも現れれば出現と見なす」で(「決定木正解率と正解率」と「正解率」を名前一覧で名前出現判定)と真が一致確認

5.4 節の「引数が足りない」誤りを踏んだのは、この関数でした。

関数の見出しを取り出す

関数の名前は、見出しの行から ●(…) と末尾の とは を正規表現で消して取り出します。正規表現のかっこで部分を取り出す(抽出文字列)方法を最初に試しましたが、空しか返らなかったので、消す形にしました。

関数は行頭のここまで で閉じます。字下げした ここまで は もし や繰り返しの終わりです。注釈(# で始まる行)は本文に含めません。注釈に書いた関数名で「届いた」と数えないためです。

実行する

./bin/gonako tools/function_coverage.nako3
関数の網羅: 48 件中 48 件(直接 38 件、間接 10 件)

0.09 秒で終わります。わざとテストの無い関数を足すと、名前を挙げて落ちます。

関数の網羅: 49 件中 48 件(直接 38 件、間接 10 件)
未到達: 百分率表示
[実行時エラー]tools/function_coverage.nako3(23行目): テストから届かない関数が 1 件あります

数字の読み方——「届く」と「通る」は違う

最初に測ったとき、間接に届く関数は 12 件でした。その中に 前処理報告作成(第 2 章)と 決定木報告作成(第 3 章)がありました。

どちらも、テストの中では学習データがあるときだけ走る分岐からしか呼ばれていません。データの無い環境では、一度も実行されていないのです。それでも、名前が現れるので「届く」と数えられます。Haskell 版では、データが無いとカバレッジが 11 ポイント下がることで、この穴が数字に出ました。静的に数える方式では出ません。

そこで、この 2 つの関数を自作の小さなデータ(10 行)で直接呼ぶテストを足しました。期待値は、第 2・3 章と同じく Python で書いた参照実装で求めています。

小アヤメ = 「がく片長さ,がく片幅,花弁長さ,花弁幅,種類{改行}0.2,0.6,0.1,0.1,Iris-setosa{改行}…」
「自作のデータで前処理の結果を報告する」で(小アヤメを前処理報告作成)と「データ件数: 10{改行}欠損値の数: がく片長さ=1, がく片幅=0, 花弁長さ=0, 花弁幅=1, 種類=0{改行}…」が一致確認

このデータには、第 2 章の「0 = 「」 が真になる」誤りを捕まえるため、値が 0 のセルを 1 つ入れてあります(がく片幅の欠損値が 0 件と数えられることを確かめる)。

足したあとは、直接 38 件・間接 10 件になりました。「届く」は「通る」の必要条件でしかありません。 数字が 100% でも、間接に届くと数えられた関数が本当に実行されているかは、どの分岐から呼ばれているかを見て確かめます。

5.6 5 つの検査を 1 つにまとめる

5 つの検査を tools/check.sh にまとめています。

./tools/check.sh
== 文法(gonako lint)
== 整形(gonako format との差分)
== テスト
-- test/chapter01_test.nako3
保留: ルールによる判定の正解率がほかの言語版と一致する(学習データがありません)
検査 14 件、失敗 0 件、保留 1 件
…
-- test/random_test.nako3
検査 10 件、失敗 0 件、保留 0 件
== 関数の網羅(tools/function_coverage.nako3)
関数の網羅: 48 件中 48 件(直接 38 件、間接 10 件)
すべての検査が通りました

テストは 10 ファイル・111 件(保留 3 件)で、全体で 4 秒ほどです。関数の網羅の検査そのもの(tools/ のなでしこ3 のファイル)も、lint と整形の対象に入れています。検査の道具も、検査される側のコードと同じ規則で書きます。

第 6 章で、この check.sh を Gulp と CI から呼びます。

5.7 まとめ

  1. 依存が 1 つも無い — CSV・JSON・正規表現は処理系の命令、乱数と線形代数と機械学習は自作。管理する版は処理系と Go のツールチェーンの 2 つだけ
  2. 版は疑似版と GOTOOLCHAIN で固定する — タグが v 無しなので疑似版を書く。Go の既定の設定ではチェックサムのデータベースで中身を確かめるので、ロックファイルが無くても「同じ版なら同じ中身」になる
  3. 取り込みは ./ を付ける — 関数名はファイルをまたいで一意にする
  4. 整形の検査は diff で作る — gonako format に検査用のモードが無い。終了コードを決めているのは diff
  5. lint が素通りさせる誤りが 5 つある — かっこの中の未定義の名前、値の位置の未定義の名前、名前を 終了 にする、の間 の条件のかっこ忘れ、命令の引数の不足。引数が足りないと、直前に計算した別の値で埋められ、呼ぶ場所によって結果が変わる
  6. 型の検査も書き方の検査も無い — 残る守りはテストだけなので、テストが最後まで走ったことと、テストがすべての関数に届いていることを検査に加えた
  7. カバレッジの代わりに関数の網羅を自作した — テストから直接・間接に名前が現れる関数を数える。長い名前の一部として現れる短い名前は数えない。「届く」は「通る」の必要条件でしかないので、データのあるときだけ走る分岐からしか呼ばれない関数には、自作のデータで直接呼ぶテストを足した
  8. 終了コードはすべて 1 — 道具が決めた値は gonako lint だけで、ほかは diff と自分で決めた値

次の章では、check.sh を Gulp と CI に載せます。check.sh がまとめ役になるので、検査の並びを書く場所が 1 箇所で済むことと、それと引き換えに CI の画面で失うものを扱います。

なでしこ3 版のほかの章は なでしこ3 版のトップ から辿れます。