エラー処理

バインディングはエラーを 2 つの領域に分けています:

  • 呼び出し側のエラーは例外として送出されます — 不正な引数には ValueError、誤った型には TypeError、ファイルシステムの失敗には OSError とそのサブクラスです。
  • バッチ内のファイルごとの解析エラーは、結果リストの中の bca.AnalysisFailure 値として「返されます」。これらは例外ではなく、送出されることはありません。

単一ファイルの bca.analyze は前者の経路をたどり、バッチの bca.analyze_batch は後者の経路をたどります。

def run(
    fixtures: Path,
    *,
    missing_path: Path,
) -> dict[str, Any]:
    """Trigger each error path and return a small report.

    ``fixtures`` is a directory containing at least ``hello.rs``;
    ``missing_path`` must NOT exist on disk.
    """
    report: dict[str, Any] = {
        "file_not_found": False,
        "unsupported": False,
        "batch_errors": 0,
    }

    # 1. 存在しないパスに対する analyze() は型付きの OSError サブクラスを送出します。
    try:
        bca.analyze(missing_path)
    except FileNotFoundError as err:
        report["file_not_found"] = True
        print(f"file_not_found: errno={err.errno} filename={err.filename}")

    # 2. 未知の拡張子に対する analyze() は
    #    UnsupportedLanguageError(それ自体が ValueError のサブクラス)を送出します。
    #    書き込みを try/finally の内側に置いているため、将来 analyse 呼び出しの
    #    前に 2 つ目の変更が加わってもクリーンアップされます。
    unknown = fixtures / "hello.unknown_extension"
    try:
        unknown.write_text("noop", encoding="utf-8")
        bca.analyze(unknown)
    except bca.UnsupportedLanguageError as err:
        report["unsupported"] = True
        print(f"unsupported_language: {err}")
    finally:
        unknown.unlink(missing_ok=True)

    # 3. analyze_batch() は AnalysisFailure を返し、ファイル単位で例外を送出することはありません。
    paths = [fixtures / "hello.rs", missing_path]
    for slot in bca.analyze_batch(paths):
        if isinstance(slot, bca.AnalysisFailure):
            report["batch_errors"] += 1
            print(f"batch_error: ({slot.error_kind}) {slot.error}")

    return report

単一ファイルの例外

bca.analyzebca.analyze_source は次を送出します:

例外継承元発生条件
bca.UnsupportedLanguageErrorValueError未知の拡張子で、シバン / emacs モードにも一致しない
bca.ParseErrorValueErrortree-sitter がソースを拒否した
ValueError(直接)allow_lossy_path=False(デフォルト)での非 UTF-8 パス
OSError とそのサブクラスstd::fs::read が失敗した

analyze が送出する OSError は、errno に基づいて正規のサブクラスへ振り分けられます:

import big_code_analysis as bca

path = "src/example.rs"

try:
    bca.analyze(path)
except FileNotFoundError as err:
    print("missing:", err.errno, err.filename)
except PermissionError as err:
    print("denied:", err.errno, err.filename)
except IsADirectoryError as err:
    print("directory:", err.errno, err.filename)

各分岐は元となる errno に基づいて振り分けられます:

例外典型的な err.errno(Linux)発生条件
FileNotFoundError2 (ENOENT)パスが存在しない。
PermissionError13 (EACCES)呼び出しユーザーに読み取り権限が与えられていない。
IsADirectoryError21 (EISDIR)パスがディレクトリを指している。

ファミリー全体を捕捉して err.errno / err.filename を自分で調べたい場合は、except OSError を使ってください。

UnsupportedLanguageErrorParseError はどちらも ValueError のサブクラスなので、単一の except ValueError で両方を捕捉できます。区別したい場合は型付きの捕捉を優先してください。

バッチのエラー

bca.analyze_batch は例外を送出する代わりに bca.AnalysisFailure 値を返すため、1 つの不正なファイルがバッチ全体を壊すことはありません。

for slot in bca.analyze_batch(paths):
    if isinstance(slot, bca.AnalysisFailure):
        log.warning("%s (%s): %s", slot.path, slot.error_kind, slot.error)
    else:
        process(slot)

error_kind は閉じた Literal です。

  • "UnsupportedLanguage" — 拡張子と shebang / emacs モードのどちらの解決も空振りに終わった場合です。
  • "ParseError" — tree-sitter が入力を拒否したか、または(まれに)Rust 側での結果の JSON シリアライズが失敗した場合です。シリアライズの失敗では error 文字列に internal: serialization error: という接頭辞が付きます。区別が必要な場合はこの接頭辞を確認してください(シリアライズの失敗はファイルの再読み込みでは回復できません)。
  • "IoError" — 最も一般的な種類で、std::fs::read が失敗した場合です。この閉じた分類には非 UTF-8 パスの失敗も畳み込まれているため、パスのエンコーディングエラーは独立した 4 つ目の値としてではなく "IoError" として現れます。

"IoError" のインスタンスでは、元となる OS の errno が Rust のデフォルト書式(Unix では "<msg> (os error <N>)")で error 文字列に保持されます。リトライの分類に必要な場合は正規表現でパースしてください:

import re

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

型付きの OSError サブクラスが必要な場合は、analyze_batch ではなくファイルごとに bca.analyze を呼び出してください — 単一ファイルの analyzeFileNotFoundError / PermissionError / IsADirectoryError を直接送出します。

バッチにおけるプログラマーエラー

analyze_batch も、呼び出し側のバグに対しては例外を送出します:

  • paths がイテラブルでない場合、または要素が str / os.PathLike[str] でない場合は TypeError です。これは呼び出し全体を中断し、不正な要素より前に計算された結果はすべて破棄されます。
  • metrics= が明示的に空のシーケンスであるか、未知の名前を含む場合は ValueError です。検証は入力イテラブルの __iter__ より「前に」実行されるため、この送出経路ではジェネレーターの副作用(および部分的な yield)は温存されます。

変更履歴(VCS)の例外

big_code_analysis.vcs の各関数は、bca.VcsError(それ自体が ValueError)を頂点とする型付き階層を送出するため、既存の except ValueError(または except bca.VcsError)ですべての VCS の失敗を捕捉できます(#624)。analyze(..., vcs=True) キーワード引数も同じオプション解析エラーを共有します。

例外継承元発生条件
bca.NotARepositoryErrorbca.VcsErrorrepo_path が git 作業ツリーの中にない
bca.InvalidRevisionErrorbca.VcsErrorreference / commit を解決できなかった
bca.InvalidDiffErrorbca.VcsErrorvcs.score_diff に渡された diff が不正である
bca.VcsEnvironmentErrorbca.VcsError履歴走査、差分計算、.mailmap、blame、またはキャッシュ I/O が失敗した
bca.VcsError(直接)ValueError不正なオプション値(ウィンドウ / タイムスタンプ / 計算式 / ファイルタイプの範囲 / バスファクターのしきい値 / ボットパターン / トレンドのポイント数)。メッセージが問題の値を示します

NotARepositoryError は「リポジトリではないのでこのディレクトリをスキップする」ために分岐すべきバリアントです。ベースの VcsError は不正なオプションに対して直接送出され、メッセージが問題の値を示します。一方、名前付きのサブクラスは入力の失敗(存在しないリビジョン、不正な diff)を扱います。VcsEnvironmentError は環境 / バックエンドの区分で、同じ失敗に対して web クレートが返す 500400 ではなく)レスポンスに対応します。

import big_code_analysis as bca
from big_code_analysis import vcs

try:
    report = vcs.rank("path/to/repo", top=20)
except bca.NotARepositoryError:
    print("not a git repository, skipping")
except bca.VcsError as err:
    # 不正なウィンドウ、計算式、ファイルタイプの範囲など。
    print("bad VCS option:", err)

analyze(..., vcs=True)NotARepositoryError のルールの例外です。リポジトリ外のファイルは例外を送出せず、単に vcs ブロックを生成しないだけなので、この経路から呼び出し側に届くのはオプション解析の VcsError だけです。

ログ出力のレシピ

バッチ出力用の小さなログ補助関数を使えば、独自の整形を書かずに成功 / 失敗を揃えて記録できます:

import logging
import big_code_analysis as bca

log = logging.getLogger(__name__)

def report(paths: list[str]) -> None:
    # skip_generated=False は結果リストのインデックスを `paths` と
    # 揃えたままにします。デフォルトの True では、生成ファイルはスロットを
    # 持たず、zip が黙ってずれてしまいます。
    for path, slot in zip(paths, bca.analyze_batch(paths, skip_generated=False)):
        if isinstance(slot, bca.AnalysisFailure):
            log.warning(
                "skip %s (%s): %s", path, slot.error_kind, slot.error
            )
        else:
            log.info(
                "ok %s sloc=%s", path,
                slot["metrics"]["loc"]["sloc"],
            )

関連項目

  • バッチ処理 — ファイルごとの失敗を AnalysisFailure のスロットへ振り向ける、例外を送出しない契約。
  • 非同期パターンasyncio.gather(..., return_exceptions=True) はバッチの契約の非同期側の等価物です。タスクごとの例外は gather 全体をキャンセルする代わりに結果リストに入ります。
  • クイックスタート — 型付き OSError サブクラスを送出する単一ファイルの analyze 経路。