maccro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +359 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/maccro.rb +147 -0
- data/lib/maccro/builtin.rb +105 -0
- data/lib/maccro/code_range.rb +63 -0
- data/lib/maccro/code_util.rb +106 -0
- data/lib/maccro/dsl.rb +182 -0
- data/lib/maccro/dsl/assign.rb +100 -0
- data/lib/maccro/dsl/expression.rb +166 -0
- data/lib/maccro/dsl/literal.rb +121 -0
- data/lib/maccro/dsl/node.rb +89 -0
- data/lib/maccro/dsl/value.rb +85 -0
- data/lib/maccro/kernel_ext.rb +15 -0
- data/lib/maccro/matched.rb +34 -0
- data/lib/maccro/rewrite_the_world.rb +4 -0
- data/lib/maccro/rule.rb +106 -0
- data/lib/maccro/version.rb +3 -0
- data/maccro.gemspec +27 -0
- metadata +108 -0
data/lib/maccro.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require_relative "./maccro/version"
|
2
|
+
|
3
|
+
require_relative "./maccro/dsl"
|
4
|
+
require_relative "./maccro/rule"
|
5
|
+
require_relative "./maccro/code_util"
|
6
|
+
|
7
|
+
require_relative "./maccro/kernel_ext"
|
8
|
+
|
9
|
+
module Maccro
|
10
|
+
@@dic = {}
|
11
|
+
@@trace_global = nil
|
12
|
+
|
13
|
+
def self.register(name, before, after, under: nil, safe_reference: false)
|
14
|
+
# Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
|
15
|
+
# Maccro.register(:double_greater_than, 'e1 > e2 > e3', 'e1 > e2 && e2 > e3')
|
16
|
+
# Maccro.register(:double_greater_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3', safe_reference: true)
|
17
|
+
# Maccro.register(:activerecord_where_equal, 'v1 = v2', 'v1 => v2', under: 'e.where($TARGET)')
|
18
|
+
if safe_reference
|
19
|
+
raise NotImplementedError, "TODO: implement it"
|
20
|
+
end
|
21
|
+
@@dic[name] = Rule.new(name, before, after, under: under, safe_reference: safe_reference)
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: apply_to_proc (that supports the list of local variables)
|
25
|
+
|
26
|
+
def self.apply(mojule, method, rules: @@dic, verbose: false, from_trace: false, get_code: false)
|
27
|
+
# Maccro.apply(X, X.instance_method(:yay), verbose: true)
|
28
|
+
if !method.source_location
|
29
|
+
raise "Native method can't be redefined"
|
30
|
+
end
|
31
|
+
|
32
|
+
ast = CodeUtil.proc_to_ast(method)
|
33
|
+
if !ast
|
34
|
+
if from_trace
|
35
|
+
# unknown and unexpected loaded ruby code (which many not have visible source)
|
36
|
+
return
|
37
|
+
else
|
38
|
+
raise "Failed to load AST nodes - source file may be invisible: #{method}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
# This node should be SCOPE node (just under DEFN or DEFS)
|
42
|
+
# But its code range is equal to code range of DEFN/DEFS
|
43
|
+
CodeUtil.extend_tree_with_wrapper(ast)
|
44
|
+
|
45
|
+
is_singleton_method = (mojule != method.owner)
|
46
|
+
|
47
|
+
first_lineno = ast.first_lineno
|
48
|
+
first_column = ast.first_column
|
49
|
+
|
50
|
+
iseq = nil
|
51
|
+
path = nil
|
52
|
+
source = nil
|
53
|
+
rewrite_method_code_range = nil
|
54
|
+
|
55
|
+
rewrite_happens = false
|
56
|
+
first_time = true
|
57
|
+
|
58
|
+
while rewrite_happens || first_time
|
59
|
+
rewrite_happens = false
|
60
|
+
first_time = false
|
61
|
+
|
62
|
+
rules.each_pair do |_name, this_rule|
|
63
|
+
try_once = ->(rule) {
|
64
|
+
matched = rule.match(ast)
|
65
|
+
next unless matched
|
66
|
+
|
67
|
+
if !source || !path || !iseq
|
68
|
+
source, path, iseq = CodeUtil.get_source_path_iseq(method)
|
69
|
+
end
|
70
|
+
|
71
|
+
source = matched.rewrite(source)
|
72
|
+
ast = CodeUtil.get_method_node(CodeUtil.parse_to_ast(source), method.name, first_lineno, first_column, singleton_method: is_singleton_method)
|
73
|
+
CodeUtil.extend_tree_with_wrapper(ast)
|
74
|
+
rewrite_method_code_range = CodeRange.from_node(ast)
|
75
|
+
rewrite_happens = true
|
76
|
+
try_once.call(this_rule)
|
77
|
+
}
|
78
|
+
try_once.call(this_rule)
|
79
|
+
|
80
|
+
break if rewrite_happens # to retry all rules
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if source && path && rewrite_method_code_range
|
85
|
+
eval_source = (" " * first_column) + rewrite_method_code_range.get(source) # restore the original indentation
|
86
|
+
return eval_source if get_code
|
87
|
+
puts eval_source if verbose
|
88
|
+
CodeUtil.suppress_warning do
|
89
|
+
mojule.module_eval(eval_source, path, first_lineno)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# TODO: check visibility: private method is still private method even after module_eval?
|
95
|
+
|
96
|
+
def self.enable(target: nil, path: nil, rules: nil)
|
97
|
+
if target || path
|
98
|
+
enable_trace(target: target, path: path, rule_names: rules)
|
99
|
+
else
|
100
|
+
if rules
|
101
|
+
raise "Cannot enable globally with specific rules"
|
102
|
+
end
|
103
|
+
enable_trace(globally: true)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.enable_trace(target: nil, path: nil, globally: false, rule_names: nil)
|
108
|
+
if globally && @@trace_global
|
109
|
+
return nil
|
110
|
+
end
|
111
|
+
|
112
|
+
if rule_names
|
113
|
+
rules = rule_names.map{|n| [n, @@dic[n]] }.to_h
|
114
|
+
else
|
115
|
+
rules = @@dic
|
116
|
+
end
|
117
|
+
|
118
|
+
trace = TracePoint.new(:end) do |tp|
|
119
|
+
current_location = tp.path
|
120
|
+
next unless globally || target == tp.self || path == current_location
|
121
|
+
|
122
|
+
this = tp.self
|
123
|
+
|
124
|
+
methods = (
|
125
|
+
this.instance_methods(false).map{|m| this.instance_method(m) } +
|
126
|
+
this.private_instance_methods(false).map{|m| this.instance_method(m) } +
|
127
|
+
|
128
|
+
# NameError: undefined singleton method `provides?' for `Bundler::RubygemsIntegration::Legacy'
|
129
|
+
this.singleton_methods.map{|m| this.singleton_method(m) rescue nil }.compact
|
130
|
+
)
|
131
|
+
|
132
|
+
methods.each do |method|
|
133
|
+
source_location = method.source_location
|
134
|
+
next if !source_location # native method
|
135
|
+
next if source_location.first == '-e'
|
136
|
+
next if source_location.first != current_location # methods defined in other file
|
137
|
+
Maccro.apply(this, method, rules: rules, from_trace: true)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
if globally
|
142
|
+
@@trace_global = trace
|
143
|
+
end
|
144
|
+
trace.enable
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "maccro"
|
2
|
+
|
3
|
+
module Maccro
|
4
|
+
module Builtin
|
5
|
+
RULES = {
|
6
|
+
# [before, after (, options)]
|
7
|
+
# options:
|
8
|
+
# * under
|
9
|
+
# * safe_reference
|
10
|
+
|
11
|
+
# continuing less/greater-than or equal-to
|
12
|
+
less_than_2: ['e1 < e2 < e3', '(e1 < e2 && e2 < e3)'],
|
13
|
+
less_than_or_equal_to_2: ['e1 <= e2 <= e3', '(e1 <= e2 && e2 <= e3)'],
|
14
|
+
less_than_and_equal_to_a: ['e1 <= e2 < e3', '(e1 <= e2 && e2 < e3)'],
|
15
|
+
less_than_and_equal_to_b: ['e1 < e2 <= e3', '(e1 < e2 && e2 <= e3)'],
|
16
|
+
greater_than_2: ['e1 > e2 > e3', '(e1 > e2 && e2 > e3)'],
|
17
|
+
greater_than_or_equal_to_2: ['e1 >= e2 >= e3', '(e1 >= e2 && e2 >= e3)'],
|
18
|
+
greater_than_and_equal_to_a: ['e1 >= e2 > e3', '(e1 >= e2 && e2 > e3)'],
|
19
|
+
greater_than_and_equal_to_b: ['e1 > e2 >= e3', '(e1 > e2 && e2 >= e3)'],
|
20
|
+
|
21
|
+
# mathematic intervals
|
22
|
+
open_interval: ['e1 < e2 < e3', '(e1 < e2 && e2 < e3)'],
|
23
|
+
closed_interval: ['e1 <= e2 <= e3', '(e1 <= e2 && e2 <= e3)'],
|
24
|
+
left_closed_interval: ['e1 <= e2 < e3', '(e1 <= e2 && e2 < e3)'],
|
25
|
+
right_closed_interval: ['e1 < e2 <= e3', '(e1 < e2 && e2 <= e3)'],
|
26
|
+
|
27
|
+
# ActiveRecord utilities
|
28
|
+
ar_where_in_range_exclusive: ['e1 <= y1 < e2', '{y1 => [(e1)...(e2)]}', {under: 'e1.where($TARGET)'}],
|
29
|
+
ar_where_in_range_inclusive: ['e1 <= y1 <= e2', '{y1 => [(e1)..(e2)]}', {under: 'e1.where($TARGET)'}],
|
30
|
+
ar_where_equal_to: ['y1 == e1', '{y1 => e1}', {under: 'e1.where($TARGET)'}],
|
31
|
+
ar_where_not_equal_to: ['y1 != e1', '["#{y1} != ?", e1]', {under: 'e1.where($TARGET)'}],
|
32
|
+
ar_where_larger_than: ['y1 > e1', '["#{y1} > ?", e1]', {under: 'e1.where($TARGET)'}],
|
33
|
+
ar_where_larger_than_or_equal_to: ['y1 >= e1', '["#{y1} >= ?", e1]', {under: 'e1.where($TARGET)'}],
|
34
|
+
ar_where_less_than: ['y1 < e1', '["#{y1} < ?", e1]', {under: 'e1.where($TARGET)'}],
|
35
|
+
ar_where_less_than_or_equal_to: ['y1 <= e1', '["#{y1} <= ?", e1]', {under: 'e1.where($TARGET)'}],
|
36
|
+
# TODO: and-or mixed query
|
37
|
+
ar_and_chain_5: ['e1.where((e2 && e3 and e4 and e5 and e6 and e7))', 'e1.where(e2).where(e3).where(e4).where(e5).where(e6).where(e7)'],
|
38
|
+
ar_and_chain_4: ['e1.where((e2 and e3 and e4 and e5 and e6))', 'e1.where(e2).where(e3).where(e4).where(e5).where(e6)'],
|
39
|
+
ar_and_chain_3: ['e1.where((e2 and e3 and e4 and e5))', 'e1.where(e2).where(e3).where(e4).where(e5)'],
|
40
|
+
ar_and_chain_2: ['e1.where((e2 and e3 and e4))', 'e1.where(e2).where(e3).where(e4)'],
|
41
|
+
ar_and_chain_1: ['e1.where((e2 and e3))', 'e1.where(e2).where(e3)'],
|
42
|
+
ar_or_chain_5: ['e1.where((e2 or e3 or e4 or e5 or e6 or e7))', 'e1.where(e2).or(e1.where(e3)).or(e1.where(e4)).or(e1.where(e5)).or(e1.where(e6)).or(e1.where(e7))'],
|
43
|
+
ar_or_chain_4: ['e1.where((e2 or e3 or e4 or e5 or e6))', 'e1.where(e2).or(e1.where(e3)).or(e1.where(e4)).or(e1.where(e5)).or(e1.where(e6))'],
|
44
|
+
ar_or_chain_3: ['e1.where((e2 or e3 or e4 or e5))', 'e1.where(e2).or(e1.where(e3)).or(e1.where(e4)).or(e1.where(e5))'],
|
45
|
+
ar_or_chain_2: ['e1.where((e2 or e3 or e4))', 'e1.where(e2).or(e1.where(e3)).or(e1.where(e4))'],
|
46
|
+
ar_or_chain_1: ['e1.where((e2 or e3))', 'e1.where(e2).or(e1.where(e3))'],
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
RULE_GROUPS = {
|
50
|
+
inequality_operators: [
|
51
|
+
:less_than_2, :less_than_or_equal_to_2, :less_than_and_equal_to_a, :less_than_and_equal_to_b,
|
52
|
+
:greater_than_2, :greater_than_or_equal_to_2, :greater_than_and_equal_to_a, :greater_than_and_equal_to_b,
|
53
|
+
],
|
54
|
+
|
55
|
+
mathematic_intervals: [:open_interval, :closed_interval, :left_closed_interval, :right_closed_interval],
|
56
|
+
|
57
|
+
activerecord_utilities: [
|
58
|
+
:ar_where_in_range_exclusive, :ar_where_in_range_inclusive,
|
59
|
+
:ar_where_equal_to, :ar_where_not_equal_to,
|
60
|
+
:ar_where_larger_than, :ar_where_larger_than_or_equal_to, :ar_where_less_than, :ar_where_less_than_or_equal_to,
|
61
|
+
:ar_and_chain_5, :ar_and_chain_4, :ar_and_chain_3, :ar_and_chain_2, :ar_and_chain_1,
|
62
|
+
:ar_or_chain_5, :ar_or_chain_4, :ar_or_chain_3, :ar_or_chain_2, :ar_or_chain_1,
|
63
|
+
]
|
64
|
+
}.freeze
|
65
|
+
|
66
|
+
def self.rule(name)
|
67
|
+
return nil unless RULES.has_key?(name)
|
68
|
+
before, after, options = RULES.fetch(name)
|
69
|
+
options ||= {}
|
70
|
+
Rule.new(name, before, after, under: options.fetch(:under, nil), safe_reference: options.fetch(:safe_reference, false))
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.rules(*names)
|
74
|
+
rules = {}
|
75
|
+
names.each do |name|
|
76
|
+
if RULES.has_key?(name)
|
77
|
+
rules[name] = rule(name)
|
78
|
+
elsif RULE_GROUPS.has_key?(name)
|
79
|
+
RULE_GROUPS[name].each do |n|
|
80
|
+
rules[n] = rule(n)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rules
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.register(name)
|
88
|
+
if RULES.has_key?(name)
|
89
|
+
before, after, options = RULES.fetch(name)
|
90
|
+
options ||= {}
|
91
|
+
Maccro.register(name, before, after, under: options.fetch(:under, nil), safe_reference: options.fetch(:safe_reference, false))
|
92
|
+
elsif RULE_GROUPS.has_key?(name)
|
93
|
+
RULE_GROUPS[name].each do |rule_name|
|
94
|
+
register(rule_name)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.register_all
|
100
|
+
RULES.each_key do |name|
|
101
|
+
register(name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'code_util'
|
2
|
+
|
3
|
+
module Maccro
|
4
|
+
class CodeRange
|
5
|
+
def self.from_node(ast)
|
6
|
+
CodeRange.new(ast.first_lineno, ast.first_column, ast.last_lineno, ast.last_column)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :first_lineno, :first_column, :last_lineno, :last_column
|
10
|
+
|
11
|
+
def initialize(first_lineno, first_column, last_lineno, last_column)
|
12
|
+
@first_lineno = first_lineno
|
13
|
+
@first_column = first_column
|
14
|
+
@last_lineno = last_lineno
|
15
|
+
@last_column = last_column
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
@first_lineno == other.first_lineno &&
|
20
|
+
@first_column == other.first_column &&
|
21
|
+
@last_lineno == other.last_lineno &&
|
22
|
+
@last_column == other.last_column
|
23
|
+
end
|
24
|
+
|
25
|
+
def <=>(other)
|
26
|
+
if @first_lineno < other.first_lineno
|
27
|
+
-1
|
28
|
+
elsif @first_lineno == other.first_lineno
|
29
|
+
if @first_column < other.first_column
|
30
|
+
-1
|
31
|
+
elsif @first_column == other.first_column
|
32
|
+
if @last_lineno < other.last_lineno
|
33
|
+
-1
|
34
|
+
elsif @last_lineno == other.last_lineno
|
35
|
+
if @last_column < other.last_column
|
36
|
+
-1
|
37
|
+
elsif @last_column == other.last_column
|
38
|
+
0
|
39
|
+
else # @last_column > other.last_column
|
40
|
+
1
|
41
|
+
end
|
42
|
+
else # @last_lineno > other.last_lineno
|
43
|
+
1
|
44
|
+
end
|
45
|
+
else # @first_column > other.first_column
|
46
|
+
1
|
47
|
+
end
|
48
|
+
else # @first_lineno > other.first_lineno
|
49
|
+
1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def source(path)
|
54
|
+
source = File.open(path){|f| f.read } # open as binary?
|
55
|
+
get(source)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get(source)
|
59
|
+
range = CodeUtil.code_range_to_range(source, self)
|
60
|
+
source[range]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Maccro
|
2
|
+
module CodeUtil
|
3
|
+
def self.code_position_to_index(source, lineno, column)
|
4
|
+
source_lines = source.lines # including newline at the end of line
|
5
|
+
if source_lines.size < lineno
|
6
|
+
raise "too few lines for specified position: lineno:#{lineno}, column:#{column}"
|
7
|
+
end
|
8
|
+
counter = 1
|
9
|
+
index = 0
|
10
|
+
while counter < lineno
|
11
|
+
index += source_lines.shift.size
|
12
|
+
counter += 1
|
13
|
+
end
|
14
|
+
if source_lines.empty?
|
15
|
+
raise "too few lines for specified position: lineno:#{lineno}, column:#{column}"
|
16
|
+
end
|
17
|
+
# column is 0 origin
|
18
|
+
if source_lines.first.size < 1
|
19
|
+
raise "empty line at the end of source"
|
20
|
+
end
|
21
|
+
if source_lines.first.size < column
|
22
|
+
raise "too few chars in the line for specified position: lineno:#{lineno}, column:#{column}"
|
23
|
+
end
|
24
|
+
return index + column
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.code_range_to_range(source, code_range)
|
28
|
+
begin_index = code_position_to_index(source, code_range.first_lineno, code_range.first_column)
|
29
|
+
end_index = code_position_to_index(source, code_range.last_lineno, code_range.last_column)
|
30
|
+
Range.new(begin_index, end_index, true) # exclude end char
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.code_range_to_code(source, code_range)
|
34
|
+
source[code_range_to_range(source, code_range)]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.suppress_warning
|
38
|
+
v = $VERBOSE
|
39
|
+
$VERBOSE = nil
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
$VERBOSE = v
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.parse_to_ast(code)
|
46
|
+
suppress_warning do
|
47
|
+
RubyVM::AbstractSyntaxTree.parse(code)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.proc_to_ast(block)
|
52
|
+
suppress_warning do
|
53
|
+
RubyVM::AbstractSyntaxTree.of(block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.proc_to_iseq(block)
|
58
|
+
RubyVM::InstructionSequence.of(block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.extend_tree_with_wrapper(tree)
|
62
|
+
return unless tree.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
63
|
+
tree.extend Maccro::DSL::ASTNodeWrapper unless tree.is_a?(Maccro::DSL::ASTNodeWrapper)
|
64
|
+
tree.children.each do |c|
|
65
|
+
extend_tree_with_wrapper(c)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.get_source_path_iseq(method)
|
70
|
+
iseq ||= CodeUtil.proc_to_iseq(method)
|
71
|
+
if !iseq
|
72
|
+
raise "Native methods can't be redefined"
|
73
|
+
end
|
74
|
+
path ||= iseq.absolute_path
|
75
|
+
if !path # STDIN or -e
|
76
|
+
raise "Methods from stdin or -e can't be redefined"
|
77
|
+
end
|
78
|
+
source ||= File.read(path)
|
79
|
+
|
80
|
+
return source, path, iseq
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.get_method_node(node, method_name, lineno, column, singleton_method: false)
|
84
|
+
if singleton_method
|
85
|
+
# TODO: consider receiver filter
|
86
|
+
# 0: (SELF@57:6-57:10)
|
87
|
+
dig_method_node(node, :DEFS, 1, method_name, lineno, column)
|
88
|
+
else
|
89
|
+
dig_method_node(node, :DEFN, 0, method_name, lineno, column)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.dig_method_node(node, def_type, method_name_index, method_name, lineno, column)
|
94
|
+
return nil unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
95
|
+
if node.type == def_type && node.children[method_name_index] == method_name && node.first_lineno == lineno && node.first_column == column
|
96
|
+
return node
|
97
|
+
elsif node.respond_to?(:children)
|
98
|
+
node.children.each do |n|
|
99
|
+
r = dig_method_node(n, def_type, method_name_index, method_name, lineno, column)
|
100
|
+
return r if r
|
101
|
+
end
|
102
|
+
end
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/maccro/dsl.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require_relative 'dsl/node'
|
2
|
+
require_relative 'dsl/literal'
|
3
|
+
require_relative 'dsl/value'
|
4
|
+
require_relative 'dsl/assign'
|
5
|
+
require_relative 'dsl/expression'
|
6
|
+
require_relative 'code_util'
|
7
|
+
|
8
|
+
module Maccro
|
9
|
+
module DSL
|
10
|
+
def self.matcher(code_snippet)
|
11
|
+
ast = CodeUtil.parse_to_ast(code_snippet)
|
12
|
+
# Top level node should be SCOPE, and children[2] will be the first expression node
|
13
|
+
return ast_node_to_dsl_node(ast.children[2])
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.ast_node_to_dsl_node(ast_node)
|
17
|
+
unless ast_node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
18
|
+
if ast_node.is_a?(Array)
|
19
|
+
ast_node.times do |i|
|
20
|
+
ast_node[i] = ast_node_to_dsl_node(ast_node[i])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
return ast_node
|
24
|
+
end
|
25
|
+
|
26
|
+
# ast_node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
27
|
+
ast_node.extend ASTNodeWrapper
|
28
|
+
if is_placeholder?(ast_node)
|
29
|
+
return placeholder_to_matcher_node(ast_node)
|
30
|
+
end
|
31
|
+
|
32
|
+
ast_node.children.each_with_index do |n, i|
|
33
|
+
ast_node.children[i] = ast_node_to_dsl_node(n)
|
34
|
+
end
|
35
|
+
ast_node
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.is_placeholder?(node)
|
39
|
+
if node.type == :VCALL && placeholder_name?(node.children.first)
|
40
|
+
true
|
41
|
+
elsif node.type == :GVAR && node.children.first == :'$TARGET'
|
42
|
+
true
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.placeholder_name?(sym)
|
49
|
+
# Expression: "eN"
|
50
|
+
# Value: "vN"
|
51
|
+
# String: "sN"
|
52
|
+
# Symbol: "yN"
|
53
|
+
# Number: "nN"
|
54
|
+
# Regular expression: "rN"
|
55
|
+
# N index is 1 origin
|
56
|
+
(sym.to_s =~ /^[evsynr][1-9]\d*$/).!.!
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.placeholder_to_matcher_node(placeholder_node)
|
60
|
+
name = placeholder_node.children.first.to_s
|
61
|
+
nodeClass = case name
|
62
|
+
when '$TARGET' then AnyNode
|
63
|
+
when /^s([1-9]\d*)$/ then String
|
64
|
+
when /^y([1-9]\d*)$/ then Symbol
|
65
|
+
when /^n([1-9]\d*)$/ then Number
|
66
|
+
when /^r([1-9]\d*)$/ then RegularExpression
|
67
|
+
when /^v([1-9]\d*)$/ then Value
|
68
|
+
when /^e([1-9]\d*)$/ then Expression
|
69
|
+
else
|
70
|
+
raise "BUG: unregistered placeholder name `#{name}`"
|
71
|
+
end
|
72
|
+
nodeClass.new(name, placeholder_node.to_code_range)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
###
|
78
|
+
# List of AST nodes: from node_children() in ast.c
|
79
|
+
|
80
|
+
# ASGN means Assignment
|
81
|
+
|
82
|
+
# BLOCK
|
83
|
+
# IF (expression)
|
84
|
+
# UNLESS (expression)
|
85
|
+
# CASE (expression)
|
86
|
+
# CASE2 (expression)
|
87
|
+
# WHEN
|
88
|
+
# WHILE
|
89
|
+
# UNTIL
|
90
|
+
# ITER
|
91
|
+
# FOR
|
92
|
+
# FOR_MASGN
|
93
|
+
# BREAK
|
94
|
+
# NEXT
|
95
|
+
# RETURN
|
96
|
+
# REDO
|
97
|
+
# RETRY
|
98
|
+
# BEGIN (expression)
|
99
|
+
# RESCUE
|
100
|
+
# RESBODY
|
101
|
+
# ENSURE
|
102
|
+
# AND (expression)
|
103
|
+
# OR (expression)
|
104
|
+
# MASGN (assign)
|
105
|
+
# LASGN (assign)
|
106
|
+
# DASGN (assign)
|
107
|
+
# DASGN_CUPR (assign)
|
108
|
+
# IASGN (assign)
|
109
|
+
# CVASGN (assign)
|
110
|
+
# GASGN (assign)
|
111
|
+
# CDECL (assign)
|
112
|
+
# OP_ASGN1 (assign)
|
113
|
+
# OP_ASGN2 (assign)
|
114
|
+
# OP_ASGN_AND (assign)
|
115
|
+
# OP_ASGN_OR (assign)
|
116
|
+
# OP_CDECL (assign)
|
117
|
+
# CALL (expression)
|
118
|
+
# OPCALL (expression)
|
119
|
+
# QCALL (expression)
|
120
|
+
# FCALL (expression)
|
121
|
+
# VCALL (expression)
|
122
|
+
# SUPER (expression)
|
123
|
+
# ZSUPER (expression)
|
124
|
+
# ARRAY (value)
|
125
|
+
# VALUES [return arguments]
|
126
|
+
# ZARRAY (value)
|
127
|
+
# HASH (value)
|
128
|
+
# YIELD (expression)
|
129
|
+
# LVAR (value)
|
130
|
+
# DVAR (value)
|
131
|
+
# IVAR (value)
|
132
|
+
# CONST (value)
|
133
|
+
# CVAR (value)
|
134
|
+
# GVAR (value)
|
135
|
+
# NTH_REF (value)
|
136
|
+
# BACK_REF (value)
|
137
|
+
# MATCH (expression)
|
138
|
+
# MATCH2 (expression)
|
139
|
+
# MATCH3 (expression)
|
140
|
+
# LIT (value)
|
141
|
+
# STR (value)
|
142
|
+
# XSTR (value)
|
143
|
+
# ONCE (value)
|
144
|
+
# DSTR (value)
|
145
|
+
# DXSTR (value)
|
146
|
+
# DREGX (value)
|
147
|
+
# DSYM (value)
|
148
|
+
# EVSTR [String interpolation (in string literals)]
|
149
|
+
# ARGSCAT
|
150
|
+
# AGSPUSH
|
151
|
+
# SPLAT
|
152
|
+
# BLOCK_PASS
|
153
|
+
# DEFN (expression)
|
154
|
+
# DEFS (expression)
|
155
|
+
# ALIAS
|
156
|
+
# VALIAS
|
157
|
+
# UNDEF
|
158
|
+
# CLASS
|
159
|
+
# MODULE
|
160
|
+
# SCLASS
|
161
|
+
# COLON2 (expression)
|
162
|
+
# COLON3 (expression)
|
163
|
+
# DOT2 (expression)
|
164
|
+
# DOT3 (expression)
|
165
|
+
# FLIP2 (expression)
|
166
|
+
# FLIP3 (expression)
|
167
|
+
# SELF (value)
|
168
|
+
# NIL (value)
|
169
|
+
# TRUE (value)
|
170
|
+
# FALSE (value)
|
171
|
+
# ERRINFO
|
172
|
+
# DEFINED (expression)
|
173
|
+
# POSTEXE
|
174
|
+
# ATTRASGN (assign)
|
175
|
+
# LAMBDA (value)
|
176
|
+
# OPT_ARG
|
177
|
+
# KW_ARG
|
178
|
+
# POSTARG
|
179
|
+
# ARGS
|
180
|
+
# SCOPE
|
181
|
+
# ARGS_AUX
|
182
|
+
# LAST
|