Is there a way for programmatically accessing a method comments? or an attribute comments?
I would like to use it as a description for the method in a documentation which I don't want to be static or generated with rdoc or equivalent.
Here is an example of a Ruby class:
Class MyClass
##
# This method tries over and over until it is tired
def go_go_go(thing_to_try, tries = 10) # :args: thing_to_try
puts thing_to_try
go_go_go thing_to_try, tries - 1
end
end
Basically, I'd like to be able to do the following:
get_comment MyClass.gogogo # => This method tries over and over until it is tired
No, you cannot do this.
The whole point of comments is that they are not part of the program! If you want a string that is part of your program, just use a string instead.
In most Ruby implementations, comments already get thrown away in the lexer, which means they don't even reach the parser, let alone the interpreter or compiler. At the time the code gets run, the comments are long gone … In fact, in implementations like Rubinius or YARV which use a compiler, there is simply no way to store the comments in the compiled executable, so even if they weren't thrown away by the lexer or the parser, there would still be no way to communicate them to the runtime.
So, pretty much your only chance is to parse the Ruby sourcefile to extract the comments. However, like I mentioned above, you cannot just take any parser, because most of the exisiting parsers throw comments away. (Which, again, is the whole point of comments, so there's nothing wrong with the parser throwing them away.) There are, however, Ruby parsers which preserve comments, most notably the ones used in tools such as RDoc or YARD.
YARD is especially interesting, because it also contains a query engine, which lets you search for and filter out documentation based on some powerful predicates like class name, method name, YARD tags, API version, type signature and so on.
However, if you do end up using RDoc or YARD for parsing, then why not use them in the first place?
Or, like I suggested above, if you want strings, just use strings:
module MethodAddedHook
private
def method_added(meth)
(@__doc__ ||= {})[meth] = @__last_doc__ if @__last_doc__
@__last_doc__ = nil
super
end
end
class Module
private
prepend MethodAddedHook
def doc(meth=nil, str)
return @__doc__[meth] = str if meth
@__last_doc__ = str
end
def defdoc(meth, doc, &block)
@__doc__[meth] = doc
define_method(meth, &block)
end
end
This gives us a method Module#doc
which we can use to document either an already existing method by calling it with the name of the method and a docstring, or you can use it to document the very next method you define. It does this by storing the docstring in a temporary instance variable and then defining a method_added
hook that looks at that instance variable and stores its content in the documentation hash.
There is also the Module#defdoc
method which defines and documents the method in one go.
module Kernel
private
def get_doc(klass, meth)
klass.instance_variable_get(:@__doc__)[meth]
end
end
This is your Kernel#get_doc
method which gets the documentation back out (or nil
if the method is undocumented).
class MyClass
doc 'This method tries over and over until it is tired'
def go_go_go(thing_to_try, tries = 10)
puts thing_to_try
go_go_go thing_to_try, tries - 1
end
def some_other_meth; end # Oops, I forgot to document it!
# No problem:
doc :some_other_meth, 'Does some other things'
defdoc(:yet_another_method, 'This method also does something') do |a, b, c|
p a, b, c
end
end
Here you see the three different ways of documenting a method.
Oh, and it works:
require 'test/unit'
class TestDocstrings < Test::Unit::TestCase
def test_that_myclass_gogogo_has_a_docstring
doc = 'This method tries over and over until it is tired'
assert_equal doc, get_doc(MyClass, :go_go_go)
end
def test_that_myclass_some_other_meth_has_a_docstring
doc = 'Does some other things'
assert_equal doc, get_doc(MyClass, :some_other_meth)
end
def test_that_myclass_yet_another_method_has_a_docstring
doc = 'This method also does something'
assert_equal doc, get_doc(MyClass, :yet_another_method)
end
def test_that_undocumented_methods_return_nil
assert_nil get_doc(MyClass, :does_not_exist)
end
end
Note: this is pretty hacky. For example, there is no locking, so if two threads define methods for the same class at the same time, the documentation might get screwed up. (I.e.: the docstring might be attributed to the wrong method or get lost.)
I believe that rake
does essentially the same thing with its desc
method, and that codebase is much better tested than this, so if you intend to use it in production, I'd steal Jim's code instead of mine.
Comments are (usually) thrown away by the lexer and are not available in the symbol tables to Ruby at execution time.
I think the closest that you could do is to either
(a) Implement get_comment in such a way that it creates a regex on the fly and searches the source file for a match. You'd need to change your syntax like this ...
get_comment :MyClass, :go_go_go
You would convert the symbols to strings, assume that the source file is myclass.rb and search therein for a match on the comment-def-method_name pattern.
(b) Have a method called from every source file which built a global comment table.
Regardless, it's messy and more hassle than it's worth.
Meanwhile, there is a "standard" gem method_source
that solves some of those issues:
https://github.com/banister/method_source
Set.instance_method(:merge).comment
Set.instance_method(:merge).source
It also comes with recent Rails (railties >= 5.0) versions and is used by Pry under the hood.