maccro 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|