Skip to content

Instantly share code, notes, and snippets.

@estum
Last active December 15, 2022 12:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save estum/8b0e2dc5a75e604b2bbcd0db6738e0c6 to your computer and use it in GitHub Desktop.
Save estum/8b0e2dc5a75e604b2bbcd0db6738e0c6 to your computer and use it in GitHub Desktop.
Materialize macro-defined method sources for documentation (ruby)
# 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