バッチ処理

bca.analyze_batch(paths) runs the same analysis as bca.analyze over every path in an iterable and never raises on per-file errors: each result element is an analysis dict, a bca.AnalysisFailure describing the failure, or None. Results preserve input order, so zip(inputs, results) lines up by index when no path is skipped. analyze_batch shares analyze's keyword-only options — exclude_tests, allow_lossy_path, skip_generated (default True), and metrics — so a file is treated the same by both entry points. (The list shape matches the [bca.analyze(p) for p in paths] comprehension only under skip_generated=False; with the default the comprehension keeps a None per skipped file while the batch drops the slot.)

def run(paths: Iterable[Path]) -> dict[str, int]:
    """Analyse ``paths`` as a batch and bucket successes vs failures.

    Returns a small summary dict (`ok`, `errors`, `skipped`, `total`) so
    the accompanying test can assert on it without re-parsing.
    """
    materialised = list(paths)
    # `skip_generated=False` は入力ごとに 1 つの結果要素を保証します
    # (生成ファイルは除外されず分析されます)。そのため `materialised`
    # に対する `strict=True` の zip が `ValueError` を送出することは
    # ありません。2.0 のデフォルト(`skip_generated=True`)では生成
    # ファイルの入力はスロットを生まず、長さがずれて strict な zip が
    # 失敗します — `pipeline_db.py` で修正されたバグ #660 と同じです。
    results = bca.analyze_batch(materialised, skip_generated=False)

    ok = 0
    errors = 0
    skipped = 0
    for path, result in zip(materialised, results, strict=True):
        if isinstance(result, bca.AnalysisFailure):
            errors += 1
            print(f"  skip {path}: ({result.error_kind}) {result.error}")
        elif result is None:
            # The read gate declined this file; the slot is held open
            # so the strict zip above stays aligned (#1238).
            skipped += 1
            print(f"  skip {path}: nothing to parse (empty or binary)")
        else:
            ok += 1
            sloc = result["metrics"]["loc"]["sloc"]
            print(f"  ok   {path}: sloc = {sloc:.0f}")

    return {
        "ok": ok,
        "errors": errors,
        "skipped": skipped,
        "total": len(materialised),
    }

重要な契約をいくつか挙げます。

  • AnalysisFailure は例外として送出されるのではなく、「返されます」Exception のサブクラスではないため、isinstance(slot, bca.AnalysisFailure) が判別手段になります。
  • paths は遅延評価で消費されるため、ジェネレーターも使えます — ただし zip のために入力を保持したい場合は、先にリストへ実体化してください。
  • デフォルトの skip_generated=True では、生成ファイルは スキップ され、要素を 一切 生成しません。そのため結果リストは入力より短くなることがあります — これは、生成ファイルに対して None を返す単一ファイル版 analyze と正確に一致する挙動です。入力ごとに 1 要素を保証したい場合は skip_generated=False を渡してください(2.0 より前のデフォルト)。このデフォルトが 2.0 で反転したのは、analyzeanalyze_batch を切り替えても生成ファイルの扱いが暗黙に変わらないようにするためです。
  • A file that cannot be parsed at all still holds its slot under skip_generated=False, as None. The read gate shared with the CLI walker declines a file of three bytes or fewer, one carrying a UTF-16 BOM, and one whose leading window is not valid UTF-8; that gate is unconditional, so before #1238 those files shrank the list even with the flag off and the zip above mis-paired every later entry. None is the same value single-file analyze returns for them, and bca.to_sarif skips it, so a batch list can be passed straight through.

ディレクトリの走査:analyze_paths

analyze_batch は、明示的なリスト で渡されたパスをそのまま分析します。まずソースファイルを 見つける ところから始めたい場合 —「リポジトリを丸ごと分析したい」場合 — は、CLI の gitignore 対応ウォーカーを再利用する analyze_paths(#658)を使ってください。

import big_code_analysis as bca

results = bca.analyze_paths("path/to/repo", include="*.py")

Each positional seed may be a file or a directory; directories are walked honouring .gitignore, the include / exclude globs (a single glob string or a sequence; a leading ./ is optional, so dir/**./dir/**), and the generated-file filter. A seed naming a file directly is always analysed regardless of exclude — an explicit request overrides ignore-style rules — while include still narrows it by basename. respect_gitignore=False opts into walking ignored files. The result is the same list[FuncSpaceDict | AnalysisFailure | None] shape and never-raise contract as analyze_batch, and it forwards the same exclude_tests / allow_lossy_path / skip_generated / metrics / vcs / vcs_per_function kwargs.

The None slots reach this entry point too, under skip_generated=False, one per discovered file the read gate declined. There is no caller-supplied ordering to pair against here — results follow the walk — so they are not there for a zip. What they buy is that a file the walk found but could not analyse stays visible in the output instead of disappearing from it. On a tree with many binary assets that is a lot of Nones; filter them with [r for r in results if r is not None] if you only want records.

変更履歴メトリクスの付与

analyze_batchanalyze_paths は、単一ファイル版 analyze と同じ vcs=True / vcs_per_function=True キーワード引数を受け付けます(#670)。バッチは、対象ファイルを含むリポジトリごとに履歴インデックス / blame エンジンを 1 つだけ 構築し、そのリポジトリのファイル間で再利用します — analyze(p, vcs=True) を内包表記で回した場合にファイルごとに繰り返される走査を償却する形です。あるファイルで VCS が失敗しても、その AST メトリクスは無傷のまま残ります(AnalysisFailure になることはありません)。どのリポジトリにも属さないファイルには、単に vcs ブロックが付かないだけです。(ファイル単位の付与ではなく)リポジトリ全体をランキングしたい場合は、専用の big_code_analysis.vcs サーフェスを使ってください。

並列実行

analyze_batch に組み込みの並行処理はありません — 逐次的なスイープです。並列化するには、ファイルごとの analyze 呼び出しをスレッドプールへファンアウトしてください。

def run_parallel(paths: Iterable[Path], *, workers: int = 4) -> list[FuncSpaceDict | None]:
    """Fan ``analyze`` out across a thread pool.

    PyO3 releases the GIL across each file's read + parse, so a
    thread pool actually parallelises the heavy work. Use this when
    you need per-file exceptions instead of ``AnalysisFailure`` slots.
    """

    def _analyze(p: Path) -> FuncSpaceDict | None:
        return bca.analyze(p)

    with ThreadPoolExecutor(max_workers=workers) as pool:
        return list(pool.map(_analyze, paths))

PyO3 の Python::detach は、各ファイルの読み込みと tree-sitter によるパースの間 GIL を解放するため、スレッドがインタープリターのロックで直列化されることはありません — ロックを取り合う協調動作ではなく、本当の並列処理です。

AnalysisFailure の分類

error_kind は閉じた Literal です。

error_kind発生条件
"UnsupportedLanguage"未知の拡張子で、シバン / emacs モードにも一致しない
"ParseError"tree-sitter がソースを拒否したか、まれな内部シリアライズ失敗(internal: serialization error: …
"IoError"std::fs::read が失敗した、「または」 パスが有効な UTF-8 でなかった

AnalysisFailure は凍結(frozen)されており、3 つのフィールドすべてに対する __eq__ / __hash__ / __repr__ を実装しているため、呼び出し側はエラーを set に入れて実行間で失敗を重複排除できます。リトライの分類のために、errno は Rust のデフォルト書式によって error 文字列内に保持されます。

import re

match = re.search(r"\(os error (\d+)\)$", slot.error)
errno = int(match.group(1)) if match else None

型付きのディスパッチ(FileNotFoundErrorPermissionError など)が必要な場合は、analyze_batch の代わりにファイルごとに bca.analyze(path) を呼んでください — 単一ファイル版 analyze は正規の OSError サブクラスを送出します。エラー処理を参照してください。