Using Built-in Emacs 29 Tree Sitter Package to Get Qualified Ruby Function Name

A Rainy Day Coding Practice Session

When I’m writing about or in Ruby 📖 code, sometimes I want to grab the qualified method name. For example, let’s say I have the following Ruby code:

module Hello
  module World
    def foo
      :bar
    end

    def self.call
      :yup
    end
  end
end

The qualified method name for the method #foo would be Hello::World#foo. The qualified method name for the singleton method .call is Hello::World.call. A Ruby documentation convention is that instance methods are prefix with a # and singleton methods are prefixed with a . or ::.

Using treesit-explore-mode, I was able to quickly refine my recursive queries. Below is treesit’s rendering of the Abstract Syntax Tree (AST 📖) of the above Ruby code:

(program
 (module module name: (constant)
  (body_statement
   (module module name: (constant)
    (body_statement
     (method def body: (identifier)
      (body_statement (simple_symbol))
      end)
     (singleton_method def object: (self) . body: (identifier)
      (body_statement (simple_symbol))
      end))
    body: end))
  body: end))

, in a moment of dreary skies and sleeping dogs, I hacked together the following functions:

jf/treesit/qualified_method_name
Copy the qualified method name to the paste buffer (e.g. the kill-ring).
jf/treesit/module_space
Recurse up from a node to create a list of the module/class ancestors.
(require 'treesit)
(cl-defun jf/treesit/qualified_method_name (&key (type "method"))
  "Get the fully qualified name of method at point."
  (interactive)
  (if-let ((func (treesit-defun-at-point)))
      ;; Instance method or class method?
      (let* ((method_type (if (string= type
                                       (treesit-node-type func))
                              "#" "."))
             (method_name (treesit-node-text
                           (car (treesit-filter-child
                                 func
                                 (lambda (node)
                                   (string=
                                    "identifier"
                                    (treesit-node-type node)))))))
             (module_space (s-join "::"
                                   (-flatten
                                    (jf/treesit/module_space func))))
             (qualified_name (concat module_space
                                     method_type
                                     method_name)))
        (message qualified_name)
        (kill-new (substring-no-properties qualified_name)))
    (user-error "No %s at point." type)))

;; An ugly bit of code to recurse upwards from the node to the "oldest"
;; parent.  And collect all module/class nodes along the way. This will
;; return a series of nested lists.  It's on the originating caller to
;; flatten that list.
(defun jf/treesit/module_space (node)
  (when-let* ((parent (treesit-parent-until
                       node
                       (lambda (n) (member (treesit-node-type n)
                                           '("class" "module")))))
              (parent_name (treesit-node-text
                            (car (treesit-filter-child
                                  parent (lambda (n)
                                           (string=
                                            "constant"
                                            (treesit-node-type n))))))))
    (list (jf/treesit/module_space parent) parent_name)))

This is most certainly a rainy day kind of project; one that helped me learn just a bit more about the treesit package.

Postscript

The list returned by jf/treesit/module_space is '(nil ("Hello" ("World"))); which is a ugly but workable. Perhaps someone will write to me with a refactor of this code.