動機
Rustでは同名の変数を再束縛(シャドーイング)できます。
これは適当な変数名を付けたいときにとても便利で、特に競技プログラミングのように可読性よりスピードを優先する場面では多用しています。
ただしその反面、意図せず同名の束縛を導入してしまい、元の変数が見えなくなっていることに気づかずバグらせることがたまによくあります
これが原因で答えが合わず、しばらくあたふたするということがたまにあります。
fn main() {
input! {
k: usize
}
for i in 0..10 {
for j in 0..10 {
// ここで意図せず k をシャドーイングしてしまう
for k in 0..10 {
// 元の k はここから見えない
...
}
}
}
}
Rust 的には正しいコードですが、""ここは本当に意図したシャドーイングなのか""をエディタ上で軽くチェックできたら便利だな、と思いました。
ということで Rust のシャドーイングを検知できるNeovim プラグインを作ってみることにしました。
目論見
最初に考えた方針は以下です。
- Tree-sitterで構文解析して変数を管理すればいけそう
- 重くなりそうなので、普段は OFF にしておいて必要なときだけ ONできるようにトグルできるようにしたい
調査
Treesitter
まずはTreesitterで構文木を解析し、変数宣言を拾う方針でいこうと考えました。
しかし競プロでよく使う proconio の input! マクロ内の変数宣言がうまく取れませんでした。
:InspectTree で見ると、以下のように ERROR ノードが混じってしまっていました。
(macro_invocation ; [12, 4] - [14, 5]
macro: (identifier) ; [12, 4] - [12, 9]
(token_tree ; [12, 11] - [14, 5]
(source_file ; [12, 11] - [14, 5]
(expression_statement ; [12, 11] - [14, 5]
(block ; [12, 11] - [14, 5]
(ERROR ; [13, 8] - [13, 10]
(identifier)) ; [13, 8] - [13, 9]
(identifier)))) ; [13, 11] - [13, 16]
(identifier) ; [13, 8] - [13, 9]
(primitive_type))) ; [13, 11] - [13, 16]
input! {
k: usize
}
マクロ内の変数を諦めれば実装はできますが、
マクロで宣言された変数も含めてシャドーイング検知できた方が便利なので、マクロ内で宣言した変数についても検知できるようにしたいと考えました。
lsp
一方で、Neovim 上の unused variable の警告は、マクロ内の変数に対しても正しく出ていることに気づきました。
そこで、LSP の semantic token を使えば、マクロ内の変数宣言も拾えるのではと思い、この方針で実装を進めることにしました。
実装概要
現時点での実装は、大まかに以下の流れになっています。
- rust-analyzerからsemantic tokenを取得
- 変数宣言っぽいところを抽出
- それらを「同一スコープ」ごとにまとめる
- 同じスコープ内で同名の束縛が複数あれば、2つ目以降を diagnostics の WARN として表示
なお、現状のスコープ判定は関数単位です。Rust本来のスコープまでは見ていません。
正当なブロック内シャドーイングも警告してしまうことがあります(このあたりは今後の課題)。
試作品
一応動くものができ、シャドーイングが発生した時にWARNとが出せるようになりました。
また、普段使う時は意図的にシャドーイングしている時がほとんどなので、:RustShadowToggle でこれを出すか出さないかをトグルできるようにしました。
バグった際のデバッグツールとして活用したいと思っています。
おわりに
まだ実装途中ですが、ソースコードは以下に置いてあります。
現時点で分かっている課題としては、
- ユーザー設定項目がほぼない
- 関数内に無名関数や関数内関数などがあると過剰検知してしまう
- Rust本来のスコープまでは見ていないため、意図しない検知をしてしまうそう
などまだまだ改善中です。
年末年始の暇な時間に、ブロックスコープ対応や精度改善を進められたらいいなと思っています。あくまでも思っています

