エラー処理
バインディングはエラーを 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.analyze と bca.analyze_source は次を送出します:
| 例外 | 継承元 | 発生条件 |
|---|---|---|
bca.UnsupportedLanguageError | ValueError | 未知の拡張子で、シバン / emacs モードにも一致しない |
bca.ParseError | ValueError | tree-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) | 発生条件 |
|---|---|---|
FileNotFoundError | 2 (ENOENT) | パスが存在しない。 |
PermissionError | 13 (EACCES) | 呼び出しユーザーに読み取り権限が与えられていない。 |
IsADirectoryError | 21 (EISDIR) | パスがディレクトリを指している。 |
ファミリー全体を捕捉して err.errno / err.filename を自分で調べたい場合は、except OSError を使ってください。
UnsupportedLanguageError と ParseError はどちらも 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 を呼び出してください — 単一ファイルの analyze は FileNotFoundError / 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.NotARepositoryError | bca.VcsError | repo_path が git 作業ツリーの中にない |
bca.InvalidRevisionError | bca.VcsError | reference / commit を解決できなかった |
bca.InvalidDiffError | bca.VcsError | vcs.score_diff に渡された diff が不正である |
bca.VcsEnvironmentError | bca.VcsError | 履歴走査、差分計算、.mailmap、blame、またはキャッシュ I/O が失敗した |
bca.VcsError(直接) | ValueError | 不正なオプション値(ウィンドウ / タイムスタンプ / 計算式 / ファイルタイプの範囲 / バスファクターのしきい値 / ボットパターン / トレンドのポイント数)。メッセージが問題の値を示します |
NotARepositoryError は「リポジトリではないのでこのディレクトリをスキップする」ために分岐すべきバリアントです。ベースの VcsError は不正なオプションに対して直接送出され、メッセージが問題の値を示します。一方、名前付きのサブクラスは入力の失敗(存在しないリビジョン、不正な diff)を扱います。VcsEnvironmentError は環境 / バックエンドの区分で、同じ失敗に対して web クレートが返す 500(400 ではなく)レスポンスに対応します。
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"],
)