maccro 0.1.0 → 0.2.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 +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
|