github.com
発音と命名の由来
Object(オブジェクト) + Genealogist(ジーニーアーロジスト) -> オブジーニーアーロジスト
という想定なのだけど、本当にそう発音していいかは知らない。造語です。
Genealogistは「家系、血縁関係、家系図を専門的に調査・研究する専門家」ということで、家系図=クラス継承ツリーを出力するということでこういう命名をしました。
使い方
例えばMyClassクラス があったとしてClassクラスに #to_tree メソッドが生えているのでそれを呼びます。
puts MyClass.to_tree
以下のような出力結果が得られます。
C MyClass (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:39)
│ ├ c (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:43)
│ └ singleton_c (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:44)
│
├── M M2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:7)
│ └ m2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:8)
│
├── M M1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:3)
│ └ m1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:4)
│
└── C NS::C2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:32)
│ └ c2 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:35)
│
├── M M4 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:15)
│ └ m4 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:18)
│
├── M M3 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:11)
│ └ m3 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:12)
│
└── C C1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:25)
│ └ c1 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:28)
│
├── M M5 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:21)
│ └ m5 (location: /Users/kozy4324/work/objenealogist/test/my_class.rb:22)
│
└── C Object (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:700)
├── M PP::ObjectMixin (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:348)
│ ├ pretty_print (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:362)
│ ├ pretty_print_cycle (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:379)
│ ├ pretty_print_inspect (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:402)
│ └ pretty_print_instance_variables (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:390)
│
├── M Kernel (location: /Users/kozy4324/.rbenv/versions/4.0.0/lib/ruby/gems/4.0.0/gems/pp-0.6.3/lib/pp.rb:720)
└── C BasicObject
クラス定義とメソッド定義のsource locationも出力されるのでVSCodeであれば command + click でコードジャンプできるので便利ですね。
ブログ記事上だとlocationあると見づらいのでシンプルなバージョンも貼っておきます。
> puts MyClass.to_tree(show_locations: false)
C MyClass
│ ├ c
│ └ singleton_c
│
├── M M2
│ └ m2
│
├── M M1
│ └ m1
│
└── C NS::C2
│ └ c2
│
├── M M4
│ └ m4
│
├── M M3
│ └ m3
│
└── C C1
│ └ c1
│
├── M M5
│ └ m5
│
└── C Object
├── M PP::ObjectMixin
│ ├ pretty_print
│ ├ pretty_print_cycle
│ ├ pretty_print_inspect
│ └ pretty_print_instance_variables
│
├── M Kernel
└── C BasicObject
作ったモチベーション
RailsのActiveRecordモデルのメタプログラミング事情を調べたくて作りました。例えばDBカラムに title というカラムがあった場合に #title, #title=, #title? といったメソッドがメタプログラミングによって生えるのですが、それらがクラス継承ツリー上はどこで定義されることになるメソッドなのか、また同様にメタプログラミングで生えるメソッドは他に何があるのかあたりを確認したいなとなりました。
Rubyは動的定義されたメソッドも動的に取得できる Object#methods や Module#instance_methods あたりが使えるので便利ですよね。
実際に出力した内容はgistに置きました。動的に定義されるDBカラムメソッド群は Article::GeneratedAttributeMethods に定義されているのが分かると思います(このmoduleもまた動的に定義されたmoduleです)。
https://gist.github.com/kozy4324/2e1e3c565f12329007922f2ec4092da8
実装を一工夫したところ
メソッド定義位置は Object#method で取得できるMethodオブジェクトの#source_locationから取得可能ですが、moduleやclassの定義位置の取得はAPIには見当たりませんでした。なのでメソッド定義位置から取得できるファイルパスでソースファイルを読み込み→Prismパース→ASTレベルで走査という順番でやってみました。ASTをシュッと利用できるRubyistってカッコいいですよね。私もそんなRubyistになりたい。
class ClassVisitor < Prism::Visitor
attr_reader :found
def initialize(target_class_names)
@target_class_names = target_class_names.map(&:to_s).map(&:to_sym)
@found = []
@stack = []
super()
end
def visit_class_node(node)
@stack << node.name
name = @stack.join("::").to_sym
@found << [name, node.location] if @target_class_names.include?(name)
super
@stack.pop
end
alias visit_module_node visit_class_node
end
source = File.read(path)
visitor = ClassVisitor.new(clazz.ancestors)
Prism.parse(source).value.accept(visitor)
visitor.found.each do |name, def_location|
(location_map[name] ||= []) << [path, def_location]
end
1歩進んだ使い方: 出力結果をLLMに渡す
irbやRails console上で使うことを想定して作りました。ActiveRecordモデルの継承やメソッド数になるとコピペもままならないので、お手軽にファイル出力できるようにもしておきました。
Article.to_tree >> "article_tree.txt"
この出力結果をLLMにぶん投げればいいわけですね。
$ cat article_tree.txt | claude "Articleモデルのクラスツリーで、これをベースに要点だけを絞ってクラス図を書いて"
みたいな感じで出力してもらった内容が以下の通り。悪くない。
Article Model Class Diagram
===========================
┌─────────────────────────────────────────────────────────────┐
│ Article │
├─────────────────────────────────────────────────────────────┤
│ Attributes: │
│ id, title, body, user_id, created_at, updated_at │
├─────────────────────────────────────────────────────────────┤
│ + GeneratedAssociationMethods │
│ + GeneratedAttributeMethods │
└───────────────────────────┬─────────────────────────────────┘
│ extends
▼
┌─────────────────────────────────────────────────────────────┐
│ ApplicationRecord │
└───────────────────────────┬─────────────────────────────────┘
│ extends
▼
┌─────────────────────────────────────────────────────────────┐
│ ActiveRecord::Base │
├─────────────────────────────────────────────────────────────┤
│ 主要な include モジュール: │
│ │
│ [永続化・CRUD] │
│ ├─ Persistence (save, destroy, update, reload...) │
│ ├─ Callbacks (before_save, after_create...) │
│ └─ Transactions (transaction, with_lock...) │
│ │
│ [バリデーション] │
│ ├─ Validations (valid?, validate!, errors) │
│ └─ ActiveModel::Validations │
│ │
│ [属性・アクセス] │
│ ├─ AttributeMethods ([], []=, attributes, has_attribute?) │
│ │ ├─ Read/Write (read_attribute, write_attribute) │
│ │ ├─ Dirty (changed?, saved_changes?) │
│ │ └─ PrimaryKey (id, id=, to_key) │
│ └─ Attributes │
│ │
│ [関連付け] │
│ ├─ Associations (association, belongs_to, has_many) │
│ ├─ AutosaveAssociation │
│ └─ NestedAttributes │
│ │
│ [クエリ・スコープ] │
│ ├─ Scoping (default_scope, scope) │
│ └─ Inheritance (STI support) │
│ │
│ [ロック] │
│ ├─ Locking::Optimistic (楽観的ロック) │
│ └─ Locking::Pessimistic (lock!, with_lock) │
│ │
│ [その他] │
│ ├─ Turbo::Broadcastable (broadcast_append, refresh...) │
│ ├─ ActiveStorage::Attached::Model (attachment_changes) │
│ ├─ GlobalID::Identification (to_gid, to_sgid) │
│ ├─ Integration (cache_key, to_param) │
│ └─ Serialization (to_json, as_json) │
└───────────────────────────┬─────────────────────────────────┘
│ extends
▼
┌─────────────────────────────────────────────────────────────┐
│ Object │
│ (ActiveSupport拡張: blank?, present?, try, in?, etc.) │
└─────────────────────────────────────────────────────────────┘
主要なメソッドの由来まとめ
==========================
| 機能 | モジュール |
|----------------------------|------------------------------|
| save, destroy, update | Persistence |
| valid?, errors | Validations |
| title, body, user_id | GeneratedAttributeMethods |
| changed?, saved_changes? | AttributeMethods::Dirty |
| before_save, after_create | Callbacks |
| transaction, with_lock | Transactions |
| broadcast_append | Turbo::Broadcastable |
| to_gid, to_sgid | GlobalID::Identification |
まとめ
Railsのソースコードリーディングや内部実装を探るお供にobjenealogistというgemを作ってみました。LLMと連携する前提でこういったツール作るのも悪くないです。