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 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