Created
February 28, 2024 09:10
-
-
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
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
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