一度のパースで複数回メトリクスを実行

big-code-analysis のワンショットエントリポイント analyze は、呼び出しのたびに Source を再パースします。異なるメトリクスサブセット、交互に行うカスタム tree-sitter 走査、設定変更後のメトリクス再実行など、ファイルを複数回スコアリングするパイプラインでは、その再パースは無駄な作業です。

1.1.0 で追加された Ast 型がこのシームを公開します。ソースを一度パースし、保持したパース結果に対して Ast::metrics を必要な回数だけ呼び出せます。

これを使うべきとき

次のいずれかに当てはまる場合は Ast を使用してください:

  • 選択的なメトリクス実行。 同じファイルに対して、レポート用にあるメトリクスの集合を計算し、次に CI のしきい値ゲート用に別の集合を計算する。
  • カスタム tree-sitter 走査。 クエリ / ハイライト / シンボル抽出のためにすでに tree_sitter::Tree を駆動しており、同じパースにメトリクスウォーカーを組み込みたい。
  • キャッシュされた解析。 パース済みファイルをメモリに保持する LSP のようなサービスは、設定が変わったときに、バイト列まで戻ることなくオンデマンドでメトリクスを再計算できるべきです。

ファイルごとにすべてのメトリクスを 1 回だけ計算するのであれば、analyze を使い続けてください。現在は内部で Ast に委譲しているため両者の形状は一致しますが、ワンショット API のほうがシンプルなままです。

呼び出しをまたいだ選択的メトリクス

#![allow(unused)]
fn main() {
use big_code_analysis::{Ast, LANG, Metric, MetricsOptions, Source};

let source = b"fn f(x: i32) -> i32 { if x > 0 { 1 } else { -1 } }";

// 1 回のパースで、2 つのメトリクスサブセットを計算します。
let ast = Ast::parse(Source::new(LANG::Rust, source))
    .expect("rust feature enabled");

let loc = ast
    .metrics(MetricsOptions::default().with_only(&[Metric::Loc]))
    .expect("walker succeeds");
let cyclomatic = ast
    .metrics(MetricsOptions::default().with_only(&[Metric::Cyclomatic]))
    .expect("walker succeeds");

println!("ploc = {}", loc.metrics.loc.ploc());
println!("ccn  = {}", cyclomatic.metrics.cyclomatic.cyclomatic_sum());
}

metrics 呼び出しはツリーを 1 回走査します。analyze を 2 回呼ぶ場合と比べた節約分はパースの省略によるものです。パースは、ごく大きなソースファイルを除くあらゆるケースで実行時間の大半を占めます。

同じパース上でのカスタム tree-sitter 走査とメトリクス

Ast::as_tree_sitter は基盤となる tree_sitter::Tree を借用します。返される参照は Ast のライフタイムの間有効で、そこから得られるノードは Ast::source に対して解決されます(マクロ展開時に source が何を返すかについては、下記の C++ プリプロセッサに関する注記 を参照してください)。

現実的な AST 作業(ノード種別のカウント、名前による構文要素の検索、パースエラーの検出、シンボルテーブルの構築など)については、AST を直接走査する を参照してください。以下の例は最小限のスモークテストです。専用の章では完全なパターン(再利用可能な深さ優先ウォーカー、フィールド名による検索、エラー検出)を示します。

#![allow(unused)]
fn main() {
use big_code_analysis::{Ast, LANG, MetricsOptions, Source};

let ast = Ast::parse(Source::new(LANG::Rust, b"fn f() {}"))
    .expect("rust feature enabled");

// 独自の目的でツリーを走査します…
let root = ast.as_tree_sitter().root_node();
assert_eq!(root.kind(), "source_file");

// …そして同じパースに対してメトリクスウォーカーを実行します。
let space = ast
    .metrics(MetricsOptions::default())
    .expect("walker succeeds");
println!("name = {:?}", space.name);
}

同じパース上のオペレーターとオペランド

Ast::ops は、スペースごとのオペレーター/オペランドの Ops ツリー(Halstead メトリクスの背後にあるデータ)を返します。トップレベルの Ops::name は、損失のあるパス文字列ではなく、呼び出し側が渡した Source::name が(None の場合も含めて)明示的にそのまま引き継がれたものです。そのため、この経路で Ops::name_was_lossy が設定されることはありません。

#![allow(unused)]
fn main() {
use big_code_analysis::{Ast, LANG, Source};

let ops = Ast::parse(
    Source::new(LANG::Rust, b"fn f() { let x = 1 + 2; }")
        .with_name(Some("snippet.rs".to_owned())),
)
.expect("rust feature enabled")
.ops()
.expect("walker succeeds");
assert_eq!(ops.name.as_deref(), Some("snippet.rs"));
assert!(ops.operators.iter().any(|op| op == "+"));
}

呼び出し側で構築したツリーの取り込み

すでに自分で tree_sitter::Tree を構築している場合(たとえばエディタや LSP が独自のパーサープールを持っている場合)は、Ast::from_tree_sitter がツリー取り込みの継ぎ目になります。損失のある UTF-8 変換でパスから名前を導出するのではなく、明示的な name: Option<String> をエンドツーエンドで引き継ぎます。

#![allow(unused)]
fn main() {
use big_code_analysis::{Ast, LANG, MetricsOptions, tree_sitter};

let source = b"fn f() {}".to_vec();
let mut parser = tree_sitter::Parser::new();
parser
    .set_language(
        &LANG::Rust
            .tree_sitter_language()
            .expect("rust feature enabled"),
    )
    .expect("rust grammar compatible");
let tree = parser
    .parse(&source, None)
    .expect("parser has a language set");

let ast = Ast::from_tree_sitter(LANG::Rust, tree, source, None)
    .expect("rust feature enabled");
let _ = ast.metrics(MetricsOptions::default()).expect("walker succeeds");
}

ツリーは、lang に対して LANG::tree_sitter_language が返す文法を使って code から生成されたものでなければなりません。不一致は unsafe ではありませんが、メトリクスウォーカーはその言語の enum に由来する tree-sitter の kind_id 値でマッチするため、別の文法から来た値は意味のない結果をもたらします。

C++ プリプロセッサ

プリプロセッサ入力(Source::with_preproc_path + Source::with_preproc)を持つ Source に対して Ast::parse が呼ばれ、言語が LANG::Cpp の場合、tree-sitter より前にマクロの事前パスが実行されます。そして Ast::source は、元の入力ではなく、パーサーが実際に見た 展開後 のバイト列を返します。

Ast::from_tree_sitter は影響を受けません。呼び出し側が構築したツリーをそのまま取り込みます。ツリーを構築する前に呼び出し側が適用した展開(あるいは展開していないこと)が、そのまま Ast::source に反映されます。

並行性

AstSend + Sync です。同じ &Ast に対して複数スレッドから Ast::metrics を実行しても安全です。ウォーカーは保持している tree_sitter::Tree から読み取るだけだからです。(並列メトリクス実行のベンチマークは別途のフォローアップです。)

対象外

  • tree_sitter::InputEdit による増分再パース。 解析パイプライン全体で安定した Ast をキャッシュすることはスコープ内ですが、保持しているツリーを編集することはスコープ外です。
  • デフォルトで並列な API。 Ast::metrics はメトリクス集合をまたいで内部的に並列化しません。サブセットごとに 1 スレッドを使いたい呼び出し側は、自由にそうできます。