🦀

RustのClippyに3カ月でパッチが50個マージされた

#Rust#tech

2025-02-25

世の中にはRustというプログラミング言語があります.さらにRustのプログラムに対するlinterとして,Clippyというものが存在します.

https://github.com/rust-lang/rust-clippy

このClippyに2024年の12月上旬からパッチを投げ続けています.すなわち本記事公開時点で3カ月弱が経ったということになるわけですが,その間に50個のパッチがマージされました.

数やペースが絶対の指標という主張をするつもりは毛頭ないですが,間に大学の卒業論文の提出や労働やサイボウズ・ラボユースの諸々などが存在していたことを踏まえるとそれなりのスピードではないかと思いますし,パッチを投げ始めて少ししてからは,学部卒業までに50個マージを個人的な目標の1つとして据えていたので,達成できて良かったと思っています.

実際@rust-langに対する私の貢献を見てみると,卒論に集中していた1月前半には少し休んでいましたが,それ以外は日常的なcontributionが発生していることが分かります:

直近のRustへの貢献度合いを示した図
直近のRustへの貢献の度合いを示した図(一部マスク)

これまでに投げた内容は,新たなlintルールの追加,既存のlintルールの改善,CI等のインフラ改善,ドキュメントの修正など様々ですが,大半は既存のlintルールのバグ修正や機能拡張を実装しています.

当初は,巨大なOSSに対して継続的に貢献を行うことがどの程度できるのかを試すためにこの活動を始めたのですが,案外続いていると思っているので,本記事では主に前半で再現性がある程度ありそうな部分について,後半で案外こういったものへの参入障壁は高いものではないかもしれない,ということについて触れたいと思います.なお,今後Clippyに対する貢献をいつまで継続するかについては何も決めていません.

モチベーション

まず何故私がClippyを貢献対象に選んだのかについて触れておきます.大きくは次のような理由から来るものです:

  • 私がよく使っていて,検知や提案が賢いと思うことが多いので,中身が気になった
  • 最近でも開発が盛ん
  • linterというソフトウェアの性質上,継続的にパッチを投げるには言語の広範な知識・新たな仕様の把握が求められる場合が多い

特に最後については,貢献を始めてから知らなかった言語仕様を知ったり,TWiRをまじめに読むようになったりした(まあ自分のパッチが載るからという理由もありますが)ので,それなりに正しかったと思います.

あと個人的には別にアドな点とは思ってはいないんですが,やたらとウケが良いので紹介しておくと,ClippyはRust本体のリポジトリ(rust-lang/rust,以下"本体")のsubtreeになっており定期的にsyncされている[1]ので,Clippyにコミットを積むと本体にもコミットを積んでいることになり一石二鳥というのはあります.このような本体のsubtreeになっているものは他にもありますが

最初の大まかな流れ

私が貢献を始める最初に何をやっていたかを大まかに書いておきます.ただしこれは完全に自己流の進行であり,どこかに書かれている手順に則るなどしているわけではないことに注意してください.また,参考情報として私はこれらのステップが一日に収まりましたが,多分最初は暇だった[2]からだと思うので,あまりあてにならないかもしれません.

ドキュメントを読んで雰囲気を掴む

CONTRIBUTING.mdをざっと眺めた後に,公式ドキュメントがあるので,これをざっくり読み,各lintルールがどのように定義されているか,EarlyLintPass/LateLintPassの違い,MIRなどに関するAPIをどのようにして使うことができるのかなどについて把握しました.主には6章の前半を読めばひとまずは良いと思います.実際現在でもこの辺り以外はろくに読んでいません.

そこまで間違っていなかったから良かったものの,根幹的な部分でいくつか致命的な間違いがある[3]ことには読みながら気付いたので,暇な時間にそのようなものの修正も行いました.

各lintルールを何となく把握する

次に,lintルールの一覧を見て,どのようなものがあるかをざっと把握しました.これは例えば新たなlintルールを追加する際に,既存のものを拡張するか,新たに作るかのどちらが適切かなどを判断する観点などで役立ったと思います.

既存のissueを分類する

次にopenなissueの一覧を見て,

  • できそうなやつ
  • 慣れればできそうなやつ
  • 現実的に無理そう[4],もしくは前提が間違っているやつ

的なカテゴリーに分けて3時間くらい(だったと思います)ずっと記録しました.このときに直近1年で出されたopenなissueはほぼ全て見たと思います.

このリストがあったので最初は2週間くらい毎日1つ以上のパッチを出すということが達成できました(多分無ければ無理だったと思います).以降は継続的に,主に新たなissueやパッチを眺めて,上記のリストを更新しています.

ソフトウェアは大抵バグっている

この章題はあえて主語を大きくしているんですが,やっていて何よりもこれを一番実感しました.冒頭にも書いた通り,私がこれまで行ってきたことは主に既存のlintルールのバグを修正することですが,中には割合としては少ないものの,非常に単純な原因でバグっているものがいくつか存在しました.

3か月前の私は,巨大なOSSに貢献することに若干の敷居の高さを感じていましたが,このようなものに遭遇したりしている内に,段々とそのハードルが下がっていったように思います.

なのでここではそのようなもので実際に私が直した例をいくつか紹介し,開発が盛んなOSSなどであってもその参入障壁は案外思っているほど高いものではないかもしれない,ひいては章題のようなマインドを持つことでモチベーションを保ちやすくなるのではないかということについて示したいと思います.

念のため書いておきますが,結果としてバグが混入したことについて責める姿勢は持ち合わせていないし,そのスタンスでは一切話していません.

no_stdメソッド

Rustには標準ライブラリ(std)が存在しますが,これに依存しないプログラムを書くこともできます.以降この環境のことをno_std環境と表記します(ここではno_coreは考慮対象から外します).

当然ですが,no_std環境ではstdに所属するモジュールは使えません.他方,Clippyはlintルールによっては修正後のコードを提案します.

しかしあるとき,no_std環境であるかどうかを考慮せずにstdに依存する修正を提案するlintルールが存在することに気付きました.このようなケースに対しては,no_std環境でも同じlintルールの警告が発出されるようなコードを作ることができれば,もれなくコンパイルが通らない提案がなされるので明確にバグといえます.

さらにこのようなものは提案する際に文字列リテラルとしてベタ書きされている(こういうのとか)ので,Clippyのコードベース上でstd::をgrepして1つ1つ見ていくことにより容易に発見できます.よって結構邪悪なバグ発見法だと思ったので,これをno_stdメソッドと勝手に名づけました.

no_stdメソッドで調査した結果,5つのlintルールがこのようなことをしていた[5]ので,このPRでまとめて直しました:

https://github.com/rust-lang/rust-clippy/pull/13999

生文字列からのエスケープ

Clippyには,write_literalprint_literalというlintルールが存在します.これはwrite!/writeln!/print!/println!マクロにおいて文字列リテラルをフォーマット文字列として渡している場合に警告及び提案を出すもので,例えば次のようなコードは検知対象です:

println!("{}", "foo");

なぜなら単にこう書けば良いためです:

println!("foo");

別にこれは良いのですが,中にはこれらのマクロ中で生文字列が使われているケースもあります.write_literalprint_literalのlintルールは,内部での検知ロジックがほぼ同じなので実装としては大部分の処理が共通化されていました.さらに実装の都合上,元のコードが生文字列を使っている場合であっても,提案をする際にはエスケープされた文字列を使うようになっていました.

例えば次のような\を出力するコードは:

println!("{}", r"\");

次のように\がエスケープされた文字列を用いて修正するよう提案されます:

println!("\\");

エスケープをするのであれば,当然生文字列中に含まれるエスケープされるべき文字を処理する必要がありますが,当時のエスケープ処理(一部抜粋)は次のようになっていました:

https://github.com/rust-lang/rust-clippy/blob/4b05f50b6bb12095a197b0390e57fb92149e99ac/clippy_lints/src/write.rs#L525

replacementが生文字列の中身で,"\"に置換した後に,\\\に置換してエスケープをやったつもりになっています.

お気づきかと思いますが,こうすると生文字列中に"が含まれる場合には,このエスケープ処理によりエスケープシーケンスが余計にエスケープされて\\"になり,結果として文字列全体が壊れます.解決するには単にこれらの置換の順番を入れ替えるだけで良いです.

これはこのPRで直しました:

https://github.com/rust-lang/rust-clippy/pull/13990

バージョンの範囲検索

変わり種として,Clippyの本筋でない部分での単純バグも1つ紹介したいと思います.

先にも示しましたが,ClippyはそのlintルールがまとめられたWebサイトがあります.ここにはいくつかのフィルターがあり,その中に,特定のバージョンやそれ以前/以降に追加されたlintルールを検索するためのフィルターが存在します.

バージョン検索フィルター
バージョン検索フィルター

あるとき,フィルターの表記上は「≧」や「≦」になっているのに,実際に検索して表示される結果の実態は「>」や「<」になっているというissueが上がりました.

すなわち,上の画像に示されるフィルターで「≧1.85.0」のようにすると,本来はRust 1.85.0以降で追加されたルール一覧が出てきてほしいが,実際はRust 1.86.0以降で追加されたルール一覧が出てきてしまう,というものでした.

どうせtypoとかで生じているバグだろうと思って調査したら,案の定でした.これはこのPRで直したんですが,diffを見れば察することができると思います:

https://github.com/rust-lang/rust-clippy/pull/14016

今後

冒頭にも書いた通り,特に何も考えていません.1つの有力な選択肢としてコンパイラに軸足を移すというのは考えてはいますが,自らの興味と深くマッチするような領域が存在するか判断しかねています.あればやるかもしれません.

脚注
  1. 現在syncされようとしているやつはCIがコケ続けているが... ↩︎

  2. 当時は「12月中に卒論を指導教員に投げておいた方が良いかな~」と思っていた矢先に指導教員から「1月前半で大丈夫だよ~」的なことを言われたので卒論をあまりやっていませんでした(大学への提出は2月頭) ↩︎

  3. 例えば例示されている関数の引数が間違っていたり,メソッドなのに関数呼び出しのように書かれていたり,存在しないマクロ名が書かれていたりしました.なんでこんなのが放置されていたんだ...という気持ちです ↩︎

  4. 発火条件を一般的に判定することがほぼ不可能なやつとかが該当します ↩︎

  5. 直した時点でのlintルールの総数が750個ちょいであることを踏まえると,中々の割合だと思います ↩︎