フラットレコードの反復処理

bca.flatten_spaces(result) は、ネストした FuncSpace ツリーを行きがけ順(pre-order)に走査し、ノードごとにスカラー値のみのフラットな dict を 1 つずつ生成します — sqlite3.executemanypandas.DataFrame.from_records をはじめ、あらゆる表形式のコンシューマーにそのまま渡せます。

メトリクスのキーは、CLI の CSV ライターと同じドット区切りの規約(cyclomatic.modified.sumhalstead.volumeloc.lloc_average など)を使います。識別キー(pathnamekindstart_lineend_lineparent_namedepth)はすべてのレコードに付加されます。

executemany による SQLite への書き込み

以下の例は、1 つのファイルを分析し、フラット化された全キーの和集合を列とする sqlite テーブルへ、FuncSpace ごとに 1 行を挿入します。

"""Flatten a FuncSpace tree into scalar rows for sqlite / pandas.

Demonstrates ``bca.flatten_spaces`` + ``sqlite3.executemany``. The
pandas equivalent is shown in the book as a non-executed snippet so
this example stays dependency-free (sqlite ships with the stdlib).

Tied to the book's ``python/flat-records.md`` page.
"""

from __future__ import annotations

import sqlite3
from contextlib import closing
from pathlib import Path

import big_code_analysis as bca


def run(path: Path, db_path: Path) -> int:
    """Analyse ``path`` and insert one row per FuncSpace into ``db_path``.

    Returns the number of rows inserted so the test can assert on it.
    """
    result = bca.analyze(path)
    if result is None:
        msg = f"{path} was skipped (looks generated)"
        raise SystemExit(msg)

    # フラット化されたキーはドット区切りの小文字名
    # (`halstead.unique_operators`、`halstead.total_operators` など)で、
    # SQLite の大文字小文字を区別しない列比較の下でも一意です(かつての
    # `N1`/`n1` の Halstead 衝突は #511 で解消済み)。そのため各キーは
    # リネームなしでそれぞれ独自の列に収まります。
    records = [dict(r) for r in bca.flatten_spaces(result)]
    if not records:
        return 0

    columns = sorted({k for r in records for k in r})
    cols_sql = ", ".join(f'"{c}"' for c in columns)
    placeholders = ", ".join("?" for _ in columns)
    rows = [tuple(r.get(c) for c in columns) for r in records]

    # `closing(sqlite3.connect(...))` がドキュメント化されたイディオムです —
    # 素の ``with sqlite3.connect(...)`` コンテキストマネージャーは
    # トランザクションのコミット / ロールバックを行うだけで、接続を
    # クローズしません。そのため長時間動くコンシューマーはファイル
    # ディスクリプターをリークします(さらに Windows では db ファイルの
    # 排他的書き込みロックを保持し続けます)。
    with closing(sqlite3.connect(db_path)) as db, db:
        db.execute(f"CREATE TABLE IF NOT EXISTS metrics ({cols_sql})")
        db.executemany(
            f"INSERT INTO metrics ({cols_sql}) VALUES ({placeholders})",
            rows,
        )

    return len(rows)


if __name__ == "__main__":
    import sys

    if len(sys.argv) != 3:
        sys.exit("usage: python flat_records.py <source-file> <out.db>")
    inserted = run(Path(sys.argv[1]), Path(sys.argv[2]))
    print(f"inserted {inserted} rows into {sys.argv[2]}")

このイテレーターは 遅延評価かつ単回使用 です。リスト全体を実体化せずに、入力を 1 回だけ走査します。同じイテレーターを 2 回目に反復しても何も返しません — 再反復が必要な場合は一度 list() を呼んでください。

Pandas

flatten_spacespandas.DataFrame.from_records への自然な入力です。Pandas はバインディングの依存関係ではないため、DataFrame ビューが必要な場合は別途インストールしてください。

import big_code_analysis as bca
import pandas as pd

result = bca.analyze("src/lib.rs")
if result is not None:
    df = pd.DataFrame.from_records(bca.flatten_spaces(result))
    print(df.head())
    # スペースの kind でグループ化し、関数・クラス・ファイルそれぞれの
    # 循環的複雑度の平均を確認します。
    by_kind = df.groupby("kind")["cyclomatic.sum"].mean()

識別列と CLI CSV の比較

フラットレコードのスキーマは CLI の CSV ライターとほぼ揃っていますが、意図的な差分がいくつかあります。

  • 識別列は、こちらでは name / kind を使いますが、CSV ライターは space_name / space_kind を使います。フラットレコードには parent_name / depth も追加されますが、CSV ライターにはありません。
  • tokens.* は JSON の形(tokens.tokenstokens.averagetokens.mintokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV では tokens.sum と綴られます。average / min / max のリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。

匿名スペース(Rust のクロージャ、JavaScript の関数式やアロー関数)は name == "<anonymous>" マーカーをそのまま保持します — flatten_spaces は正規化を行いません。

注意点

  • parent_name だけでは、異なる親の下にネストした同名の兄弟を区別できません(例えば、異なる外側クラスの下にある 2 つの Inner クラスは、どちらも自身の子に対して parent_name == "Inner" として現れます)。完全修飾パスが必要な場合は、depth とソース順の位置を組み合わせるか、コンシューマー側で修飾名を再構築してください。
  • 反復中に入力の result を変更しないでください。ウォーカーは入力への参照を保持しているため、まだ生成されていないサブツリーへの変更は後続のレコードに反映されてしまいます。
  • 存在しないメトリクスのサブツリーはキーを生成しません(None ではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。
  • flatten_spaces は、入力がマッピングでない場合に TypeError を送出します。呼び出し側は、渡す前に bca.analyzeNone 戻り値(例えば skip_generated=True の下での生成ファイル)をフィルタリングする必要があります。