Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: RailsガイドがRails 8.0.1に対応ほか(20250123)

こんにちは、hachi8833です。間が空いてしまって申し訳ありません。🙇🙏

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やX.comでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 最近の改修(Rails公式ニュースより)

間が空いてしまったので、昨年12月頃からの主な改修をピックアップしました。🙏

🔗 ActiveRecord::NormalizationがActive Modelに移行(その後取り消し)

動機/背景

この振る舞いをサポートするために、実装の大部分を新しいActiveModel::Normalizationモジュールに移動した。「永続性」関連の言語、メソッド、テスト範囲はすべて削除された。

追加情報

実装における唯一の変更点は、属性の読み書きに関連している。

 def normalize_attribute(name)
   # 値を新規の非normalize値として扱う
-  self[name] = self[name]
+  send(:"#{name}=", send(name))
 end

このプルリクは、rails/rails#53886のような変更が行われた場合は取り消す可能性がある。
同PRより


つっつきボイス:「ActiveRecord::Normalizationを切り出して、Active Modelで使えるようにしたんですね」「移動できるものは移動しようという感じかな」

参考: Rails API ActiveRecord::Normalization

「これには続きがあって、上のプルリク↑がマージされたのは昨年だったんですが、その後以下でマージが取り消されていました↓」「あら」「APIドキュメントを準備するまで保留か」「遠からず実際に移動しそうではありますね👍」

#53887を取り消す。

これはマージしてよい状態ではなかった。ドキュメントもActive Recordから移動すべき。
振る舞いをドキュメント化するためだけに空のモジュールを必要とすべきではない。
同PRより

🔗 ActiveRecord::Persistence#increment!を新規レコードで呼んだ場合はraiseするよう修正

修正: #26420

Rails 4では新規レコードに対して#increment!を呼び出すと保存される。この振る舞いはよくないと思うが、もう何年も修正されていない。

#update_columnsと同様にraiseする方がよい。
同PRより


つっつきボイス:「保存していない新規レコードやdestroy済みのレコードを更新できたらおかしいので、この修正はわかる👍」「!付きの#increment!って使ったことなかった」「UPDATE文でSET hoge=hoge+1みたいなのを楽に書きたいときに使うのかなと思ったけど、その属性だけを即値でインクリメントするみたいですね」

# activerecord/lib/active_record/persistence.rb#L649
    def increment!(attribute, by = 1, touch: nil)
+     raise ActiveRecordError, "cannot update a new record" if new_record?
+     raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
+
      increment(attribute, by)
      change = public_send(attribute) - (public_send(:"#{attribute}_in_database") || 0)
      self.class.update_counters(id, attribute => change, touch: touch)
      public_send(:"clear_#{attribute}_change")
      self
    end

参考: Rails edge API increment! -- ActiveRecord::Persistence

データベースに更新を書き込むincrementのラッパー。attributeのみが更新され、レコード自体は保存されない。つまり、変更された他の属性はダーティなままとなる。バリデーションとコールバックはスキップされる。
increment! -- ActiveRecord::Persistenceより

🔗 シリアライズされる属性にcomparableオプションを指定可能になった

修正: #28416

シリアライズされた属性を扱うときに割とよくある問題は、オブジェクトのシリアライズ表現が時間の経過とともに変化する可能性があること。変化する理由としては、あるシリアライザから別のシリアライザに移行したとき (YAMLからJSONやmsgpackへの移行など)や、使っているシリアライザの出力が微妙に変更されたときがある。

1つの例としてlibyamlがある。以前は末尾に余分な空白があったが、最近yaml/libyaml#186で修正された。

このようなことが起こると、実際には変更されていないのに変更されたと報告されるレコードが多数発生し、最もましな場合でもデータベースへの書き込みが大幅に増加し、最悪の場合は厄介なバグにつながる。

したがって、シリアライズ表現を比較するのではなくデシリアライズされたオブジェクトを比較するのが理想的ではあるが、デシリアライズされた型に、オブジェクトを深いところまで適切に比較できる==メソッドが存在することを想定するわけにはいかない。

すなわち、現状の最善の策としては、型が比較可能であると宣言されている場合にのみ比較することである。
同PRより


つっつきボイス:「このプルリクを見ていて、Rubyのバージョンが変わると以前のバージョンでシリアライズしたオブジェクトをデシリアライズできなくなるみたいな話を思い出しました」「あぁ、それはMarshall.dumpしたものをMarshall.loadできるかどうかはそのときのRubyのバージョンに依存することがあるという話ですね」

参考: Marshal.#dump (Ruby 3.4 リファレンスマニュアル)
参考: Marshal.#load (Ruby 3.4 リファレンスマニュアル)

「JSONやmsgpackをシリアライザとして使っていれば、それ自体に変更が入らない限り通常は問題ないはず」「プルリクではシリアライザを変更した場合に問題が起きることがあると書かれていますけど、シリアライザを変更することってそうそうなさそうですよね」「たまに変更が必要になることがなくもないけど、あんまりやりたくない作業」「"デシリアライズされた型にオブジェクトを深いところまで適切に比較できる==メソッドが存在することを想定するわけにはいかない"、これはその通り」

「このプルリクではActiveRecord::AttributeMethods::Serializationserializecomparableオプションを追加して、オプションがtrueのときだけ比較可能にしたんですね」「なるほど」

# activerecord/lib/active_record/attribute_methods/serialization.rb#L183
-       def serialize(attr_name, coder: nil, type: Object, yaml: {}, **options)
+       def serialize(attr_name, coder: nil, type: Object, comparable: false, yaml: {}, **options)
          coder ||= default_column_serializer
          unless coder
            raise ArgumentError, <<~MSG.squish
              missing keyword: :coder
              If no default coder is configured, a coder must be provided to `serialize`.
            MSG
          end
          column_serializer = build_column_serializer(attr_name, coder, type, yaml)
          attribute(attr_name, **options)
          decorate_attributes([attr_name]) do |attr_name, cast_type|
            if type_incompatible_with_serialize?(cast_type, coder, type)
              raise ColumnNotSerializableError.new(attr_name, cast_type)
            end

            cast_type = cast_type.subtype if Type::Serialized === cast_type
-           Type::Serialized.new(cast_type, column_serializer)
+           Type::Serialized.new(cast_type, column_serializer, comparable: comparable)
          end
        end

参考: Rails edge API serialize -- ActiveRecord::AttributeMethods::Serialization::ClassMethods - Ruby on Rails API

🔗 Range#eachRange#stepのモンキーパッチを削除した

#11474を取り消す。

動機/背景

  • Range#each

rangeの最初の要素が#succを実装していない場合、Range#eachはTypeErrorをraiseする。現状のモンキーパッチはRubyと同じ振る舞いを実装しているため、当てる価値がない。

  • Range#step

Ruby 3.4でRange#stepの振る舞いが変更され(#7444)、連続する要素の生成で#+メソッドが使われるようになったことで、一部の非数値型に対して適切な結果が得られるようになった。この変更により、次のようなコードが可能になる (Ruby 3.4.0-preview2でテスト済み)。

(Time.current..).step(30.minutes).first(3)
# =>
# [Thu, 12 Dec 2024 23:55:41.568749000 CET +01:00,
#  Fri, 13 Dec 2024 00:25:41.568749000 CET +01:00,
#  Fri, 13 Dec 2024 00:55:41.568749000 CET +01:00]

ActiveSupport::TimeWithZoneでは#+を独自に実装している

しかし、RubyのRange#stepがオーバーライドされており、特にActiveSupport::TimeWithZoneの境界が妨げられるため、この独自実装は現在機能しない。

(Time.current..).step(30.minutes).first(3)
# can't iterate from ActiveSupport::TimeWithZone (TypeError)

#11474でこのオーバーライドが導入された当時は、Rubyの振る舞いが異なっていた。

Ruby 3.4より前のRange#stepは、Range#eachと同様にTypeErrorをraiseするので、これらのバージョンにモンキーパッチを当てる価値はない。

詳細

このプルリクは、Range#eachRange#stepのモンキーパッチを削除する。
同PRより


つっつきボイス:「このあたりは年末にたまたま追いかけていてWebチーム内発表でも取り上げたんですが、要するにRuby 3.4.0の変更↓によってこれまでActive Supportで当てられていたモンキーパッチが不要になったというか、モンキーパッチがあると動かなくなったので削除したという流れでした」「こんなふうにRubyの内部実装の変更でRails側の対応が変わることはたまにありますね」

参考: Range#step の意味が少し変わった -- プロと読み解くRuby 3.4 NEWS - STORES Product Blog

参考: Make Range#step to consistently use + for iteration by zverok · Pull Request #7444 · ruby/ruby


なお、Ruby 3.4では、非数値のrangeでstep(0)step(0.0)を呼び出してもエラーにならなくなっています↓。

$ ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +YJIT +PRISM [arm64-darwin24]
$ irb
irb(main):001> ('a'..'c').step(0)
=> #<Enumerator: ...>
irb(main):002> ('a'..'c').step(0.0)
=> #<Enumerator: ...>

参考: Feature #18368: Range#step semantics for non-Numeric ranges - Ruby master - Ruby Issue Tracking System

🔗 ActionDispatch::Session::CacheStorecheck_collisionsオプションが追加された

新しく生成されるセッションIDは、128ビットのランダムネスを利用する。これは衝突が発生しないことを保証するには十分すぎるほどだが、セッションをさらに強化する必要がある場合は、このオプションを有効にして、セッションストアでIDが実際に空いているかどうかを保証できる。ただし、これによりセッション作成時に追加の書き込みが発生する。

Shia
同Changelogより

背景

ActiveSupport::Cache::Storeをセッションストレージとして利用するSession::CacheStoreは、セッションIDの一意性を保証するためにSecureRandom.hex(16)に依存している(ref)。
これにより 2^128個の値が提供され、ほとんどの場合に実質的に十分だが(参考: 誕生日攻撃 - Wikipedia)、追加のオーバーヘッドを犠牲にしても一意性を保証する追加対策が好ましいシステムもある(例: RedisCacheStoreの単一のsetコマンド)。

提案

生成されたセッションIDが既に利用されているかどうかをチェックすることで、セッションIDの一意性を保証するオプションをSession::CacheStoreに追加することを提案したい。このオプションは、ensure_sid_uniquenessのような名前(またはもっと適切な名前)になるだろう。

実装の参照


つっつきボイス:「セッション用キャッシュストアにcheck_collisionsというオプションが追加されたそうです」「これを有効にするとパフォーマンスが落ちる代わりに、新しいセッションIDを生成するときに既存のセッションIDと絶対かぶらないようチェックされるんですね: 多少の速度を犠牲にしてでも絶対にセッションIDが重複しないことをシステム上保証したいケースは確かにありそう👍」

# actionpack/lib/action_dispatch/middleware/session/cache_store.rb#L26
    class CacheStore < AbstractSecureStore
      def initialize(app, options = {})
        @cache = options[:cache] || Rails.cache
        options[:expire_after] ||= @cache.options[:expires_in]
+       @check_collisions = options[:check_collisions] || false
        super
      end
...
        end

+       def generate_sid
+         if @check_collisions
+           loop do
+             sid = super
+             key = cache_key(sid.private_id)
+             break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
+           end
+         else
+           super
+         end
+       end

参考: Rails edge API ActionDispatch::Session::CacheStore - Ruby on Rails API

🔗 ActiveStorage::Attachment <-> Blobリレーションにinverse_ofを追加したときの挙動を修正

修正: #50800
修正: #53451

動機/背景

このコミットは、#50800で導入された振る舞い変更を修正するため。この振る舞い変更によって、#53451#53452で報告された問題が発生する。

詳細

コードパスやコールバックが多数発生していて問題が微妙であるため、説明が少し難しいが、簡単に言うと、ActiveStorage::Attachment <-> Blobリレーションにinverse_ofを追加すると、添付ファイルの保存方法に振る舞いの違いが追加される。

問題

#50800以後、ActiveStorage::Attachment <-> Blobinverse_ofを追加すると、関連付けられた AttachmentがBlobで認識されるようになり、関連付けの保存コールバックの順序に問題が発生するようになった。

class User < ApplicationRecord
  has_one_attached :avatar

  blob = create_active_storage_blob("avatar.png")
  User.create!(avatar: blob)
end

#50800がマージされる前は、以下のステップをたどってレコードが永続化された。

  1. Blobが作成される
  2. Userが作成される
  3. BlobがUser<->Blob autosave関連付けを介して更新される
    Active storageは、添付ファイルが作成されるときにBlobのメタデータの一部を変更する(rails/activestorage/lib/active_storage/attached/changes/create_one.rb at ad9bc2a51d5980d1c0990db97e552015410778f0 · rails/rails
  4. AttachmentがUser<->Attachment autosave関連付けを介して作成される

しかし#50800以後は以下のように変わった。

1-3. (上と同じ)
4. Attachmentが(Userではなく)Blob<->Attachment autosave関連付けを介して作成される


これが問題になる理由は、#53451#53452でそれぞれ異なるため、個別に説明する。

#53451での問題

Blob#touch_attachmentsafter_updateコールバックより前のタイミングに実行されるafter_updateコールバックで、Blob関連付けを介して添付ファイルが作成されるようになったため、Blobが関連付けられた添付ファイルをtouchするようになり、その結果、レコード(上の場合はUserのレコード)が読み込まれてtouchされる。

user.touch_laterを呼び出すと、ユーザー(メモリ上のユーザーではなく、データベースから読み込まれたユーザー)がトランザクションに追加され、最終的にメモリ上のユーザーが置き換えられてcommitコールバックが実行される(これこれ
以下に簡単な例を示す。

class Car < ApplicationRecord
  attr_accessor :engine_sound

  after_save :touch_car
  after_commit :rev_engine

  def touch_car
    Car.find(1).touch
  end

  def rev_engine
    puts engine_sound # => nil
  end
end

Car.new(id: 1, engine_sound: "vroom")

#53452での問題

従来は、UserのAttachment#previously_new_record?はtrueになっていた。これは、リレーションのautosave関連付けにより添付ファイルが作成されていたため。

AttachmentはBlobのautosave関連付けを介して保存されるようになったため、User<->Attachmentのautosave関連付けが呼び出されると、添付ファイルはすでに保存済みなので更新と見なされ、コードパスが変わり、結果として@previously_new_recordフラグがリセットされる(これこれ)。

ソリューション

Blob<->Attachmentリレーションを変更してautosaveをfalseに設定する方法よりも良い方法が見当たらない。
この方法では、Blobは添付ファイルを保存する責務を持たないが、レコード(User)には保存する責務がある。
また、Blobに添付ファイルを保存する責任を持たせることにそもそも意味がないと思われる。

BlobとAttachmentをインスタンス化し、Blobを保存してAttachmentを保存しないままにしておくと、問題が発生する理由を考えようとしたが、何も思いつかなかった。理由は、Attachmentが有効であるためにはレコード(Userなど)が必要であり、その同じレコードがAttachmentを保存するため。

重要

アプリケーションが何らかの理由で既存のBlobのメタデータを変更して保存した場合、既存のAttachmentとダーティ属性はこのコミットでは保存されない。

よくわからないのだが、これは普通によくあるシナリオなのだろうか?

追加情報

このコミットの代わりに、#50800を取り消して、Blob<->Attachment のinverse_ofを明示的にnilに設定する方法もありだろう。
同PRより


つっつきボイス:「説明は長いですが、#50800がマージされて以降のAttachmentモデルのautomergeのリレーションが、inverse_ofを追加したときにUser<->AttachmentからBlob<->Attachmentに変わってしまったということだそうです」「割とレアなケースみたいだけど、こういう微妙な変更を知らずに踏むとつらい」「修正は以下の1行だけでした↓」

# activestorage/app/models/active_storage/blob.rb#L19
class ActiveStorage::Blob < ActiveStorage::Record
  MINIMUM_TOKEN_LENGTH = 28
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
  store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
  class_attribute :services, default: {}
  class_attribute :service, instance_accessor: false
- has_many :attachments
+ has_many :attachments, autosave: false

添付ファイルがあるレコードのダーティ属性がリセットされ、そのレコードのafter_commitコールバックが期待どおりに動作しなくなる問題が修正された。

この変更は内部的なものであり、アプリケーションの変更を必要としない。Active Storageの添付ファイルは引き続き (別のリレーションを介して)自動保存される。

Edouard-chin
同Changelogより

🔗 structure.sqlファイル内のバージョン表示を工夫してマージの競合を削減した

  • スキーマダンパー用のバージョンフォーマッタを導入

スキーマダンパーがstructure.sqlファイル内のバージョン情報をフォーマットする方法をオーバーライド可能になった。
改修前は、このファイルのスキーマのバージョンは降順で単純にソートされていたが、大規模なチームでは、これによりリストの上部付近でマージの競合が多くの発生する可能性がある。

カスタムフォーマッタにカスタムソートロジック(バージョンのハッシュ値によるソートなど)を提供できるようにしたことで、競合の数を大幅に削減できるようになった。

fatkodima
同Changelogより

#44363の続き(cc @ghiculescu)。

従来は、(スキーマの)バージョンが降順でソートされ、新しいバージョンがリストの先頭に追加されていた。チームの人数が十分多い場合、このリストの先頭付近で多くのマージ競合が発生する可能性がある。私の会社では多くの開発者が多くのスキーマ変更をプッシュしており、誰かがプルリクをマージする時間の半分で、既存の多数のプルリクがmasterブランチと競合するようになった。

この修正によって、バージョンは逆順にした表現(20241201183002->20038110214202)によって降順でソートされるようになる(昇順でもかまわないが、以前の実装との一貫性を保つため降順にした)。これにより、新しいバージョンが既存のバージョンリストのランダムな場所に挿入されるようになり、マージ競合の可能性が大幅に減少する。簡単な計算をしたところ、従来は上位5つの要素のリスト内で競合が発生する可能性が20%あったが、変更後は600個以上の項目のリスト全体で競合が発生する可能性が0.1%に下がった。

また、ダンプされたstructure.sqlファイル内の各バージョンにも、逆バージョン表現のコメントを追加するようにした。これにより、マージの競合を解決しやすくなる。ただし、これを削除して、アルゴリズムを説明するコメント INSERTの先頭に追加することも可能。

既存のアプリの場合、次回structure.sqlが生成されたときに大きな差異が発生することになる。
同PRより


つっつきボイス:「大勢の開発者がスキーマをそんなにしょっちゅう更新することってあるんでしょうか?」「普通にありますね: 競合をなるべく防ぐためにも、最初にスキーマ変更だけを行う小さなプルリクを投げて速やかにマージすること、というルールを設定したりすることもあります」「なるほど、スキーマ変更前にSlackとかで声掛けしたりしますか?」「それはさすがに繁雑になりすぎて開発速度が落ちるでしょう」「それもそうですね😅」

「バージョンを逆順にするって最初何だろう?と思ったけど、20241201183002->20038110214202のように日時の表示が逆順になればバージョンの挿入場所が適度にランダムになるので、バージョンリストの冒頭に更新が集中しにくくなってコンフリクトが減る、ということのようですね」「あ、そういうことか!」「この改修は、schema.rbじゃなくてstructure.sqlを使っている場合にコンフリクトを減らすということなんですね」「最初にスキーマ変更だけをマージするような運用にしていれば普段そこまで困らないと思うけど、スキーマ変更の競合が減ったのはよさそう👍」

🔗 path_paramsがクエリパラメータ経由で渡された場合にクラッシュしないよう修正

Railsアプリのエンドユーザーによってpath_paramsurl_forに渡される可能性がある。Kaminariは現在これをトリガーする。

また、これが起こらないようにするためのテストが書きにくい。機能テストでpath_params: "string"を渡すと、テストランナーがクラッシュする(クラッシュするRailsバグレポートテンプレートのこの例を参照)。

文字列値は Rails内部で500エラーを引き起こす。?path_params[inject]=stringハッシュを使うと、URL生成にpath_paramを挿入することが可能になる。ただし、path_paramsは実際のパラメータよりも優先度が低く、一致するものがない場合には無視されるため、脆弱性はないと思われる。
また、非常によく似た#39616が警告しなかったことを考えると、これを脆弱性だとは思わない。それでも、このキーを削るのはよいことだと思う。なお、似たような問題に対する脆弱性がかつて存在していた: GHSA-r5jw-62xg-j433

動機/背景

自分は、RubyGems.orgのセキュリティスキャンによって発生した500エラーを修正しようとしていた。こちらが知らない研究者が、多数のページに膨大なパラメータリストを送信していた。以下はその抜粋。

これによってrubygems.orgで500エラーが少なくとも2件発生する。対応の手間は小さいものの、オンコールで誰かを叩き起こすには十分。

実際の問題はkaminariにまでさかのぼり、そのためにkaminari/kaminari#1123をオープンした。

これはkaminari側で修正する必要があると思うが、このキーが適切に処理されていることを確認するためのテストを書くのがやや難しいため、Rails側でも修正する必要があると思う(上部にリンクしたバグレポートテンプレートを参照)。

このプルリクによって、path_params:キーを含まないURL生成ごと1個のハッシュアロケーションと1個のマージも節約される(ほとんどの場合)。ただし、このメリットはoption = option.dupによってほとんど帳消しになる可能性がある(重複を保存するように再構成することはおそらく可能だろう)。

詳細

このプルリクは、ActionDispatch::Journey::Formatter#generateを変更して、ハッシュの場合にのみpath_paramsを抽出してマージするようにする。

このプルリクでは、path_paramsのフィルタリングについては特に何も対処していない(kaminariがこれを実行すべきだと思う)。ただし、これにより、path_paramsがハッシュではない場合のクラッシュに対処できるようになり、ほとんどのURL生成の効率がわずかに高まる。

追加情報

これは、自分の同僚@simiがRailsに送信した別の修正#39616と非常によく似ている。この修正も、現在のRailsコードにまだ適用できるのであれば、同じ理由で注目に値すると思う。

それまでは、不要な500エラーが発生しないように、rubygems.orgでもこの修正をパッチとして適用している。
同PRより


つっつきボイス:「このプルリクはrubygems.orgのメンテナーだそうです」「rubygems.orgに誰かさんがpath_params入りのリクエストを投げて500エラーが頻発していたのを修正するためなのね」「脆弱性ではないにしても、path_paramsを投げるだけで500エラーになるのは嬉しくないヤツ」「path_paramsはクエリパラメータの形式だと無理やり送信できてしまうんですね」

🔗Rails

🔗 Rails公式サイトにドキュメントへのランディングページが追加(Rails公式ニュースより)


つっつきボイス:「最近のRails公式サイトのリニューアルの一環としてドキュメントのランディングページが追加されました↓🎉」「なるほど、Rails GuidesだけではなくAPIドキュメントやDiscussionへの動線もポータル的にここに置いたんですね👍」


Ruby on Rails — Docsより

🔗 libyear: 依存関係の更新がどのぐらい遅れているかを数値化するツール

jaredbeck/libyear-bundler - GitHub


つっつきボイス:「これは今翻訳中のEvil Martiansの記事↓で知りました」「なるほど、依存関係の更新がどのぐらい遅れているかを数値化するというアイデアなのね: CIに入れてみてもいいかも👍」「libyearはRuby以外にもJavaScriptやPythonやRustやGoなどいろんな言語に対応していますね」「大規模プロジェクトならlibyearが数世紀に達することもあるみたいです😆」

参考: Railsmas on Mars: 12 Days of Mandatory Developer Joy and Challenge—Martian Chronicles, Evil Martians’ team blog

(上の記事はめちゃめちゃ長いので翻訳公開までしばらくお待ちください🙏)

🔗 RailsガイドがRails 8.0.1に対応


つっつきボイス:「昨年12月にRails 8.0.1がリリースされて、Guidesの更新が多かったのでRailsガイドに一通り反映しました」「お〜🎉」「詳しくは上のNote記事で解説していますが、特にRailsをはじめようガイドが大幅に更新されてRails 8の新機能の概要をキャッチアップしやすくなっています」

参考: Ruby on Rails — Rails Version 8.0.1 has been released!

🔗Ruby

🔗 rail_inspectorのSyntaxTreeがPrismに置き換えられた(Rails公式ニュースより)


つっつきボイス:「Railsの更新情報ですが、lintツールであるrail_inspectorのコンフィグでSyntaxTreeがPrismパーサーに置き換えられたそうです」「お〜、着々とPrismが浸透していますね🎉」

# tools/rail_inspector/lib/rail_inspector/configuring.rb#L14
      def call(path)
-       @cache[path] ||= SyntaxTree.parse(SyntaxTree.read(path))
+       @cache[path] ||= Prism.parse_file(path.to_s).value
      end

Ruby: 2024年までのPrismパーサーの長い歴史を振り返る(翻訳)

🔗 syntax_finder: RubyスクリプトをPrismで解析


つっつきボイス:「これもPrismがらみで、_ko1さんがついこの間作ったツールです」「こんな感じにSyntaxFinderを継承してlook(node)メソッドを定義するとPrismベースで検索できるようになるんですね↓: Prismのおかげでこういうのが作りやすくなった👍」「サンプルコードもいろいろ揃っていますね」

# 同リポジトリより
require 'syntax_finder'

# Count up all `if` statements (if/elsif/?:) and `then` keywords.

class IfThenFinder < SyntaxFinder
  def look node
    if node.type == :if_node
      if node.if_keyword_loc # if or elsif
        inc node.if_keyword_loc.slice
        if node.then_keyword_loc
          inc 'then'
          # inc path: @file
        end
      else
        # a ? b : c
        inc '?:'
      end
    end
  end
end

🔗 その他Ruby

私hachi8833はこの度、地域rbのひとつとして三浦半島.rbの立ち上げに参加しました。近辺のRubyistの皆さま、ご興味がおありでしたらどうぞ。


今回は以上です。

バックナンバー(2024年度第4四半期)

週刊Railsウォッチ: Rails公式のdevcontainerでKamalをサポート、RailsBump.orgほか(20241108)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。