Rakeタスクの定義を追う

# はじめに

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

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

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

---

# Rakeタスク


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

```rb
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タスク


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

```rb
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シンタックスシュガー → シンタックスシュガーを外した場合を考える
- <font color=lightgray>定義はどこに</font>
- <font color=lightgray>gemの仕様</font>


---

# 元々の定義

```rb
  task :task1 => :environment do
    ...
  end

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

---

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

```rb
  task { :task1 => :environment } do
    ...
  end

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

---

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

```rb
  task({ :task1 => :environment }) do
    ...
  end

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

ここまでくると、 `task()` メソッドにタスク名その他の引数と、ひとつのブロックを与えて呼び出している、ということがわかります。
（引数ブロックについては[このポスト](https://yakst.com/ja/posts/1634)がわかりやすい）

---

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

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

```rb
  # 引数はHashのみ
  # タスク名っぽいsymbolがHashのkeyに入ってる
  task({ :task1 => :environment }) do
    ...
  end

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

---

# 定義を辿る

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

---

# RubyMine

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

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

---

# gemの実装を読む

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

---

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

```rb
    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()` を読んでいくと以下のようなフォーマットになっていることが分かりました

```rb
# 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
```

---

# まとめ

ソースを読むために：

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