Ruby
Rails

Rakeタスクの定義を追う

はじめに

最近業務で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

まとめ

ソースを読むために:

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