Rakeタスクの定義を追う

  • 0
    Like
  • 0
    Comment

    はじめに

    最近業務でRailsを触り始めました。
    Rails tutorialはこなしているものの、仕事の対象は既存の大規模アプリケーションなので、カスタマイズされている部分が多く読み解くのに苦労しています。

    そもそもなぜコードを読むのに苦労するのかということについて、おおよそ以下の3つの項目が重なっているためだということがわかったので、Rakeを例にとって説明します。

    Rubyを読む際に苦労する点
    - 豊富なシンタックスシュガー
    - 定義はどこに
    - gemの仕様


    Rakeタスク

    私が触っているアプリケーションにはこのようなタスクが定義されていました。

    namespace :hoge do
      desc 'Task without arguments'
      task task1: :environment do
        puts "This is task1"
      end
    
      desc 'Task with arguments'
      task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
        puts "This is task2 with: #{arg1}, #{arg2}"
      end
    end
    

    Rakeタスク

    私が触っているアプリケーションにはこのようなタスクが定義されていました。

    namespace :hoge do
      desc 'Task without arguments'
      task task1: :environment do # :environment とは
        puts "This is task1"
      end
    
      desc 'Task with arguments'
      # 唐突に登場する "=>" (Hashの初期化に使用する記号では?
      # 上のタスク定義とフォーマットが異なる。こちらでは :environment が "=>" で指し示されているけれど何が違うのか?
      task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
        puts "This is task2 with: #{arg1}, #{arg2}"
      end
    end
    

    🤔


    シンタックスシュガーを外す

    • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
    • 定義はどこに
    • gemの仕様

    元々の定義

      task :task1 => :environment do
        ...
      end
    
      task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
        ...
      end
    

    引数のシンタックスシュガーを外す

      task { :task1 => :environment } do
        ...
      end
    
      task :task2, { ['arg1', 'arg2'] => :environment } do |arg1, arg2|
        ...
      end
    

    メソッド呼び出しのシンタックスシュガーを外す

      task({ :task1 => :environment }) do
        ...
      end
    
      task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
        ...
      end
    

    ここまでくると、 task() メソッドにタスク名その他の引数と、ひとつのブロックを与えて呼び出している、ということがわかります。
    (引数ブロックについてはこのポストがわかりやすい)


    引数のフォーマットが異なる 🤔

    しかし、ブロックに引数がある場合とない場合で、 task() メソッドの引数のフォーマットが異なり、ここはまだよくわかりません。

      # 引数はHashのみ
      # タスク名っぽいsymbolがHashのkeyに入ってる
      task({ :task1 => :environment }) do
        ...
      end
    
      # タスク名っぽいsymbolとHash、ふたつの値を引数にとっている
      task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
        ...
      end
    

    定義を辿る

    • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
    • 定義はどこに → RubyMineのGo To Declarationを使用する
    • gemの仕様

    RubyMine

    選択した要素を右クリックして Go To -> Declaration で定義に飛んでくれます。
    namespaceやModuleをincludeしている部分も解決してくれるので優秀です。

    task() の実装を追っていくと、最終的に Rake::TaskManager.define_task() がタスク生成処理を行っているということが分かります。


    gemの実装を読む

    • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
    • 定義はどこに → RubyMineのGo To Declarationを使用する
    • gemの仕様 → コードを読む

    Rake::TaskManager.define_task() がタスク生成処理を司っているので、見てみます

        def define_task(task_class, *args, &block) # :nodoc:
          task_name, arg_names, deps = resolve_args(args) # 引数の解釈
    
          ...
    
          task_name = task_class.scope_name(@scope, task_name)
          deps = [deps] unless deps.respond_to?(:to_ary)
          deps = deps.map { |d| Rake.from_pathname(d).to_s }
          task = intern(task_class, task_name)  # task_class.new してオブジェクト生成
    
          # 以下taskオブジェクトのセットアップ
          task.set_arg_names(arg_names) unless arg_names.empty?
          ...
          task.enhance(deps, &block)
          ...
        end
    

    ここまでくるとコードを読むことができるようになり、↑の例だと resolve_args() の内容を辿ると引数の解釈部分の実装が出てきます。


    定義からわかったtaskの引数

    一行一行解説はしませんが、 resolve_args() を読んでいくと以下のようなフォーマットになっていることが分かりました

    # dependency ( `:environment` など )がない場合
    task(:task_name, :arg1_name, :arg2_name, ...) do |arg1, arg2, ...| 
      ...
    end
    
    # dependencyがあり、ブロックに引数がない場合
    task({ :task_name => [ dependency1, dependency2, ...] }) do 
      ...
    end
    
    # dependencyがあり、ブロックに引数がある場合
    task(:task_name, { [:arg1_name, :arg2_name, ...] => [ dependency1, dependency2, ...] }) do |arg1, arg2, ...|
      ...
    end
    
    # dependencyがひとつのみの場合は配列を外すことも可能
    task({ :task_name => dependency }) do 
      ...
    end
    

    まとめ

    ソースを読むために:

    • シンタックスシュガーを外す
    • メソッド定義を突き止める
    • その上で実装を読み解く