objenealogistというgemを作った

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

作ったモチベーション

RailsActiveRecordモデルのメタプログラミング事情を調べたくて作りました。例えばDBカラムに title というカラムがあった場合に #title, #title=, #title? といったメソッドがメタプログラミングによって生えるのですが、それらがクラス継承ツリー上はどこで定義されることになるメソッドなのか、また同様にメタプログラミングで生えるメソッドは他に何があるのかあたりを確認したいなとなりました。

Rubyは動的定義されたメソッドも動的に取得できる Object#methodsModule#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に渡す

irbRails 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と連携する前提でこういったツール作るのも悪くないです。