maccro 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +65 -9
- data/lib/maccro.rb +50 -37
- data/lib/maccro/code_util.rb +62 -5
- data/lib/maccro/impl.rb +34 -0
- data/lib/maccro/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e54cc161c036cb372e37f06ad44e852c16cca5e59a9733c2c18f790b1c94e90
|
4
|
+
data.tar.gz: 710e14885e1133eff4c47c02206859497a447399b837e76c66374088f0a89fe1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60a16df8c0f7c4411a31bb3485b92fb3a1c851ce5a8ac099989a318c770a4a3e6cc098b01aef89dd98c1dfe32ffb99f1fbc7569b93e4ce15da429227c058e5e3
|
7
|
+
data.tar.gz: 348bad681d4495b8b61529a8b936e0f03d88bd85e841ea41e0a39706e5452d7e484267271707c83b7d6ecd8a3f1c88aab5c2140baed3c572a74f52081b32837b
|
data/README.md
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
Maccro is a library to introduce macro (dynamic code rewriting), written in Ruby 100%.
|
4
4
|
|
5
5
|
```ruby
|
6
|
+
require "maccro"
|
6
7
|
# name, before, after
|
7
8
|
Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
|
8
9
|
|
9
|
-
# This rewrites
|
10
|
+
# This rewrites the code below
|
10
11
|
class Foo
|
11
12
|
def foo(v)
|
12
13
|
if 1 < v < 2
|
@@ -27,6 +28,41 @@ class Foo
|
|
27
28
|
end
|
28
29
|
```
|
29
30
|
|
31
|
+
Another example is about ActiveRecord queries.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require "maccro/builtin"
|
35
|
+
Maccro::Builtin.register(:activerecord_utilities)
|
36
|
+
Maccro.enable(path: __FILE__)
|
37
|
+
|
38
|
+
# This rewrites the code below
|
39
|
+
class Users < ApplicationRecord
|
40
|
+
def not_admin_or_under_20_age_users
|
41
|
+
Users.where(:priv != "admin || :age < 20)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# To this automatically
|
46
|
+
class Users < ApplicationRecord
|
47
|
+
def not_admin_or_under_20_age_users
|
48
|
+
Users.where('priv != ?', "admin").or(Users.where('age < ?', 20))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Maccro also provides methods to rewrite any code in blocks:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
require "maccro"
|
57
|
+
Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
|
58
|
+
|
59
|
+
does_sandwitch_one = ->(a, b){ a < 1 < b }
|
60
|
+
Maccro.rewrite(does_sandwitch_one).call(0, 2) #=> true
|
61
|
+
|
62
|
+
# or execute the block parameter immediately
|
63
|
+
Maccro.execute{ 0 < 1 < 2 } #=> true
|
64
|
+
```
|
65
|
+
|
30
66
|
Maccro comes from "Macro" and "Makkuro"(means "pure black" in Japanese).
|
31
67
|
|
32
68
|
### Why Maccro?
|
@@ -40,15 +76,13 @@ Maccro comes from "Macro" and "Makkuro"(means "pure black" in Japanese).
|
|
40
76
|
Maccro can:
|
41
77
|
|
42
78
|
* run with Ruby 2.6 or later
|
43
|
-
* rewrite code, only written in methods using `def` keyword, in `module` or `class
|
79
|
+
* rewrite code, only written in methods using `def` keyword, in `module` or `class`, or blocks
|
44
80
|
* not rewrite singleton methods, which are used just after definition
|
45
81
|
* not rewrite methods from command line option (`-e`) or REPLs (irb/pry)
|
46
82
|
|
47
83
|
Maccro features below are not supported yet:
|
48
84
|
|
49
85
|
* Non-idempotent method calls
|
50
|
-
* Proc rewrite and local variable name matching (currntly, local variable name in before/after could be referred as VCALL)
|
51
|
-
* Specifying a type of literal by placeholders
|
52
86
|
* Handling method visibilities
|
53
87
|
* Rewriting singleton methods with non-self receiver
|
54
88
|
* Placeholder validation
|
@@ -59,7 +93,7 @@ Maccro features below are not supported yet:
|
|
59
93
|
Maccro users do:
|
60
94
|
* register rules how to rewrite methods, with code patterns
|
61
95
|
* or use built-in rules
|
62
|
-
* apply a set of registered rules to a method
|
96
|
+
* apply a set of registered rules to a method, or to a block
|
63
97
|
* enable automatic applying to a module/class or to a file, or globally
|
64
98
|
|
65
99
|
### Terminology
|
@@ -158,10 +192,10 @@ Types of placeholders are defined by alphabetic chars:
|
|
158
192
|
* defining methods and singleton methods
|
159
193
|
* double and trible colon `::` and `:::`
|
160
194
|
* dots and flip-flop
|
161
|
-
|
162
|
-
*
|
163
|
-
*
|
164
|
-
*
|
195
|
+
* `s`: strings
|
196
|
+
* `y`: symbols
|
197
|
+
* `n`: numbers
|
198
|
+
* `r`: regular expressions
|
165
199
|
|
166
200
|
#### Using a placeholder twice (or more)
|
167
201
|
|
@@ -327,6 +361,8 @@ $ ruby -rmaccro/rewrite_the_world file_to_run.rb
|
|
327
361
|
|
328
362
|
#### `Maccro#register(name, before, after, **kwarg_options)`
|
329
363
|
|
364
|
+
Register a macro rule to the global dictionary.
|
365
|
+
|
330
366
|
* name: a symbol to represents the rule
|
331
367
|
* before: a string of Ruby code which matches to be rewritten
|
332
368
|
* after: a string of Ruby code which replaces the matched part
|
@@ -336,11 +372,31 @@ $ ruby -rmaccro/rewrite_the_world file_to_run.rb
|
|
336
372
|
|
337
373
|
#### `Maccro#apply(module, method, **kwarg_options)`
|
338
374
|
|
375
|
+
Apply the registered rules (or a set of rules specified) to a method.
|
376
|
+
|
339
377
|
* module: a module/class, the applied method is defined in
|
340
378
|
* method: a method object (an instance method or a singleton method)
|
341
379
|
* kwarg_options:
|
342
380
|
* rules: an array of symbols of rule names (default: all registered rules)
|
343
381
|
|
382
|
+
#### `Maccro#rewrite(proc=nil, **kwarg_options, &block)`
|
383
|
+
|
384
|
+
Rewrite a specified proc object by the registered rules (or a set of rules specified) and return the updated (rewritten) proc object.
|
385
|
+
|
386
|
+
* proc: a Proc object to be rewritten (exclusive with `block`)
|
387
|
+
* block: a block parameter to be rewritten (exclusive with `proc`)
|
388
|
+
* kwarg_options:
|
389
|
+
* rules: an array of symbols of rule names (default: all registered rules)
|
390
|
+
|
391
|
+
#### `Maccro#execute(proc=nil, **kwarg_options, &block)`
|
392
|
+
|
393
|
+
Rewrite a specified proc object and call it immediately. The proc must be with no arguments.
|
394
|
+
|
395
|
+
* proc: a Proc object to be rewritten (exclusive with `block`), which must be with no arguments
|
396
|
+
* block: a block parameter to be rewritten (exclusive with `proc`), which must be with no arguments
|
397
|
+
* kwarg_options:
|
398
|
+
* rules: an array of symbols of rule names (default: all registered rules)
|
399
|
+
|
344
400
|
#### `Maccro#enable(**kwarg_options)`
|
345
401
|
|
346
402
|
* kwarg_options:
|
data/lib/maccro.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative "./maccro/version"
|
2
2
|
|
3
|
+
require_relative "./maccro/impl"
|
3
4
|
require_relative "./maccro/dsl"
|
4
5
|
require_relative "./maccro/rule"
|
5
6
|
require_relative "./maccro/code_util"
|
@@ -21,10 +22,46 @@ module Maccro
|
|
21
22
|
@@dic[name] = Rule.new(name, before, after, under: under, safe_reference: safe_reference)
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
+
def self.clear!
|
26
|
+
@@dic = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.execute(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param)
|
30
|
+
block = block_param if !block && block_param
|
31
|
+
if block.arity > 0
|
32
|
+
raise "Block with parameters can't be executed via Maccro"
|
33
|
+
end
|
34
|
+
|
35
|
+
rewrite(block, rules: rules, verbose: verbose, get_code: get_code).call
|
36
|
+
end
|
37
|
+
|
38
|
+
# Maccro.rewrite(->(){ ... }).call(args)
|
39
|
+
def self.rewrite(block=nil, rules: @@dic, verbose: false, get_code: false, &block_param)
|
40
|
+
block = block_param if !block && block_param
|
41
|
+
if !block.source_location
|
42
|
+
raise "Native block can't be rewritten"
|
43
|
+
end
|
44
|
+
|
45
|
+
ast = CodeUtil.proc_to_ast(block)
|
46
|
+
if !ast
|
47
|
+
raise "Failed to load AST nodes - source file may be invisible"
|
48
|
+
end
|
49
|
+
source, _ = CodeUtil.get_source_path(block)
|
50
|
+
ast, source = Impl.update_by_rules(ast, source, rules) do |src, lineno, column|
|
51
|
+
CodeUtil.get_proc_node(CodeUtil.parse_to_ast(src), lineno, column)
|
52
|
+
end
|
53
|
+
eval_source = if ast.type == :SCOPE
|
54
|
+
CodeUtil.convert_scope_to_lambda(CodeRange.from_node(ast).get(source))
|
55
|
+
else
|
56
|
+
CodeRange.from_node(ast).get(source)
|
57
|
+
end
|
58
|
+
return eval_source if get_code
|
59
|
+
puts eval_source if verbose
|
60
|
+
block.binding.eval(eval_source)
|
61
|
+
end
|
25
62
|
|
63
|
+
# Maccro.apply(X, X.instance_method(:yay), verbose: true)
|
26
64
|
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
65
|
if !method.source_location
|
29
66
|
raise "Native method can't be redefined"
|
30
67
|
end
|
@@ -40,53 +77,29 @@ module Maccro
|
|
40
77
|
end
|
41
78
|
# This node should be SCOPE node (just under DEFN or DEFS)
|
42
79
|
# But its code range is equal to code range of DEFN/DEFS
|
43
|
-
CodeUtil.extend_tree_with_wrapper(ast)
|
44
|
-
|
45
80
|
is_singleton_method = (mojule != method.owner)
|
46
81
|
|
47
|
-
|
48
|
-
|
82
|
+
source, path = CodeUtil.get_source_path(method)
|
83
|
+
# The reason to get the entire source code is to capture/rewrite
|
84
|
+
# the exact code snippet using CodeRange (positions in the entire file)
|
49
85
|
|
50
|
-
iseq = nil
|
51
|
-
path = nil
|
52
|
-
source = nil
|
53
86
|
rewrite_method_code_range = nil
|
54
87
|
|
55
|
-
|
56
|
-
|
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
|
88
|
+
ast, source = Impl.update_by_rules(ast, source, rules) do |src, lineno, column|
|
89
|
+
CodeUtil.get_method_node(CodeUtil.parse_to_ast(src), method.name, lineno, column, singleton_method: is_singleton_method)
|
82
90
|
end
|
83
91
|
|
92
|
+
# required to restore code positions of the method definition
|
93
|
+
first_lineno = ast.first_lineno
|
94
|
+
first_column = ast.first_column
|
95
|
+
|
96
|
+
rewrite_method_code_range = CodeRange.from_node(ast)
|
84
97
|
if source && path && rewrite_method_code_range
|
85
98
|
eval_source = (" " * first_column) + rewrite_method_code_range.get(source) # restore the original indentation
|
86
99
|
return eval_source if get_code
|
87
100
|
puts eval_source if verbose
|
88
101
|
CodeUtil.suppress_warning do
|
89
|
-
mojule.module_eval(eval_source, path, first_lineno)
|
102
|
+
mojule.module_eval(eval_source, path, ast.first_lineno)
|
90
103
|
end
|
91
104
|
end
|
92
105
|
end
|
data/lib/maccro/code_util.rb
CHANGED
@@ -66,18 +66,75 @@ module Maccro
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
-
def self.
|
70
|
-
|
69
|
+
def self.convert_scope_to_lambda(scope_source)
|
70
|
+
raise "Scope source must start with '{'" unless scope_source.start_with?('{')
|
71
|
+
raise "Scope source must end with '}'" unless scope_source.end_with?('}')
|
72
|
+
|
73
|
+
if m = scope_source.match(/^\{\s*\|(.*)\|/o)
|
74
|
+
matched_source = m[0]
|
75
|
+
args_source = m[1]
|
76
|
+
return "->(#{args_source})" + scope_source.sub(matched_source, '{')
|
77
|
+
end
|
78
|
+
|
79
|
+
"->" + scope_source
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.get_source_path(block)
|
83
|
+
iseq = CodeUtil.proc_to_iseq(block)
|
71
84
|
if !iseq
|
72
85
|
raise "Native methods can't be redefined"
|
73
86
|
end
|
74
|
-
path
|
87
|
+
path = iseq.absolute_path
|
75
88
|
if !path # STDIN or -e
|
76
89
|
raise "Methods from stdin or -e can't be redefined"
|
77
90
|
end
|
78
|
-
source
|
91
|
+
source = File.read(path)
|
92
|
+
|
93
|
+
return source, path
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.get_proc_node(node, lineno, column)
|
97
|
+
return nil unless node.type == :SCOPE
|
98
|
+
dig_proc_node(node, lineno, column)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.dig_proc_node(node, lineno, column)
|
102
|
+
return nil unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
103
|
+
is_target_scope = ->(n){ n.type == :SCOPE && n.first_lineno == lineno && n.first_column == column }
|
104
|
+
|
105
|
+
case node.type
|
106
|
+
when :LAMBDA # ->(){ }
|
107
|
+
if is_target_scope.call(node.children[0])
|
108
|
+
return node
|
109
|
+
end
|
110
|
+
when :ITER # method call with block (iterator?)
|
111
|
+
# lambda{}, proc{}
|
112
|
+
if node.children[0].type == :FCALL \
|
113
|
+
&& (node.children[0].children[0] == :lambda || node.children[0].children[0] == :proc) \
|
114
|
+
&& is_target_scope.call(node.children[1])
|
115
|
+
return node
|
116
|
+
# Kernel.lambda, Kernel.proc{}, Proc.new{}
|
117
|
+
elsif node.children[0].type == :CALL \
|
118
|
+
&& node.children[0].children[0].type == :CONST \
|
119
|
+
&& (node.children[0].children[0].children[0] == :Kernel && (node.children[0].children[1] == :lambda || node.children[0].children[1] == :proc) \
|
120
|
+
|| node.children[0].children[0].children[0] == :Proc && node.children[0].children[1] == :new ) \
|
121
|
+
&& is_target_scope.call(node.children[1])
|
122
|
+
return node
|
123
|
+
end
|
124
|
+
when :SCOPE # for block parameters
|
125
|
+
if is_target_scope.call(node)
|
126
|
+
return node
|
127
|
+
end
|
128
|
+
end
|
79
129
|
|
80
|
-
|
130
|
+
if node.respond_to?(:children)
|
131
|
+
node.children.each do |n|
|
132
|
+
r = dig_proc_node(n, lineno, column)
|
133
|
+
return r if r
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
nil
|
81
138
|
end
|
82
139
|
|
83
140
|
def self.get_method_node(node, method_name, lineno, column, singleton_method: false)
|
data/lib/maccro/impl.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Maccro
|
2
|
+
module Impl
|
3
|
+
# for internal use
|
4
|
+
def self.update_by_rules(ast, source, rules)
|
5
|
+
CodeUtil.extend_tree_with_wrapper(ast)
|
6
|
+
|
7
|
+
rewrite_happens = false
|
8
|
+
first_time = true
|
9
|
+
|
10
|
+
while rewrite_happens || first_time
|
11
|
+
rewrite_happens = false
|
12
|
+
first_time = false
|
13
|
+
|
14
|
+
try_once = ->(rule) {
|
15
|
+
matched = rule.match(ast)
|
16
|
+
next unless matched
|
17
|
+
|
18
|
+
source = matched.rewrite(source)
|
19
|
+
ast = yield source, ast.first_lineno, ast.first_column
|
20
|
+
CodeUtil.extend_tree_with_wrapper(ast)
|
21
|
+
rewrite_happens = true
|
22
|
+
try_once.call(rule)
|
23
|
+
}
|
24
|
+
|
25
|
+
rules.each_pair do |_name, this_rule|
|
26
|
+
try_once.call(this_rule)
|
27
|
+
break if rewrite_happens # to retry all rules
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return ast, source
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/maccro/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: maccro
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TAGOMORI Satoshi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- lib/maccro/dsl/literal.rb
|
77
77
|
- lib/maccro/dsl/node.rb
|
78
78
|
- lib/maccro/dsl/value.rb
|
79
|
+
- lib/maccro/impl.rb
|
79
80
|
- lib/maccro/kernel_ext.rb
|
80
81
|
- lib/maccro/matched.rb
|
81
82
|
- lib/maccro/rewrite_the_world.rb
|