Railsのソースコード読んでみる | Active Support parents編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照ください
shitake4.hatenablog.com

読んだ箇所

parents を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

parentsメソッドは、レシーバに対してparentを呼び出し、Objectに到着するまでパスをさかのぼります。連鎖したモジュールは、階層の下から上の順に配列として返されます。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z
 
X::Y::Z.parents # => [X::Y, X, Object]
M.parents       # => [X::Y, X, Object]

引用:RAILS GUIDES:parents

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def parents を探してみます

f:id:sktktk1230:20180920165902p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > introspection.rb
# frozen_string_literal: true

require "active_support/inflector"

class Module

省略

  # Returns all the parents of this module according to its name, ordered from
  # nested outwards. The receiver is not contained within the result.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M.parents    # => [Object]
  #   M::N.parents # => [M, Object]
  #   X.parents    # => [M, Object]
  def parents
    parents = []
    if parent_name
      parts = parent_name.split("::")
      until parts.empty?
        parents << ActiveSupport::Inflector.constantize(parts * "::")
        parts.pop
      end
    end
    parents << Object unless parents.include? Object
    parents
  end
end

まず、parents 変数を空の配列で初期化しています

次に、if文内をみてみます

if parent_name
  parts = parent_name.split("::")
  until parts.empty?
    parents << ActiveSupport::Inflector.constantize(parts * "::")
    parts.pop
  end
end

分岐条件はparent_nameメソッドの戻り値が真の場合に処理が実行されるようです

※ parent_nameメソッドのコードリーディングについてはparentメソッドを読んだときに書きましたので、こちらを参照くださいRailsのソースコード読んでみる | Active Support parent編 - そういうこともある

parent_nameの戻り値は、親のモジュールの定数を戻り値とします
たとえば、このような戻り値となります

X::Y::Z.parent_name # => "X::Y"
M.parent_name       # => "X::Y"

真になるパターンは親のモジュールがある場合です
偽になるパターンは無名またはトップレベルの場合になります
parent_nameメソッドは無名またはトップレベルの場合に、戻り値がnilになるためです

さらに読み進めていきます
parts = parent_name.split("::") はparent_nameの戻り値の文字列を'::'を区切り文字として分割し、parts変数へ格納するという処理になります
parent_nameの戻り値が、"X::Y"だとするとsplit後の戻り値はこちらになります

"X::Y".split("::")
=> ['X', 'Y']

Ruby 2.5.0 リファレンスマニュアル#split

次にunless内の処理をみていきます

until parts.empty?
  parents << ActiveSupport::Inflector.constantize(parts * "::")
  parts.pop
end

until parts.empty? でparts配列が空になるまでuntil … endを繰り返し処理を行っています

Ruby 2.5.0 リファレンスマニュアル#until
Ruby 2.5.0 リファレンスマニュアル#empty

until内の処理をみてみます

parents << ActiveSupport::Inflector.constantize(parts * "::")
parts.pop

constantizeメソッドの引数 parts * "::" ですが、 配列partsの文字列要素の間に"::"を連結し、文字列を生成しています
Arry#* を調べてみると

self * sep -> String[permalink][rdoc]

指定された sep を間にはさんで連結した文字列を生成して返します。Array#join(sep) と同じ動作をします。

[PARAM] sep:

文字列を指定します。 文字列以外のオブジェクトを指定した場合は to_str メソッドによる暗黙の型変換を試みます。

p [1,2,3] * ","
# => "1,2,3"

引用:Ruby 2.5.0 リファレンスマニュアル#Array*

ActiveSupport::Inflector.constantizeは引数の文字列を定数に変換します
※ constantizeメソッドのコードリーディングについては、こちらを参照くださいRailsのソースコード読んでみる | Active Support constantize編 - そういうこともある

そしてその戻り値をメソッドの最初行で初期化した変数に入れています
parents << ActiveSupport::Inflector.constantize(parts * "::")

そして parts.pop で配列の末尾の要素を取り除いています

Ruby 2.5.0 リファレンスマニュアル#pop

ここまでuntil、if文の処理が読み終わりました

最後にparents変数にObjectが含まれているか確認し、なかった場合はObjectを追加しています
そして戻り値はレシーバの親の定数が含まれた配列を戻り値としています

parents << Object unless parents.include? Object
parents