Module: Maccro

Defined in:
lib/maccro.rb,
lib/maccro/dsl.rb,
lib/maccro/impl.rb,
lib/maccro/rule.rb,
lib/maccro/builtin.rb,
lib/maccro/matched.rb,
lib/maccro/version.rb,
lib/maccro/dsl/node.rb,
lib/maccro/code_util.rb,
lib/maccro/dsl/value.rb,
lib/maccro/code_range.rb,
lib/maccro/dsl/assign.rb,
lib/maccro/dsl/literal.rb,
lib/maccro/dsl/expression.rb

Defined Under Namespace

Modules: Builtin, CodeUtil, DSL, Impl Classes: CodeRange, Match, Matched, Rule

Constant Summary collapse

VERSION =
"0.2.0"
@@dic =
{}
@@trace_global =
nil

Class Method Summary collapse

Class Method Details

.apply(mojule, method, rules: @@dic, verbose: false, from_trace: false, get_code: false) ⇒ Object

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



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 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!Object



25
26
27
# File 'lib/maccro.rb', line 25

def self.clear!
  @@dic = {}
end

.enable(target: nil, path: nil, rules: nil) ⇒ Object

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



109
110
111
112
113
114
115
116
117
118
# 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) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# 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) ⇒ Object



29
30
31
32
33
34
35
36
# 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) ⇒ Object



14
15
16
17
18
19
20
21
22
23
# 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) ⇒ Object

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



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 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