TIL about TracePoint
!
To expand on my original problem, I had a superclass I wanted to extend many times:
class BaseClass
end
I wanted all extended versions of the class to have a FRIENDLY_NAME
constant. When specified explicitly in the subclass, that would be the value for the constant. EG:
class Subclass1 < BaseClass
FRIENDLY_NAME = 'Bob seems like a friendly name!'
end
Thus:
pry(main)> Subclass1::FRIENDLY_NAME
=> "Bob seems like a friendly name!"
However, maybe I have another subclass where I don't define the constant. EG:
class Subclass2 < BaseClass
end
This is what I'd get without the constant (obviously):
pry(main)> Subclass2::FRIENDLY_NAME
NameError: uninitialized constant Subclass2::FRIENDLY_NAME
from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:79:in `block in load_missing_constant'
Caused by NameError: uninitialized constant Subclass2::FRIENDLY_NAME
from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:60:in `block in load_missing_constant'
How to add that constant? You can add self.inherited
to the superclass. This method is called each time the superclass is extended. There's also a method on Module
named const_set
, so I could automatically set the constant on the child like this:
class BaseClass
def self.inherited(child)
child.const_set(:FRIENDLY_NAME, child.name)
end
end
This works just fine for Subclass2
:
pry(main)> Subclass2::FRIENDLY_NAME
=> "Subclass2"
However, it produces ugly warnings for Subclass1
(pry):10: warning: already initialized constant Subclass1::FRIENDLY_NAME
(pry):3: warning: previous definition of FRIENDLY_NAME was here
Initially I tried using the const_defined?
method to check to see if the FRIENDLY_NAME
constant was already defined. The problem is, the self.inherited
method is invoked before the constant is defined in the subclass. Basically, as soon as Ruby see < BaseClass
it runs self.inherited
.
Eventually I found this SO question about how to execute code after a class has been defined. The answer there was to use TracePoint
.
TracePoint
is a class that allows us to handle events in our code. These could be when a line of code is executed, when we call a method, when an error is raised, etc. What we care about is :end
event, which happens when a class or module definition is finished.
Knowing that, we can update our self.inherited
method like this:
class BaseClass
def self.inherited(child)
TracePoint.trace(:end) do |t|
if child == t.self
unless child.const_defined?(:FRIENDLY_NAME)
child.const_set(:FRIENDLY_NAME, child.code)
end
end
end
end
end
Now, both Subclass1
and Subclass2
have a FRIENDLY_NAME
constant, but neither produce warnings!