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

f:id:sktktk1230:20180726124729p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

parentメソッドは、名前がネストしたモジュールに対して実行でき、対応する定数を持つモジュールを返します。

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

引用:RAILS GUIDES:parent

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

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

f:id:sktktk1230:20180905100624p:plain

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

1. activesupport > lib > active_support > core_ext > module > introspection.rb
省略

  # Returns the module which contains this one according to its name.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M::N.parent # => M
  #   X.parent    # => M
  #
  # The parent of top-level and anonymous modules is Object.
  #
  #   M.parent          # => Object
  #   Module.new.parent # => Object
  def parent
    parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
  end

省略

三項演算子 xxx ? yyy : zzz を使っています
parent_name の戻り値によって、
真の場合は ActiveSupport::Inflector.constantize(parent_name)
偽の場合は Object
となるようです

parent_name の実装をみてみます

# frozen_string_literal: true

require "active_support/inflector"

class Module
  # Returns the name of the module containing this one.
  #
  #   M::N.parent_name # => "M"
  def parent_name
    if defined?(@parent_name)
      @parent_name
    else
      parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
      @parent_name = parent_name unless frozen?
      parent_name
    end
  end

省略

Moduleクラスをオープンクラスし、parent_name メソッドを実装しています

まずは最初のif文をみてみると

if defined?(@parent_name)
  @parent_name
else

変数@parent_nameが定義されている場合はそのまま変数を戻り値とします
変数@parent_nameが定義されていない場合の処理は以下になります

else
  parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
  @parent_name = parent_name unless frozen?
  parent_name
end

こちらからみていきます
name =~ /::[^:]+\Z/

Moduleクラスのnameメソッドの仕様を調べてみると

モジュールやクラスの名前を文字列で返します。

このメソッドが返す「モジュール / クラスの名前」とは、 より正確には「クラスパス」を指します。 クラスパスとは、ネストしているモジュールすべてを 「::」を使って表示した名前のことです。 クラスパスの例としては「CGI::Session」「Net::HTTP」が挙げられます。

[RETURN]

名前のないモジュール / クラスに対しては nil を返します。

module A
  module B
  end

  p B.name  #=> "A::B"

  class C
  end
end

p A.name    #=> "A"
p A::B.name #=> "A::B"
p A::C.name #=> "A::C"

# 名前のないモジュール / クラス
p Module.new.name   #=> nil
p Class.new.name    #=> nil

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

クラスパスを戻り値をするメソッドのようです

=~ も調べてみます

正規表現 other とのマッチを行います。 マッチが成功すればマッチした位置のインデックスを、そうでなければ nil を返します。

other が正規表現でも文字列でもない場合は other =~ self を行います。

このメソッドが実行されると、組み込み変数 $~, $1, ... にマッチに関する情報が設定されます。

[PARAM] other:

正規表現もしくは =~ メソッドを持つオブジェクト

[EXCEPTION] TypeError:

other が文字列の場合に発生します。

例

p "string" =~ /str/   # => 0
p "string" =~ /not/   # => nil
p "abcfoo" =~ /foo/   # => 3

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

正規表現にパターンマッチした情報を$1などに格納する処理でマッチした場合は、Integerクラスが戻り値となるようです

正規表現をみてみると ::[^:]+\Z/ ::ではじまり[^:]+ :にマッチしない1回以上の繰り返しを末尾とする文字にマッチするようです

三項演算子の真の場合の処理 $`.freeze をみてみます

現在のスコープで最後に成功した正規表現のパターンマッチでマッチした 部分より後ろの文字列です。 最後のマッチが失敗していた場合には nil となります。

Regexp.last_match.post_match と同じです。

この変数はローカルスコープかつスレッドローカル、読み取り専用です。 Ruby起動時の初期値は nil です。

例
str = '<p><a href="http://example.com">example.com</a></p>'
if %r[<a href="(.*?)">(.*?)</a>] =~ str
  p $'
end
#=> "</p>"

引用:Ruby 2.5.0 リファレンスマニュアル#variable $'

パターンマッチした部分より、後ろの文字列を戻り値とし、それを.freeze で変更不可としています

ここまでの処理をサンプルのクラスで実行し、動きをみてみました
f:id:sktktk1230:20180905100707p:plain

次に @parent_name = parent_name unless frozen? をみます

frozen? でレシーバが変更不可状態がチェックしています
理由がわからなかった為、コミットログから探してみます

f:id:sktktk1230:20180905100725p:plain

こちらのコミットが該当のコミットとイシューのようです

プルリクエスト
github.com

イシュー
github.com

イシューをみてみると
f:id:sktktk1230:20180905100750p:plain

オブジェクトをfreezeした状態でインスタンス変数に変更を加えてしまうとエラーとなる為、回避しているようです

frozen? をしている理由がわかったので、 unless の条件文内部の処理をみてみます
@parent_name = parent_name こちらの処理でインスタンス変数@parent_nameににいれることでキャッシュとして扱えるようにしています
2回以降の parent_name メソッドを呼ぶと 一度目の処理でインスタンス変数は初期化済みの為、if defined?(@parent_name) がtrueとなるので、else以降の処理は呼ばれず、インスタンス変数を返す処理になります

parent_nameメソッドの処理は読めたので、 parent メソッドに戻ります

省略

  # Returns the module which contains this one according to its name.
  #
  #   module M
  #     module N
  #     end
  #   end
  #   X = M::N
  #
  #   M::N.parent # => M
  #   X.parent    # => M
  #
  # The parent of top-level and anonymous modules is Object.
  #
  #   M.parent          # => Object
  #   Module.new.parent # => Object
  def parent
    parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
  end

省略

parent_nameの戻り値が真だった場合をみてみます

ActiveSupport::Inflector.constantizeを調べると

Tries to find a constant with the name specified in the argument string.

constantize('Module')   # => Module
constantize('Foo::Bar') # => Foo::Bar

The name is assumed to be the one of a top-level constant, no matter whether it starts with “::” or not. No lexical context is taken into account:

C = 'outside'
module M
  C = 'inside'
  C                # => 'inside'
  constantize('C') # => 'outside', same as ::C
end

引用:Ruby on Rails 5.2.1#ActiveSupport::Inflector#constantize

引数で指定した名前の定数を探すメソッドのようです

<追記>
constantizeもコードリーディングしてみました
blog.shitake4.tech

なので、ActiveSupport::Inflector.constantize(parent_name) は parent_nameの戻り値の定数を返します
偽の場合は、オブジェクトを戻り値としています