module Maccro

Constants

Match
VERSION

Public Class Methods

apply(mojule, method, rules: @@dic, verbose: false, from_trace: false, get_code: false) click to toggle source

Maccro.apply(X, X.instance_method(:yay), verbose: true)

# File lib/maccro.rb, line 64
def self.apply(mojule, method, rules: @@dic, verbose: false, from_trace: false, get_code: false)
  if !method.source_location
    raise "Native method can't be redefined"
  end

  ast = CodeUtil.proc_to_ast(method)
  if !ast
    if from_trace
      # unknown and unexpected loaded ruby code (which many not have visible source)
      return
    else
      raise "Failed to load AST nodes - source file may be invisible: #{method}"
    end
  end
  # This node should be SCOPE node (just under DEFN or DEFS)
  # But its code range is equal to code range of DEFN/DEFS
  is_singleton_method = (mojule != method.owner)

  source, path = CodeUtil.get_source_path(method)
  # The reason to get the entire source code is to capture/rewrite
  # the exact code snippet using CodeRange (positions in the entire file)

  rewrite_method_code_range = nil

  ast, source = Impl.update_by_rules(ast, source, rules) do |src, lineno, column|
    CodeUtil.get_method_node(CodeUtil.parse_to_ast(src), method.name, lineno, column, singleton_method: is_singleton_method)
  end

  # required to restore code positions of the method definition
  first_lineno = ast.first_lineno
  first_column = ast.first_column

  rewrite_method_code_range = CodeRange.from_node(ast)
  if source && path && rewrite_method_code_range
    eval_source = (" " * first_column) + rewrite_method_code_range.get(source) # restore the original indentation
    return eval_source if get_code
    puts eval_source if verbose
    CodeUtil.suppress_warning do
      mojule.module_eval(eval_source, path, ast.first_lineno)
    end
  end
end
clear!() click to toggle source
# File lib/maccro.rb, line 25
def self.clear!
  @@dic = {}
end
enable(target: nil, path: nil, rules: nil) click to toggle source

TODO: check visibility: private method is still private method even after module_eval?

# File lib/maccro.rb, line 109
def self.enable(target: nil, path: nil, rules: nil)
  if target || path
    enable_trace(target: target, path: path, rule_names: rules)
  else
    if rules
      raise "Cannot enable globally with specific rules"
    end
    enable_trace(globally: true)
  end
end
enable_trace(target: nil, path: nil, globally: false, rule_names: nil) click to toggle source
# File lib/maccro.rb, line 120
def self.enable_trace(target: nil, path: nil, globally: false, rule_names: nil)
  if globally && @@trace_global
    return nil
  end

  if rule_names
    rules = rule_names.map{|n| [n, @@dic[n]] }.to_h
  else
    rules = @@dic
  end

  trace = TracePoint.new(:end) do |tp|
    current_location = tp.path
    next unless globally || target == tp.self || path == current_location

    this = tp.self

    methods = (
      this.instance_methods(false).map{|m| this.instance_method(m) } +
      this.private_instance_methods(false).map{|m| this.instance_method(m) } +

      # NameError: undefined singleton method `provides?' for `Bundler::RubygemsIntegration::Legacy'
      this.singleton_methods.map{|m| this.singleton_method(m) rescue nil }.compact
    )

    methods.each do |method|
      source_location = method.source_location
      next if !source_location # native method
      next if source_location.first == '-e'
      next if source_location.first != current_location # methods defined in other file
      Maccro.apply(this, method, rules: rules, from_trace: true)
    end
  end

  if globally
    @@trace_global = trace
  end
  trace.enable
  nil
end
execute(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param) click to toggle source
# File lib/maccro.rb, line 29
def self.execute(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param)
  block = block_param if !block && block_param
  if block.arity > 0
    raise "Block with parameters can't be executed via Maccro"
  end

  rewrite(block, rules: rules, verbose: verbose, get_code: get_code).call
end
register(name, before, after, under: nil, safe_reference: false) click to toggle source
# File lib/maccro.rb, line 14
def self.register(name, before, after, under: nil, safe_reference: false)
  # Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
  # Maccro.register(:double_greater_than, 'e1 > e2 > e3', 'e1 > e2 && e2 > e3')
  # Maccro.register(:double_greater_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3', safe_reference: true)
  # Maccro.register(:activerecord_where_equal, 'v1 = v2', 'v1 => v2', under: 'e.where($TARGET)')
  if safe_reference
    raise NotImplementedError, "TODO: implement it"
  end      
  @@dic[name] = Rule.new(name, before, after, under: under, safe_reference: safe_reference)
end
rewrite(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param) click to toggle source

Maccro.rewrite(->(){ … }).call(args)

# File lib/maccro.rb, line 39
def self.rewrite(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param)
  block = block_param if !block && block_param
  if !block.source_location
    raise "Native block can't be rewritten"
  end

  ast = CodeUtil.proc_to_ast(block)
  if !ast
    raise "Failed to load AST nodes - source file may be invisible"
  end
  source, _ = CodeUtil.get_source_path(block)
  ast, source = Impl.update_by_rules(ast, source, rules) do |src, lineno, column|
    CodeUtil.get_proc_node(CodeUtil.parse_to_ast(src), lineno, column)
  end
  eval_source = if ast.type == :SCOPE
                  CodeUtil.convert_scope_to_lambda(CodeRange.from_node(ast).get(source))
                else
                  CodeRange.from_node(ast).get(source)
                end
  return eval_source if get_code
  puts eval_source if verbose
  block.binding.eval(eval_source)
end