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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8015c1a7ab5cb06c29416d9801752700847984f780bed543684ad447adbc62a9
4
- data.tar.gz: 35f530a6eb5ce1de62d16764088939eec432e409769a78e3a2ad79f9cdcb682a
3
+ metadata.gz: 3e54cc161c036cb372e37f06ad44e852c16cca5e59a9733c2c18f790b1c94e90
4
+ data.tar.gz: 710e14885e1133eff4c47c02206859497a447399b837e76c66374088f0a89fe1
5
5
  SHA512:
6
- metadata.gz: 749783b767f55fe27e8097dd3954dc3a4bf20d430f7874e71fbc825fdbf77718592e374480e4dddb761f50718fe61370163623273a93829b58a61be5dd98f8d1
7
- data.tar.gz: 6a8d9a2e6a7ae910127bc91cc5941afabe4faf4015056117683886eb9923cfdac8b3dccf12336f302bc8781294c8bfe8bd35c4562672f3b9fc59721777d601ea
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 this code
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
- * TODO: implement placeholder for strings
163
- * TODO: implement placeholder for symbols
164
- * TODO: implement placeholder for numbers
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
- # TODO: apply_to_proc (that supports the list of local variables)
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
- first_lineno = ast.first_lineno
48
- first_column = ast.first_column
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
- 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
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
@@ -66,18 +66,75 @@ module Maccro
66
66
  end
67
67
  end
68
68
 
69
- def self.get_source_path_iseq(method)
70
- iseq ||= CodeUtil.proc_to_iseq(method)
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 ||= iseq.absolute_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 ||= File.read(path)
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
- return source, path, iseq
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)
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Maccro
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-04-19 00:00:00.000000000 Z
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