Rubyの文字列補間式からクラス名を抽出
Tailwind CSSのRuby向けエクストラクタにおいて、ヒアドキュメント内の文字列補間式(#{...})からクラス名が抽出されない問題が修正されました。コメント判定ロジックの改善により、補間式内の文字列リテラルが正しく認識されるようになります。
背景
Tailwind CSSのRuby向けエクストラクタは、コメント(#で始まる行)を無視する処理を実装していました。しかし、この処理がRubyの文字列補間式(#{...})も誤ってコメントとして扱ってしまい、ヒアドキュメント内の補間式からクラス名が抽出されない問題が発生していました。#19728 でこの問題が報告されています。
既に strict locals の定義(<%# locals: ... %>)に対する例外処理は実装されていましたが、文字列補間式については考慮されていませんでした。以下のコードでは、'w-100' が抽出されない状態でした:
def width_class_heredoc(width = nil)
<<~STYLE_CLASS
inline-flex
#{width || 'w-100'}
STYLE_CLASS
end
この問題はヒアドキュメント内の補間式に特有で、通常の文字列補間("#{width || 'w-99'}")や単純な式(width || 'w-98')からは正しくクラス名が抽出されていました。
技術的な変更
crates/oxide/src/extractor/pre_processors/ruby.rs の コメント判定ロジック に、文字列補間式を除外する条件が追加されました。
変更前:
b'#' if !matches!(cursor.prev(), b'%') => {
result[cursor.pos] = b' ';
cursor.advance();
// ... コメント行のスキップ処理
}
変更後:
b'#' if !matches!(cursor.prev(), b'%') && !matches!(cursor.next(), b'{') => {
result[cursor.pos] = b' ';
cursor.advance();
// ... コメント行のスキップ処理
}
!matches!(cursor.next(), b'{') という条件により、# の次の文字が { である場合(つまり #{ パターン)はコメントとして扱わないようになりました。これにより、文字列補間式内の文字列リテラルがクラス名として正しく抽出されます。
追加されたリグレッションテストでは、ヒアドキュメント内の補間式から 'w-100' が抽出されることを確認しています:
#[test]
fn test_interpolated_expressions() {
let input = r#"
def width_class(width = nil)
<<~STYLE_CLASS
#{width || 'w-100'}
STYLE_CLASS
end
"#;
Ruby::test_extract_contains(input, vec!["w-100"]);
}
設計判断
コメント判定の条件に1文字先読みを追加する方式 が採用されました。
Rubyのコメント構文と文字列補間式は共に # で始まるため、文脈を考慮した判定が必要です。本修正では、既存の cursor.prev() による前方文字チェック(strict locals用)に加えて、cursor.next() による後方文字チェックを追加しています。この判定順序により、以下の3パターンを正しく区別できます:
-
<%# locals: ... %>: strict localsの定義(%#パターン)→ スキップしない -
#{ ... }: 文字列補間式(#{パターン)→ スキップしない -
# comment: 通常のコメント → スキップする
1文字の先読みのみで判定を完結させることで、パーサの状態管理を複雑化させずに問題を解決しています。
まとめ
本PRは、Rubyのコメント判定ロジックに文字列補間式の除外条件を追加した修正です。1文字の先読みチェックを加えるだけで、ヒアドキュメント内の補間式からのクラス名抽出が可能になり、Rubyプロジェクトでのユーティリティファーストなクラス定義パターンがより柔軟になります。