📦

Rust crateの公開API情報を取得する

2023/12/13に公開

TL;DR

ライブラリを作りましたのでそちらを使ってください。

コマンドとして一覧だけ欲しい場合はcargo-public-apiを使うと良いと思います。

はじめに

とある事情でcrateの公開API情報を取得する必要に迫られたので、その方法をまとめます。

crates.ioで公開されているcrateの公開APIは全てdoc.rsで確認することができますが、これの型情報などをJSON形式で取得したいものです。

docs.rsにそれ用のAPIなどがあれば良いなと思ったのですが、パッと調べた感じ、なさそうです。
また手元のpublishしていないcrateの情報も調べたいときがあるので、Rustツールチェインの中にそれらしいことができるコマンドオプションがないか探してみました。

結論としては、rustdocのjson出力オプションが使えます。

cargo +nightly rustdoc -- -Z unstable-options --output-format json

またこれ相当のことをしてくれるrustdoc-jsonというライブラリもあります。

結果はtarget/docに出力されます。中身を見てみると......

{"root":"0:0:1862","crate_version":"0.1.0","includes_private":false,"index":{"0:485":{"id":"0:485","crate_id":0,"name":null,"span":{"filename":"src/lib.rs","begin":[588,23],"end":[588,32]},"visibility":"default","docs":null,"links":{},"attrs":["#[automatically_derived]"],"deprecation":null,"inner":{"impl":{"is_unsafe":false,"generics":{"params":[],"where_predicates":[]},"provided_trait_methods":["ne"],"trait":{"name":"PartialEq","id":"2:2324:203","args":{"angle_bracketed":{"args":[{"type":{"resolved_path":{"name":"Crate","id":"0:477:1773","args":{"angle_bracketed":{"args":[],"bindings":[]}}}}}],"bindings":[]}}},"for":{"resolved_path":{"name":"Crate","id":"0:477:1773","args":{"angle_bracketed":{"args":[],"bindings":[]}}}},"items":["0:486:652"],"negative":false,"synthetic":false,"blanket_impl":null}}},"0:342:1805":{"id":"0:342:1805","crate_id":0, ...}, ...}, ...}

以下の規則に従ってシリアライズ[1]されているようです。

  • 結果は平坦化されている(木構造ではない)
  • itemが0:0:1862などのようにユニークなIDで識別されている
  • トップレベルには以下のキーがある:
    • root: rootモジュールのID
    • crate_version: 対象crateのバージョン
    • includes_private: 非公開APIの情報も含めているかどうか
    • index: 公開API情報の本体(key: ID, value: item)
    • paths: IDと(一意に特定できる)パスの対応
    • external_crates: 外部crateの番号と名前・URLの対応
    • format_version: このフォーマットのバージョン。本記事執筆時点の最新版であるrustdoc 1.73.0では27
  • pub use foo::{bar, baz};などのようなimportは1つずつitemとして登録される
  • pub use foo::*;などのようなglob importも1つのitemとして登録される

厄介なのが木構造ではないという点で、例えばあるstructのフィールドを見るにしてもindexの中から探す必要があります。

そこでこのJSONをdeserializeしてASTのようにvisitできるcrateを作りました。

使い方

CrateBuilderでJSONに入っていた情報を保持するCrateを作成します。

use crate_inspector::CrateBuilder;

let builder = CrateBuilder::default()
    .toolchain("nightly")
    .manifest_path("Cargo.toml");
let krate = builder.build().unwrap();

モジュールやブロックなど中にアイテムを持てるアイテムはitemsメソッドでイテレータを取得できます。
さらにその中からstructだけを取り出したい場合はstructsメソッド、enumを取り出したい場合はenumsメソッド、といった具合にメソッドが定義されています。

for item in krate.items() {
    println!("item: {:?}", item.name);
}
for strc in krate.structs() {
    println!("struct: {}", strc.name());
    println!("#impls: {}", strc.impls().count());
}
for enm in krate.enums() {
    println!("enum: {}", enm.name());
    println!("variants: {:?}", enm.variants().collect::<Vec<_>>());
    println!("#methods: {}", enm.impls().fold(0, |acc, i| acc + i.functions().count()));
}

structのitemはStructItem<'_>などといった風に、itemのサブタイプのように振舞います。
***Item<'_>Crateの参照を持っており、フィールドやメソッドの列挙などができます。
itemのダウンキャストはCrate.downcastメソッドで行えます。

for item in krate.items() {
    if let Some(strc) = krate.downcast::<StructItem>(item) {
        println!("struct: {}", strc.name());
    }
}

また、特定のitemを取得したい場合はCrate.get_*メソッドが使えます。

if let Some(item) = krate.get_item("format") {
    println!("item: {:?}", item.id);
}
if let Some(strc) = krate.get_struct("Crate") {
    println!("struct: {}", strc.name());
}

使用可能なメソッドの一覧はAPIドキュメントを参照してください。

Acknowledgement

crate-inspector2023年度未踏IT人材発掘・育成事業に採択されたプロジェクト「Pythonにトランスパイル可能な静的型付けプログラミング言語の開発」の開発の一環として作成し、独立したcrateとして公開したものです。

プロジェクトの成果は2024年2月17/18日に発表される予定です(案内ページ2022年度の様子)。
プロジェクトの内容やcrate-inspectorがどう使われたのか気になった方はぜひ発表をご覧ください。

脚注
  1. 実装はこちら。serdeでシリアライズしている ↩︎

Discussion