Skip to content

Instantly share code, notes, and snippets.

@baweaver
Created February 28, 2024 09:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baweaver/8f63fe7e143bc4737d4f7ad721785d27 to your computer and use it in GitHub Desktop.
Save baweaver/8f63fe7e143bc4737d4f7ad721785d27 to your computer and use it in GitHub Desktop.
Ruby AST translator hack - Needs a lot of work later, but hey, proof of concept
require 'rubocop'
# Target - What I want to get to
# class ShorthandProcCallMacro2 < Rule
# matches "$receiver.$method_call { |$arg| $arg.$arg_name }"
# replaces "$receiver.$method_call(&:$arg_name)"
# end
class Literal
def initialize(s)
@s = s
end
def to_s = @s.to_s
def inspect = to_s # Remove parens
end
def ast_from(string)
processed_source_from(string).ast
end
def processed_source_from(string)
RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
end
def deep_deconstruct(node)
return node unless node.respond_to?(:deconstruct)
node.deconstruct.map { deep_deconstruct(_1) }
end
def pattern_deconstruct(node, tokens: [], seen: Hash.new(0), is_head: false)
if node.respond_to?(:deconstruct)
# Receivers especially tend to get nested like this
if node in [:send, nil, potential_token]
return Literal.new(potential_token) if tokens.include?(potential_token)
end
node.deconstruct.each_with_index.map do |node, i|
pattern_deconstruct(node, tokens:, seen:, is_head: i.zero?)
end
else
return node if node.nil? || is_head || !tokens.include?(node)
seen[node] += 1
is_repeated = seen[node] > 1
Literal.new("#{'^' if is_repeated}#{node}")
end
end
def create_translator(input_string:, target_string:)
# Sort is a stupid hack to beat partial word matches. Should make this smarter
# later on. Probably invoke this into a tree rewriter ruleset which
# is far more involved than I want to do tonight
input_tokens = input_string.scan(/\$\w+/).sort_by { -_1.size }
input_token_match = Regexp.union(input_tokens)
input_clean = input_string.gsub(input_token_match) { |v| v[1..-1] }
input_ast = ast_from(input_clean)
# Strip off the global-psuedo-var syntax
input_scan_tokens = input_tokens.map { _1[1..-1].to_sym }
pattern_match_stanza = pattern_deconstruct(input_ast, tokens: input_scan_tokens)
# Interpolate the new values
#
# Don't mind the really danged hacky AST to source coercion here,
# need to think on cleaning that up real fast later.
target_source = target_string.gsub(input_token_match) do |v|
"\#\{#{v[1..-1]}.then { |x| x.is_a?(RuboCop::AST::Node) ? x.source : x }\}"
end
extractor_source = <<~RUBY
-> node do
node = node.is_a?(String) ? ast_from(node) : node
return unless node in #{pattern_match_stanza}
"#{target_source}"
end
RUBY
puts extractor_source
eval(extractor_source)
end
input_string = "$receiver.$method_call { |$arg| $arg.$arg_name }"
target_string = "$receiver.$method_call(&:$arg_name)"
translator = create_translator(input_string:, target_string:)
translator.call("[1, 2, 3, 4].select { |v| v.even? }")
# => "[1, 2, 3, 4].select(&:even?)"
translator.call("[1, 2, 3, 4].map { |v| v + 2 }")
# => nil
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment