Rust入門者がrust-analyzerへのコントリビュートを達成するまで

はじめに

はじめまして、テクノロジー本部バックエンド開発グループの松田です。

この記事は、CADDi Advent Calendar 15日目の記事です。昨日は、和田さんによる「Rust と nalgebra で MLP を実装した話」でした!

CADDiでは、バックエンドの主要な開発言語としてRustを採用していますが、エンジニアの多くはRust未経験の状態でCADDiに入社し、入社前後にRustの勉強を開始しています。11月に入社した私も、Rustを本格的に利用するのはCADDiが初めてで、入社が決まって以降、Rustの勉強を行っていました。

また、これは個人的な話ですが、これまでにOSSへのコントリビュートを一度も行ったことがなく、いつかはOSSコントリビュートを行ってみたいと思っていました。

今回、Rust入門とOSSコントリビュート達成という2つの目標を同時に追求するため、「何でもいいからRustのOSSのIssueをひとつ選んでコントリビュートする」ということを行ってみました。

その模様を、毎週火曜日の午前に行っているCADDiの社内勉強会である「STUDDi」で発表しましたので、ダイジェストをここにまとめたいと思います。

伝えたいこと

OSSへのコントリビュートに興味があるがコントリビュート経験がない読者様に対し、

  • 手頃なIssueを探せば、その言語や技術の熟練者でなくてもOSSにコントリビュートすることは可能
  • 簡単に見えるIssueであっても、取り組んでみると勉強になる

ことをお伝えすることがこの記事の目的です。

逆に、既にOSSへのコントリビュートの経験がある方にとっては、あまり得られるものがないかもしれません。

いろいろなコントリビュート

OSSへのコントリビュートを行うきっかけとしてよくあるのは、「あるOSSを自プロダクトで利用したいが、必要な機能が存在しない」という状況だと思います。この場合、どのOSSに対してどのようなコントリビュートを行うべきかは明確です。

一方、私の場合、身も蓋もなく言えば、「何でもいいからRust製OSSへのコントリビュートをしてみたい」というモチベーションだったため、何から手を付ければ良いか分かりませんでした。

このため、私はまず、Rustの公式ウェブサイトのコントリビュータ向けページに飛びました。

すると、一口に「コントリビュート」といっても以下のような様々な方法があることが紹介されていました。

  • 問題を捜し、仕分け、修正する
  • ドキュメント
  • コミュニティの形成
  • ツール、IDE、インフラ
  • ライブラリ
  • 言語、コンパイラ、標準ライブラリ
  • 国際化

このように、「コントリビュート」には、ソースコードを書いてPRを提出することだけでなく、ドキュメントの整備や、コミュニティの形成といった取り組みも含まれます。

ただし、私のモチベーションとしては、とりあえず何らかRustのソースコードに手を入れてみたい気持ちがありました。そのため、上記のうち、「問題を捜し、仕分け、修正する」の方法に取り組むことにしました。

初心者向けのIssueを探そう

さて、問題を捜し、仕分け、修正すると言っても、私はRust入門者ですから、いきなり複雑度の高いIssueに取り組むのは困難です。幸い、Rustのコントリビュータ向けページでは、初心者向けのIssueの探し方が紹介されていました。

それは、E-easyまたはE-mentorというラベルが付いたIssueを探すことです。

E-easyは文字通り、難易度が低いことを示すラベルです。一方、E-mentorは、そのIssueに関連するコードや、修正の方針について相談に乗ってくれるメンターがアサインされていることを示します。

また、上記の他、ソースコードの該当箇所や、修正の方向性などが分かりやすくブレークダウンして記載されていることを示すE-has-instructionsタグや、そのOSSに初めてコントリビュートする人向けのIssueであることを示すgood first issueラベルなども存在します。

good first issueを求めて

私は、とりあえず上記のラベルが付けられたIssue(例えば、good first issueタグが付いたIssueの一覧)を片っ端から見て回り、自分でも取り組めそうなIssueを探すことにしました。

ここで直面したのは、手軽に取り組めそうなIssueほど、既に他の開発者が着手してしまっているという問題です。

業務のプログラミングであれば、新規メンバー向けの手頃なタスクを先輩メンバーが用意してくれたりするものですが、OSSでは誰かがタスクをあてがってくれるわけではないので、この点はどうしようもありません。

私は、自分が取り組めそうで、かつ、他の開発者が着手していないIssueを手に入れるため、rust-lang/rust, rust-lang/rustfmt, rust-analyzer/rust-analyzerなど、思いついたRust製OSSのリポジトリのIssueをひたすら探してまわりました。

誰も着手していないIssueを発見

その結果、発見したのが、rust-analyzerのこのIssueです。

これは、rust-analyzerの機能のうち、Assistと呼ばれる入力補助機能群の一つに関するテストについて、冗長になっているものを最小化せよというIssueです。

具体的には、テスト対象の関数内に分岐Aと分岐Bが存在して、それぞれM通りとN通りのパスに分かれているとき、分岐A内の処理と分岐B内の処理の内容が独立であればテストケースの数はM+N個で済むはずのところ、MxN個のテストケースが書かれてしまっていることが問題となっていました。

整数リテラルの基数を変換するAssist機能

テスト対象のAssist機能は、具体的には、エディタ上に入力された整数リテラルの基数を変換するという機能です。

rust-analyzerをインストールしたエディタ上で整数リテラルを入力し、その上にカーソルを合わせると、電球のマーク(VSCodeの場合)が表示されると思います。この電球のマークをクリックすると、Convert integer baseという選択肢が表示されます。これを選択することにより、1000b1100100に、あるいは1000x64に、といった具合に基数を変換することができます。

Convert integer base

基数を選ぶ

テストの肥大化

今回のAssist機能の場合、

  • 基数は2, 8, 10, 16の4通り
  • 整数リテラルがunderscoreで区切られているかどうかで2通り
    • Rustでは、100_000のようにunderscoreで整数リテラルを区切ることができます
  • 型名がsuffixとして付いているかどうかで2通り
    • Rustでは、100i64のように型名をsuffixとして付けることができます

という分岐が存在するところ、変換元の基数 * underscoreの有無 * suffixの有無の組み合わせ全パターンのテストが存在する状態となっていました。

rust-analyzerのコード規約においても、テストは最小化することとされており、これはコード規約にも反している状態でした。

Issueを完遂できるか検証する

簡単そうなタスクだと思ったら、依存関係が複雑に広がっていて想定の何倍も手がかかった、という経験は日常業務においても誰しもしていることだと思います。

OSSの場合、休日に趣味として取り組んでいるわけで、軽い気持ちで手を出してみたもののなかなか完遂できず、しかし一度着手したものを投げ出すのも……という気持ちででダラダラと作業を続ける、という状態になってしまうと精神的に辛そうです。

そこで、このIssueが本当に自分の想定の範囲内で完遂できるかどうか、事前に検証することにしました。

幸い、修正対象のコードの位置は、Issueから貼られたリンクにより示されていました。初心者向けのタグが付けられたIssueの場合、このようにコードの該当箇所へのリンクが示されている場合が多いようです。

また、docs/dev/以下に置かれたドキュメントを参照しつつ、rust-analyzer全体のアーキテクチャを確認しました。その結果、今回の対象となるAssist機能は、rust-analyzerのコアな部分から疎結合な設計となっており、rust-analyzer全体を理解しなくても着手できそうだということが分かりました。

以上を踏まえ、休日の1日の間に完遂できそう、と判断しました。

手を挙げる

やるときめたらすぐに手を挙げておかないと、他の開発者が同じIssueに着手してしまいます。

そこで、すぐに手を挙げました。

すると、10分も経たないうちにメンテナの方から返信があり、このIssueは私が担当することが決まりました。

手を挙げる

テスト対象機能内の各分岐は独立

晴れてIssueを獲得したので、さっそく修正に着手します。

テスト対象の機能においては、変換元の基数、underscoreの有無、suffixの有無に応じて分岐が生じていることを上述しましたが、これらの分岐が相互に独立していると言えるのかどうか、改めて検証してみました。

その結果、これら3つの分岐は相互に独立していることが確認できました。

具体的には、テスト対象の機能においては、以下のような順序で処理が行われていました。

  1. まず、underscoreとsuffixを除去する
    • その際、suffixについては、その有無と内容をOption型の変数に保持
  2. underscoreとsuffixが除去された整数リテラルの基数を変換
  3. suffixが付いていた場合、変換後の整数リテラルに変換前と同じsuffixを付加

したがって、underscore有りの場合およびsuffix有りの場合については、どれか1つの基数の場合についてのみテストを行えば足りるということが分かりました。

実際のコードは以下のとおりです。それぞれの処理は、他の分岐でどのパスを通ったかに影響されないことが分かるかと思います。

underscoreの除去
let buf;
if text.contains("_") {
    buf = text.replace('_', "");  // underscoreの除去
    text = buf.as_str();
};

let value = u128::from_str_radix(text, radix as u32).ok()?;
Some(value)
基数の変換とsuffixの付け直し
// 変換先の基数に応じて変換
let mut converted = match target_radix {
    Radix::Binary => format!("0b{:b}", value),
    Radix::Octal => format!("0o{:o}", value),
    Radix::Decimal => value.to_string(),
    Radix::Hexadecimal => format!("0x{:X}", value),
};

let label = format!("Convert {} to {}{}", literal, converted, suffix.unwrap_or_default());

if let Some(suffix) = suffix {
    converted.push_str(suffix); // suffixを付け直す
}

不要なテストケースをひたすら消していく

あとは、不要なテストケースをひたすら消していくだけのお仕事です。

具体的には、underscore有りの場合およびsuffix有りの場合については、変換元の基数が10進数の場合のテストのみを残し、それ以外の基数のテストは削除しました。

また、これまでの説明では省略しましたが、テスト対象の機能について、私が作業を行う1週間ほど前にリファクタリングが行われていたところ、それによって不要になったはずのエッジケースに対応するテストなども残存していたので、これらもまとめてお掃除しました。

リポジトリをforkしてPR

通常の社内リポジトリの場合、作業対象のリポジトリ内でfeatureブランチなどを切り、master/mainブランチなどに対してPRを送るのが一般的だと思います。

これに対し、OSSの場合、作業対象リポジトリを自分のGitHubアカウント内にforkし、自分のアカウント内で作業を行ったものをfork元リポジトリに対してPRする、という手順が採られることが多いようです。

forkしてPR

このようにするメリットとして、PRがマージされない限り、作業対象リポジトリに対して一切影響を与えないことが保証されるという点が挙げられます。

完成したPR

完成したPRは以下のとおりです。

pr

rust-analyzerの場合、PRやコミットメッセージの書き方に明確なルールはないようだったので、よしなに書きました。

なお、私の場合、英語でのコミュニケーションにあまり自信がありません。そのため、PRの内容に細かい質問をされて英語でのディスカッションが始まると辛いなと思ったので、初めから丁寧な説明を書いて、それを読んで全てを理解してもらう、という戦略を採りました。

diffは以下のような感じです。

ほとんど削除しただけなので真っ赤ですね。

Merged!

PR提出後、ドキドキしながら晩ごはんを食べていたのですが、30分も経たないうちにメンテナの方によりマージされました。

merged

削除するだけの簡単な内容ですが、Perfectと言ってもらえると嬉しいですね。

まとめ

初心者でもOSSへのコントリビュートは可能

OSS、それも、rust-analyzerのようにある程度複雑で規模の大きいOSSへのコントリビュートというと、その言語を初めたばかりの初心者には難しいのではないかと思ってしまいがちです。

しかしながら、自分の力量にあったIssueを探せば、初心者でもOSSへのコントリビュートは十分に可能と感じました。

逆に言えば、そのような手頃なIssueを探すことが最も大変だとも言えます。簡単なIssueほどすぐに着手されがちであり、空いていることが少ないからです。

この点については、初心者向けのタグが付いたIssueを片っ端から当たっていくしかないのではないかと思います。

簡単なIssueであっても、やってみると勉強になる

漠然とコードリーディングをするのと、1行でも変更するつもりで読むのとでは、当事者意識が大きく違ってくると感じました。

今回の場合、やったことはテストを削除しただけですが、あるテストケースを削除して良いかどうか判断するためには、テスト対象の処理を注意深く読む必要があり、その結果として、テスト対象の処理そのものについても理解を深めることができました。

自分のPRがマージされたときの達成感は大きい

自分のPRが国籍も所属会社も異なる海外のメンテナによって評価され、マージされるという体験は、日常業務とはまた異なる達成感がありました。とても楽しいので今後も挑戦していきたいです。

最後に

CADDiでは、フロントエンド,バックエンド,アルゴリズム,SREと幅広くエンジニアを募集しております.興味をお持ちいただけた方は,こちらからご連絡ください!

明日は、@yskeee000さんが、CADDiのサプライチェーン管理システムについてご説明する予定です!