Last active
December 15, 2022 12:12
-
-
Save estum/8b0e2dc5a75e604b2bbcd0db6738e0c6 to your computer and use it in GitHub Desktop.
Materialize macro-defined method sources for documentation (ruby)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# The refinement module helps to inspect a composed source of meta-generated methods, | |
# which were defined via Module#module_eval or Module#class_eval methods | |
# in strings with interpolations. | |
# | |
# Unless this refinement is using, looking up for an actual source of such | |
# kind of method will result with a raw string literal with no interpolation applied. | |
# | |
# It's monkey patch the Module#module_eval and Module#class_eval methods to | |
# write a source with applied interpolation into a temporary file per method owner and | |
# substitutes an evaluted located path & lineno for the string. | |
# | |
# @example Usage | |
# # In each file where you want to handle this behaviour | |
# require 'mat_module_eval' | |
# using MatModuleEval | |
module MatModuleEval | |
extend Dry::Core::Cache | |
extend Dry::Core::ClassAttributes | |
defines :tmpfile_opts, coerce: Dry::Types['hash'] | |
tmpfile_opts mode: File::APPEND, | |
autoclose: true | |
TempfileBuilder = -> (basename) { Tempfile.create([basename, '.rb'], **tmpfile_opts) } | |
ModuleHandler = Struct.new(:basename, :tmp, :sources) | |
Basename = begin | |
singleton_name_pattern = /^#<Class:\K.+?(?=>$)/ | |
Dry::Transformer::Conditional[:is, Module, | |
Dry::Transformer::Conditional[:guard, proc(&:singleton_class?), | |
-> (m) { m.to_s[singleton_name_pattern] } | |
] >> -> (m) { m.to_s.underscore } | |
] | |
end | |
Constructor = | |
Basename >> | |
-> (basename, tmp = nil, sources = nil) do | |
ModuleHandler[ | |
basename, | |
tmp || TempfileBuilder[basename], | |
sources || Concurrent::Map.new | |
] | |
end | |
SOURCE_COMMENT_TPL = <<~RUBY | |
# @note Eval from %s | |
# @note Source template %p | |
%s | |
RUBY | |
HookEvalArgs = -> (base, src, *loc) do | |
handler = fetch_or_store(base) { Constructor[base] } | |
handler.sources.fetch_or_store(src.sum) do | |
expanded = format(SOURCE_COMMENT_TPL, caller(6,1)[0], loc, src) | |
lnum, lineno = expanded.lines.size, handler.tmp.lineno | |
handler.tmp.write(expanded) | |
handler.tmp.lineno = lineno + lnum | |
[expanded, handler.tmp.path, lineno.next] | |
end | |
end | |
refine ::Module do | |
def module_eval(*args, &block) = args.size > 0 ? super(*HookEvalArgs[self, *args]) : super | |
def class_eval(*args, &block) = args.size > 0 ? super(*HookEvalArgs[self, *args]) : super | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment