Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

bca.flatten_spaces(result) walks the nested FuncSpace tree in pre-order and yields one flat, scalar-only dict per node, ready for sqlite3.executemany, pandas.DataFrame.from_records, or any other tabular consumer. This page is for a caller loading metrics into a database or a DataFrame.

Metric keys use the same dotted convention as the CLI’s CSV writer (cyclomatic.modified.sum, halstead.volume, loc.lloc_average, and so on). Identity keys (path, name, kind, start_line, end_line, parent_name, depth) are added on every record.

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]}")

The iterator is lazy and single-use: it walks the input once without materialising the whole list. A second iteration of the same iterator yields nothing; call list() once if you need to re-iterate.

Pandas

flatten_spaces is the natural input to pandas.DataFrame.from_records. pandas is not a dependency of the bindings; install it separately if you want the DataFrame view.

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 の比較

The flat-record schema is mostly aligned with the CLI’s CSV writer, with a few intentional deltas:

  • 識別列は、こちらでは name / kind を使いますが、CSV ライターは space_name / space_kind を使います。フラットレコードには parent_name / depth も追加されますが、CSV ライターにはありません。
  • The per-space cognitive.value, cyclomatic.value, cyclomatic.modified.value, abc.value and nargs.value leaves (the numbers bca check thresholds against) are present here, as in the JSON shape, but absent from the CSV header.
  • tokens.* は JSON の形(tokens.tokens、tokens.average、tokens.min、tokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV では tokens.sum と綴られます。average / min / max のリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。

Anonymous spaces (Rust closures, JavaScript function expressions and arrows) keep their name == "<anonymous>" marker verbatim; flatten_spaces does not normalise.

注意点

  • parent_name alone cannot disambiguate same-named siblings nested under different parents (two Inner classes under two different outer classes both surface as parent_name == "Inner" for their own children). Pair it with depth and source-order position, or rebuild the qualified name in your consumer, if you need a fully-qualified path.
  • Do not mutate the input result while iterating: the walker keeps references into it, so mutations to not-yet-yielded subtrees show up in later records.
  • 存在しないメトリクスのサブツリーはキーを生成しません(None ではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。
  • flatten_spaces raises TypeError if the input is not a mapping. Filter out the None returns from bca.analyze (generated files under skip_generated=True) before passing, and likewise the None slots analyze_batch emits under skip_generated=False, which mark a file the read gate declined to parse.