ripper-plus 1.1.0.pre2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,31 @@
1
1
  # ripper-plus
2
2
 
3
- Ripper is the Ruby parser library packaged with Ruby 1.9. While quite complete, it has some quirks that can make use frustrating. This gem intends to correct them. The most important is bareword resolution in Ruby ASTs.
3
+ Ripper is the Ruby parser library packaged with Ruby 1.9. While quite complete, it still has bugs (2 of which I've patched alone while working on Laser, with the fixes targeted for 1.9.3: [mlhs-splats](http://redmine.ruby-lang.org/issues/4364) and [words/qwords](http://redmine.ruby-lang.org/issues/4365)), and it has higher-level quirks that can make use frustrating. ripper-plus is a gem intended to demonstrate what I believe the correct output from Ripper should be, *with the goal that these changes become the standard Ripper behavior*. I do *not* want to invent nor maintain a new AST standard. I do however believe these changes warrant separate implementation and discussion before a potential integration with Ripper, and creating a small library to demonstrate the proposed output seemed the simplest approach. Plus, [Laser](https://github.com/michaeledgar/laser/) needs all these improvements, so I had to do it anyway.
4
4
 
5
- ## Bareword Resolution
5
+ NB: Ripper is a SAX-style parser; one can construct an AST from the parser events however one wishes. Ripper also has a convenience method `Ripper.sexp`, which generates an Array-based AST directly from the SAX events. I personally use `Ripper.sexp` in my work, so the examples will be of `Ripper.sexp` output. All of the discussion below reflects deficiencies in the underlying SAX parser: they are unavoidable whether one uses `Ripper.sexp` or not.
6
+
7
+ ## tl;dr Version
8
+
9
+ If you don't want to read everything, here's what you should take away:
10
+
11
+ 1. Ripper does not distinguish between reading a local variable and implicit-self, no-paren, no-argument method calls. Due to Ruby's semantics, this must be done by the parser. Example: `y = foo`. Is foo a method call? Ripper does not tell you.
12
+ 2. Ruby's Bison grammar rejects some invalid syntax not by restricting the grammar, but by parsing with a more permissive grammar and then validating the results. Ripper does report some of these errors in its parse tree, but not all of them, and sometimes inconveniently deep in the parse tree. Example: `proc { |a, a| }` is a syntax error, but Ripper parses it without flinching.
13
+
14
+ ## Still Here?
15
+
16
+ If you use Ripper for anything important, I'll assume you've gotten this far. Quick info about `ripper-plus`: it is a gem I hope will correct the following issues until they are addressed in Ripper proper. To use `ripper-plus`, install it with `gem install ripper-plus`. The gem has two entry points:
17
+
18
+ require 'ripper-plus'
19
+ # Returns an AST from Ripper, modified as described below
20
+ RipperPlus.sexp(any_input_object_Ripper_accepts)
21
+ # Transforms a Ripper AST into a RipperPlus AST, as described below
22
+ RipperPlus.for_ripper_ast(some_ripper_ast)
23
+ # Transforms a Ripper AST into a RipperPlus AST, in-place
24
+ RipperPlus.for_ripper_ast(some_ripper_ast, :in_place => true)
25
+
26
+ That's it - now read about why exactly ripper-plus exists.
27
+
28
+ # Bareword Resolution
6
29
 
7
30
  In Ruby, a local variable is created when it is assigned to. If Ruby finds a bareword `foo` before an assignment `foo = 5`, then Ruby will call `self.foo()`.
8
31
 
@@ -10,6 +33,7 @@ Kind of.
10
33
 
11
34
  What happens when you run this code?
12
35
 
36
+
13
37
  def label
14
38
  'hello'
15
39
  end
@@ -19,92 +43,162 @@ What happens when you run this code?
19
43
  end
20
44
  print_label
21
45
 
46
+
22
47
  If one takes the intuitive approach, one would assume it prints "Label: hello", right? Afraid not: it errors out. The intuitive approach says that `'Label: ' + label` is run before the assignment to the local variable `label`, so the `label` method will be called and appended to `"Label: "`. In reality, it won't, because local variables aren't created on assignment. Local variables are created *immediately when the Ruby parser parses a left-hand-side local variable*. Before the right-hand-side is even parsed, the local variable has been created. This also results in an infamous anomaly:
23
48
 
49
+ [code]
24
50
  def say_what?
25
51
  x = 'hello' unless defined?(x)
26
52
  puts x
27
53
  end
28
- say_what?
54
+ [/code]
29
55
 
30
56
  `say_what?` prints a blank line! This is because as soon as the `x` on the LHS is parsed, `x` is a local variable with value `nil`. By the time `defined?(x)` is executed, `x` has long since been a local variable!
31
57
 
32
- ## Ripper's Bareword Behavior
58
+ This is further complicated by the many ways a local variable can be introduced:
59
+
60
+ 1. Single- or multiple-assignment (including sub-assignment) anywhere an expression is allowed
61
+ 2. A `for` loop's iterator variable(s)
62
+ 3. Formal arguments to a method
63
+ 4. Block arguments and block-local variables
64
+ 5. Stabby Lambda variables
65
+ 6. Rescue exception naming (`rescue StandardError => err`)
66
+ 7. Named Regexp captures in literal matches (`/name: (?<person>\w+)/ =~ 'name: Mike'` creates a local variable named "person" with value "Mike")
67
+
68
+ I think that's a complete list, but I may be mistaken. In fact, I had forgotten named captures until I double-checked this list while writing this blog post. I tried to find all paths in `parse.y` to a call to `assignable()`, the C function in Ruby's parser that creates local variable in the current scope, and I think I caught them all, but I may have slipped up.
33
69
 
34
- Ripper doesn't carry scope information as it parses, and as such, parses any lone identifier as an AST node of type `:var_ref`. It is up to consumers of the AST to figure out the meaning of the `:var_ref` node. It can be reasonably argued that the semantics of the `:var_ref` node should not be part of the AST, as my thesis adviser pointed out when I complained about this, as the two are syntactically identical. Unfortunately, the meaning of the `var_ref` node comes from the parser itself; any attempt to determine semantics based solely on the AST is simply re-creating the work the parser ordinarily does. Indeed, when Ruby parses the code for execution, it *does* create different internal AST node types upon seeing a method-call bareword and a local variable bareword!
70
+ ## Ripper's Mistake
35
71
 
36
- I'd like to see this behavior rolled into Ripper proper. Until then, `ripper-plus` does it.
72
+ Ripper doesn't carry scope information as it parses, and as such, parses any lone identifier as an AST node of type `:var_ref`. It is up to consumers of the AST to figure out the meaning of the `:var_ref` node. It can be reasonably argued that the semantics of the `:var_ref` node should not be part of the AST, as my thesis adviser pointed out when I complained about this, as the two are syntactically identical. Unfortunately, the meaning of the `:var_ref` node comes from the parser itself; any attempt to determine semantics based solely on the Ripper AST must simulate the work the parser ordinarily does. Indeed, when Ruby parses the code for execution, it *does* create different internal node types upon seeing a method-call bareword and a local variable bareword!
37
73
 
38
- ## ripper-plus
74
+ I'd like to see this behavior rolled into Ripper proper. Until then, `ripper-plus` is a reasonable replacement.
39
75
 
40
- By using our knowledge of Ruby's parser, we can recreate the scope-tracking behavior by walking the AST generated by Ripper. We simply must do normal scope tracking, observe the creation of local variables, and ensure that we walk each node of the tree in the order in which those nodes would have been parsed. Most subtrees generated by Ripper are already in this order, with the exception of the modifier nodes (`foo.bar if baz`, `x = bar() while x != nil`). Most importantly, since everything is an expression in Ruby and local variable assignments can occur just about anywhere, exact scoping semantics must be recreated. Corner cases such as this:
76
+ ## ripper-plus's Approach
41
77
 
78
+ By using our knowledge of Ruby's parser, we can simulate the scope-tracking behavior by walking the AST generated by Ripper. We simply do normal scope tracking, observe the creation of local variables, and ensure that we walk each node of the tree in the order in which those nodes would have been parsed. Most subtrees generated by Ripper are already in this order, with the exception of the modifier nodes (`foo.bar if baz`, `x = bar() while x != nil`). Most importantly, since everything<sup>[*](http://carboni.ca/blog/p/Statements-in-Ruby)</sup> is an expression in Ruby and local variable assignments can occur just about anywhere, exact scoping semantics must be recreated. Every possible introduction of local variables (exception naming, named captures, ...) must be considered as the Ruby parser would. Corner cases such as this:
79
+
80
+ [code]
42
81
  def foo(x, y = y)
43
82
  end
83
+ [/code]
44
84
 
45
85
  Need to be properly resolved. Did you know that, unlike the `label = label` example above, the above code is equivalent to:
46
86
 
87
+ [code]
47
88
  def foo(x, y = y())
48
89
  end
90
+ [/code]
49
91
 
50
- Anyway, ripper-plus turns all method-call `:var_ref` nodes into `:zcall` nodes; the node structure is otherwise unchanged. It runs in O(N) time and O(h) space, where h is the height of the tree.
51
-
52
- ## Syntax Errors
53
-
54
- Not all syntax errors in Ruby fail to parse as valid Ruby. Using `next`/`redo`/`break`/`retry` when they have no meaning will cause a "compile error (SyntaxError)" exception to be raised during parsing. Attempting to use those expressions in a value context will also fail to compile. Ripper, however, doesn't do any of this validation:
92
+ Why doesn't y end up `nil` in the default case, as it would if you typed `y = y` into a method definition?
55
93
 
56
- pp Ripper.sexp('x = 5; next x')
57
- #=>
58
- [:program,
59
- [[:assign, [:var_field, [:@ident, "x", [1, 0]]], [:@int, "5", [1, 4]]],
60
- [:next, [:args_add_block, [[:var_ref, [:@ident, "x", [1, 12]]]], false]]]]
94
+ Anyway, ripper-plus turns all method-call `:var_ref` nodes into `:zcall` nodes; the node structure is otherwise unchanged. I believe Ripper should already make this distinction, and it can relatively simply: it simply has to re-implement the existing scope-tracking behavior to the relevant Ripper action routines. Not trivial, but `ripper-plus` does it in a couple hundred lines of Ruby.
61
95
 
62
- Assigning to set read-only variables is also caught by the Ruby parser, and not at runtime. An easy way to verify this is to put a simple print statement before the offending syntax error:
96
+ # Error Handling
63
97
 
64
- $ ruby -e 'p 5; $1 = "5"'
65
- -e:1: Can't set variable $1
98
+ As mentioned above, not all invalid syntaxes are excluded by Ruby's CFG production rules: additional validation happens occasionally, and when validation fails, `yyerror` is called, and parsing attempts recovery. The Ruby program will not run if such an error exists, but since parsing continues, additional such errors might be found. These are some of (but likely not all) of the errors I'm describing:
66
99
 
67
- Ripper catches this by wrapping the offending assignment in an `:assign_error` node. But not all such invalid assignments are caught:
100
+ 1. `class name ... end`/`module name ... end` with a non-constant name (`=~ /^[^A-Z]/`)
101
+ 2. `alias gvar $[0-9]`: you can't alias the numeric capture variables
102
+ 3. duplicate argument names to a method, block
103
+ 4. constant/instance variable/class variable/global variable as an argument name
104
+ 5. assigning a constant in a method definition
105
+ 6. class/module definition syntax in a method body
106
+ 7. defining singleton methods on literals using expression syntax (`def (expr).foo; end`)
68
107
 
69
- pp Ripper.sexp('$1 = 5')
70
- #=>
71
- [:program,
72
- [[:assign, [:assign_error, [:@backref, "$1", [1, 0]]], [:@int, "5", [1, 5]]]]]
108
+ Whatever your tool is that is dealing with Ruby ASTs, it likely is concerned with whether the program is valid or not, and any program containing the above errors is invalid: Ruby won't even attempt to run it. Ripper, one would suppose, would also inform consumers of the parse tree that a given program is invalid in these ways.
73
109
 
74
- pp Ripper.sexp('nil = self')
75
- #=>
76
- [:program,
77
- [[:assign,
78
- [:var_field, [:@kw, "nil", [1, 0]]],
79
- [:var_ref, [:@kw, "self", [1, 6]]]]]]
110
+ ## Ripper's Mistake
80
111
 
81
- Ripper has at least one other error node type, `:class_name_error`:
112
+ Some of these errors are noticed by Ripper, and the offending portion of the syntax tree will be wrapped in a corresponding error node:
82
113
 
114
+ [code]
115
+ # Invalid alias
116
+ pp Ripper.sexp('alias $foo $2')
117
+ #=>[:program,
118
+ [[:alias_error,
119
+ [:var_alias, [:@gvar, "$foo", [1, 6]], [:@backref, "$2", [1, 11]]]]]]
120
+ # Invalid class name
83
121
  pp Ripper.sexp('class abc; end')
84
- #=>
85
- [:program,
86
- [[:class,
87
- [:const_ref, [:class_name_error, [:@ident, "foo", [1, 6]]]],
88
- nil,
89
- [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
90
-
91
- Ruby has a few dozen of these syntax errors that are caught at compile time, and most of them are things you would be hard-pressed to justify: re-using argument names (`def foo(x, x); end`), creating classes or modules in method bodies, and so on. Yet Ripper does not provide an easy way to check if these errors are present in a given AST.
92
-
93
- I'm not convinced that wrapping offending nodes in different node types is the best way to handle this issue. Firstly, the error nodes are not always high enough in the tree (`:alias_error`, which arises when aliasing `$1, $2, ...`, is a notable exception). The position of the `:class_name_error` node forces every consumption of a `:class` node to check if the name subtree contains a `:class_name_error` node. The whole class is invalid though! If anything, the entire `:class` node should be inside an `:error` node. The same holds true for `:assign` nodes. This isn't really the Ripper designer's faults: Ripper is essentially a separate set of action routines in the same bison grammar used by YARV proper, and as an SAX-style parser, it'd be much harder to push errors upward.
94
-
95
- It does seem reasonable, however, that while we are transforming `:var_ref` into `:zcall` nodes, we can find these errors (including ones ignored by Ripper), wrap them in `:error` nodes, and provide a list of exceptions corresponding to the semantic errors. `ripper-plus` does just that. For example, the above error will transform into this:
96
-
97
- [:program,
98
- [[:error,
99
- [:class,
100
- [:const_ref, [:class_name_error, [:@ident, "foo", [1, 6]]]],
101
- nil,
102
- [:bodystmt, [[:void_stmt]], nil, nil, nil]],
103
- 'class/module name must be CONSTANT']]]
104
-
105
- ## You Should be Using ripper-plus
106
-
107
- The truth is, everybody who is using Ripper right now *should* be doing *all* of this. Anything short, and you have bugs. [Laser](https://github.com/michaeledgar/laser/) has bugs as a result. It's a pain in the ass to get it all right. `ripper-plus` probably has bugs - I'm not gonna lie, I found one while writing this. But I'm pretty damn sure it's solid. Hopefully, in Ruby 1.9.x, this will be the default. For now, you *should* use ripper-plus.
122
+ #=> [:program,
123
+ [[:class,
124
+ [:const_ref, [:class_name_error, [:@ident, "abc", [1, 6]]]],
125
+ nil,
126
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
127
+ [/code]
128
+
129
+ The first error, a disallowed alias, results in an `:alias_error` node wrapping the entire `:var_alias` node. This is good: anybody walking this AST would see that the entire contents of the node are semantically invalid *before* any information about the attempted alias. The second error, an invalid class name, results in a `:class_name_error` node deep in the `:class` node structure. This is bad: it puts the onus on every consumer of the AST to check all class/module definitions for `:class_name_error` nodes before assuming *anything* about the meaning of the `:class` node. This unfortunate placement also occurs when a parameter is named with a non-identifier:
130
+
131
+ [code]
132
+ pp Ripper.sexp('def foo(a, @b); end')
133
+ #=> [:program,
134
+ [[:def,
135
+ [:@ident, "foo", [1, 4]],
136
+ [:paren,
137
+ [:params,
138
+ [[:@ident, "a", [1, 8]], [:param_error, [:@ivar, "@b", [1, 11]]]],
139
+ nil, nil, nil, nil]],
140
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
141
+ [/code]
142
+
143
+ Worse, some errors are not caught at all:
144
+
145
+ [code]
146
+ pp Ripper.sexp('def foo(a, a, a); end')
147
+ #=> [:program,
148
+ [[:def,
149
+ [:@ident, "foo", [1, 4]],
150
+ [:paren,
151
+ [:params,
152
+ [[:@ident, "a", [1, 8]],
153
+ [:@ident, "a", [1, 11]],
154
+ [:@ident, "a", [1, 14]]],
155
+ nil, nil, nil, nil]],
156
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
157
+ [/code]
158
+
159
+ This is very bad. Now every consumer of the Ripper parse tree must check *all method definitions* for repeated arguments, and bail accordingly. Don't forget about subassignments in block argument declarations; you'll need to check those yourself, too:
160
+
161
+ [code]
162
+ pp Ripper.sexp('something.each { |a, (b, *c, (d, e), a), f| }')
163
+ #=> [:program,
164
+ [[:method_add_block,
165
+ [:call,
166
+ [:var_ref, [:@ident, "something", [1, 0]]], :".",
167
+ [:@ident, "each", [1, 10]]],
168
+ [:brace_block, [:block_var,
169
+ [:params,
170
+ [[:@ident, "a", [1, 18]],
171
+ [:mlhs_paren,
172
+ [:mlhs_add_star,
173
+ [[:mlhs_paren, [:@ident, "b", [1, 22]]]],
174
+ [:@ident, "c", [1, 26]]]],
175
+ [:@ident, "f", [1, 41]]],
176
+ nil, nil, nil, nil], nil],
177
+ [[:void_stmt]]]]]]
178
+ [/code]
179
+
180
+ Can your Ripper-consuming code handle that case?
181
+
182
+ ## ripper-plus's Approach
183
+
184
+ This is where it gets murky, and I don't have a definitive answer just yet. Right now, `ripper-plus` wraps all offending nodes it finds (which is *not* all such errors - yet) in a generic `:error` node. At least with this solution, anyone walking a `RipperPlus` AST will know when a given node is semantically invalid. However, the code walking the AST must find an `:error` node before it knows the entire AST is meaningless. For a tool like [YARD](http://yardoc.org/) or [Redcar](http://redcareditor.com/), which likely would prefer to keep extracting information from the AST, this seems preferable. The `:error` node could potentially be specified to potentially include descriptions and locations of the syntax errors discovered.
185
+
186
+ Yet it also seems convenient to receive, upon attempting to parse an invalid program, a simple list of errors and nothing else. For a tool like Laser, this is far preferable, and wastes a lot less time. I think Laser may be in the minority here. So Laser will do its own thing regardless.
187
+
188
+ # Minor Gripes
189
+
190
+ 1. Flip-flops don't have their own node type. Upon encountering a range, you must check if you are in a conditional context manually. This is annoying.
191
+ 2. \_\_END\_\_ just stops parsing: there's no getting at the actual data past it from Ripper's output.
192
+ 3. Ripper.sexp never fails. Given a program that fails to parse, Ripper will simply return the best recovery parse Bison can come up with. (`Ripper.sexp('x*^y#$a') == [:program, [:var_ref, [:@ident, "y", [1, 3]]]]`)
193
+ 4. This space reserved for future gripes.
194
+
195
+ ## Conclusion
196
+
197
+ Overall, Ripper is very complete for a library covered in "EXPERIMENTAL" warning labels, and gives concise, traditional ASTs. What I've put forward is all I ask after nearly a year of being neck-deep in Ripper output. I think the main two points I've covered need to be addressed in the 1.9 branch of Ruby, and over the coming months, hope to work to get that done.
198
+
199
+ In the meantime, I'll be using `ripper-plus` in Laser to statically analyze all kinds of stuff about Ruby, but I look forward to the day where I can add `RipperPlus = Ripper if RUBY_VERSION >= MERGED_VERSION` to ripper-plus's main file. `ripper-plus` is O(N), though it's not blazingly fast: it takes about 20-30 ms on my machine to transform the Ripper AST for [the biggest, ugliest Laser code file: the CFG compiler](https://github.com/michaeledgar/laser/blob/master/lib/laser/analysis/control_flow/cfg_builder.rb) to a RipperPlus AST. It takes 50ms for Ripper to parse it in the first place. (That benchmarked transformation is done in-place: `ripper-plus`'s default behavior is to duplicate the Ripper AST.)
200
+
201
+ If you have ideas or thoughts, please discuss them on ruby-talk and/or ruby-core! I don't have comments implemented for a reason: I feel it fragments the center of discussion.
108
202
 
109
203
  ## Contributing to ripper-plus
110
204
 
@@ -119,5 +213,4 @@ The truth is, everybody who is using Ripper right now *should* be doing *all* of
119
213
  ## Copyright
120
214
 
121
215
  Copyright (c) 2011 Michael Edgar. See LICENSE.txt for
122
- further details.
123
-
216
+ further details.
@@ -2,6 +2,7 @@ if RUBY_VERSION < "1.9"
2
2
  raise 'ripper-plus requires Ruby 1.9+'
3
3
  end
4
4
  require 'ripper'
5
+ $:.unshift(File.expand_path(File.dirname(__FILE__)))
5
6
  require 'ripper-plus/version'
6
7
  require 'ripper-plus/ripper-plus'
7
8
  require 'ripper-plus/scope_stack'
@@ -1,13 +1,15 @@
1
1
  # Top-level module for Ripper Plus. Provides global methods for
2
2
  # getting a RipperPlus AST for a given input program.
3
3
  module RipperPlus
4
+ DEFAULT_OPTS = {:in_place => false}
4
5
  # Parses the given Ruby code into a RipperPlus AST.
5
- def self.sexp(text)
6
- for_ripper_ast(Ripper.sexp(text))
6
+ def self.sexp(text, opts={})
7
+ for_ripper_ast(Ripper.sexp(text), opts.merge(:in_place => true))
7
8
  end
8
9
 
9
10
  # Transforms the provided Ripper AST into a RipperPlus AST.
10
- def self.for_ripper_ast(tree)
11
- Transformer.transform(tree)
11
+ def self.for_ripper_ast(tree, opts={})
12
+ opts = DEFAULT_OPTS.merge(opts)
13
+ Transformer.transform(tree, opts)
12
14
  end
13
15
  end
@@ -25,7 +25,10 @@ module RipperPlus
25
25
  "< #{middle} >"
26
26
  end
27
27
 
28
- def add_variable(var)
28
+ def add_variable(var, allow_duplicates=true)
29
+ if !allow_duplicates && @stack.last.include?(var)
30
+ raise DuplicateArgumentError.new("duplicated argument name (#{var})")
31
+ end
29
32
  @stack.last << var
30
33
  end
31
34
 
@@ -1,5 +1,10 @@
1
1
  require 'set'
2
2
  module RipperPlus
3
+ class SyntaxError < StandardError; end
4
+ class LHSError < SyntaxError; end
5
+ class DynamicConstantError < SyntaxError; end
6
+ class InvalidArgumentError < SyntaxError; end
7
+ class DuplicateArgumentError < SyntaxError; end
3
8
  # Transforms a 1.9.2 Ripper AST into a RipperPlus AST. The only
4
9
  # change as a result of this transformation is that the nodes for
5
10
  # local variable references and zcalls (bareword calls to self)
@@ -43,14 +48,9 @@ module RipperPlus
43
48
  module Transformer
44
49
  extend self
45
50
 
46
- class SyntaxError < StandardError; end
47
- class LHSError < SyntaxError; end
48
- class DynamicConstantError < SyntaxError; end
49
- class InvalidArgumentError < SyntaxError; end
50
-
51
51
  # Transforms the given AST into a RipperPlus AST.
52
- def transform(root)
53
- new_copy = clone_sexp(root)
52
+ def transform(root, opts={})
53
+ new_copy = opts[:in_place] ? root : clone_sexp(root)
54
54
  scope_stack = ScopeStack.new
55
55
  transform_tree(new_copy, scope_stack)
56
56
  new_copy
@@ -59,135 +59,173 @@ module RipperPlus
59
59
  # Transforms the given tree into a RipperPlus AST, using a scope stack.
60
60
  # This will be recursively called through each level of the tree.
61
61
  def transform_tree(tree, scope_stack)
62
- case tree[0]
63
- when :assign, :massign
64
- lhs, rhs = tree[1..2]
65
- begin
66
- add_variables_from_node(lhs, scope_stack)
67
- rescue SyntaxError => err
68
- wrap_node_with_error(tree)
69
- else
70
- transform_tree(rhs, scope_stack)
71
- end
72
- when :for
73
- vars, iterated, body = tree[1..3]
74
- add_variables_from_node(vars, scope_stack)
75
- transform_tree(iterated, scope_stack)
76
- transform_tree(body, scope_stack)
77
- when :var_ref
78
- # When we reach a :var_ref, we should know everything we need to know
79
- # in order to tell if it should be transformed into a :zcall.
80
- if tree[1][0] == :@ident && !scope_stack.has_variable?(tree[1][1])
81
- tree[0] = :zcall
82
- end
83
- when :class
84
- name, superclass, body = tree[1..3]
85
- if name[1][0] == :class_name_error || scope_stack.in_method?
86
- wrap_node_with_error(tree)
87
- else
88
- transform_tree(superclass, scope_stack) if superclass # superclass node
89
- scope_stack.with_closed_scope do
90
- transform_tree(body, scope_stack)
91
- end
92
- end
93
- when :module
94
- name, body = tree[1..2]
95
- if name[1][0] == :class_name_error || scope_stack.in_method?
96
- wrap_node_with_error(tree)
97
- else
98
- scope_stack.with_closed_scope do
99
- transform_tree(body, scope_stack) # body
62
+ if Symbol === tree[0]
63
+ case tree[0]
64
+ when :assign, :massign
65
+ lhs, rhs = tree[1..2]
66
+ begin
67
+ add_variables_from_node(lhs, scope_stack)
68
+ rescue SyntaxError => err
69
+ wrap_node_with_error(tree)
70
+ else
71
+ transform_tree(rhs, scope_stack)
100
72
  end
101
- end
102
- when :sclass
103
- singleton, body = tree[1..2]
104
- transform_tree(singleton, scope_stack)
105
- scope_stack.with_closed_scope do
73
+ when :for
74
+ vars, iterated, body = tree[1..3]
75
+ add_variables_from_node(vars, scope_stack)
76
+ transform_tree(iterated, scope_stack)
106
77
  transform_tree(body, scope_stack)
107
- end
108
- when :def
109
- scope_stack.with_closed_scope(true) do
110
- param_node = tree[2]
111
- body = tree[3]
112
- begin
113
- transform_params(param_node, scope_stack)
114
- rescue SyntaxError
78
+ when :var_ref
79
+ # When we reach a :var_ref, we should know everything we need to know
80
+ # in order to tell if it should be transformed into a :zcall.
81
+ if tree[1][0] == :@ident && !scope_stack.has_variable?(tree[1][1])
82
+ tree[0] = :zcall
83
+ end
84
+ when :class
85
+ name, superclass, body = tree[1..3]
86
+ if name[1][0] == :class_name_error || scope_stack.in_method?
115
87
  wrap_node_with_error(tree)
116
88
  else
117
- transform_tree(body, scope_stack)
89
+ transform_tree(superclass, scope_stack) if superclass # superclass node
90
+ scope_stack.with_closed_scope do
91
+ transform_tree(body, scope_stack)
92
+ end
118
93
  end
119
- end
120
- when :defs
121
- transform_tree(tree[1], scope_stack) # singleton could be a method call!
122
- scope_stack.with_closed_scope(true) do
123
- param_node = tree[4]
124
- body = tree[5]
125
- begin
126
- transform_params(param_node, scope_stack)
127
- rescue SyntaxError
94
+ when :module
95
+ name, body = tree[1..2]
96
+ if name[1][0] == :class_name_error || scope_stack.in_method?
128
97
  wrap_node_with_error(tree)
129
98
  else
99
+ scope_stack.with_closed_scope do
100
+ transform_tree(body, scope_stack) # body
101
+ end
102
+ end
103
+ when :sclass
104
+ singleton, body = tree[1..2]
105
+ transform_tree(singleton, scope_stack)
106
+ scope_stack.with_closed_scope do
130
107
  transform_tree(body, scope_stack)
131
108
  end
132
- end
133
- when :rescue
134
- list, name, body = tree[1..3]
135
- transform_tree(list, scope_stack)
136
- # Don't forget the rescue argument!
137
- if name
138
- add_variables_from_node(name, scope_stack)
139
- end
140
- transform_tree(body, scope_stack)
141
- when :method_add_block
142
- call, block = tree[1..2]
143
- # first transform the call
144
- transform_tree(call, scope_stack)
145
- # then transform the block
146
- block_args, block_body = block[1..2]
147
- scope_stack.with_open_scope do
148
- if block_args
149
- transform_params(block_args[1], scope_stack)
150
- if block_args[2]
151
- block_args[2].each { |var| add_variables_from_node(var, scope_stack) }
109
+ when :def
110
+ scope_stack.with_closed_scope(true) do
111
+ param_node = tree[2]
112
+ body = tree[3]
113
+ transform_params_then_body(tree, param_node, body, scope_stack)
114
+ end
115
+ when :defs
116
+ transform_tree(tree[1], scope_stack) # singleton could be a method call!
117
+ scope_stack.with_closed_scope(true) do
118
+ param_node = tree[4]
119
+ body = tree[5]
120
+ transform_params_then_body(tree, param_node, body, scope_stack)
121
+ end
122
+ when :lambda
123
+ param_node, body = tree[1..2]
124
+ scope_stack.with_open_scope do
125
+ transform_params_then_body(tree, param_node, body, scope_stack)
126
+ end
127
+ when :rescue
128
+ list, name, body = tree[1..3]
129
+ transform_tree(list, scope_stack)
130
+ # Don't forget the rescue argument!
131
+ if name
132
+ add_variables_from_node(name, scope_stack)
133
+ end
134
+ transform_tree(body, scope_stack)
135
+ when :method_add_block
136
+ call, block = tree[1..2]
137
+ # first transform the call
138
+ transform_tree(call, scope_stack)
139
+ # then transform the block
140
+ param_node, body = block[1..2]
141
+ scope_stack.with_open_scope do
142
+ begin
143
+ if param_node
144
+ transform_params(param_node[1], scope_stack)
145
+ if param_node[2]
146
+ add_variable_list(param_node[2], scope_stack, false)
147
+ end
148
+ end
149
+ rescue SyntaxError
150
+ wrap_node_with_error(tree)
151
+ else
152
+ transform_tree(body, scope_stack)
153
+ end
154
+ end
155
+ when :binary
156
+ # must check for named groups in a literal match. wowzerz.
157
+ lhs, op, rhs = tree[1..3]
158
+ if op == :=~
159
+ if lhs[0] == :regexp_literal
160
+ add_locals_from_regexp(lhs, scope_stack)
161
+ transform_tree(rhs, scope_stack)
162
+ elsif lhs[0] == :paren && !lhs[1].empty? && lhs[1] != [[:void_stmt]] && lhs[1].last[0] == :regexp_literal
163
+ lhs[1][0..-2].each { |node| transform_tree(node, scope_stack) }
164
+ add_locals_from_regexp(lhs[1].last, scope_stack)
165
+ transform_tree(rhs, scope_stack)
166
+ else
167
+ transform_in_order(tree, scope_stack)
152
168
  end
169
+ else
170
+ transform_in_order(tree, scope_stack)
153
171
  end
154
- transform_tree(block_body, scope_stack)
172
+ when :if_mod, :unless_mod, :while_mod, :until_mod, :rescue_mod
173
+ # The AST is the reverse of the parse order for these nodes.
174
+ transform_tree(tree[2], scope_stack)
175
+ transform_tree(tree[1], scope_stack)
176
+ when :alias_error, :assign_error # error already top-level! wrap it again.
177
+ wrap_node_with_error(tree)
178
+ else
179
+ transform_in_order(tree, scope_stack)
155
180
  end
156
- when :if_mod, :unless_mod, :while_mod, :until_mod, :rescue_mod
157
- # The AST is the reverse of the parse order for these nodes.
158
- transform_tree(tree[2], scope_stack)
159
- transform_tree(tree[1], scope_stack)
160
- when :alias_error, :assign_error # error already top-level! wrap it again.
161
- wrap_node_with_error(tree)
162
181
  else
163
182
  transform_in_order(tree, scope_stack)
164
183
  end
165
184
  end
166
185
 
186
+ def transform_params_then_body(tree, params, body, scope_stack)
187
+ transform_params(params, scope_stack)
188
+ rescue SyntaxError
189
+ wrap_node_with_error(tree)
190
+ else
191
+ transform_tree(body, scope_stack)
192
+ end
193
+
194
+ def add_locals_from_regexp(regexp, scope_stack)
195
+ regexp_parts = regexp[1]
196
+ if regexp_parts.one? && regexp_parts[0][0] == :@tstring_content
197
+ regexp_text = regexp_parts[0][1]
198
+ captures = Regexp.new(regexp_text).names
199
+ captures.each { |var_name| scope_stack.add_variable(var_name) }
200
+ end
201
+ end
202
+
203
+ def add_variable_list(list, scope_stack, allow_duplicates=true)
204
+ list.each { |var| add_variables_from_node(var, scope_stack, allow_duplicates) }
205
+ end
206
+
167
207
  # Adds variables to the given scope stack from the given node. Allows
168
208
  # nodes from parameter lists, left-hand-sides, block argument lists, and
169
209
  # so on.
170
- def add_variables_from_node(lhs, scope_stack)
210
+ def add_variables_from_node(lhs, scope_stack, allow_duplicates=true)
171
211
  case lhs[0]
172
212
  when :@ident
173
- scope_stack.add_variable(lhs[1])
213
+ scope_stack.add_variable(lhs[1], allow_duplicates)
174
214
  when :const_path_field, :@const, :top_const_field
175
215
  if scope_stack.in_method?
176
216
  raise DynamicConstantError.new
177
217
  end
178
218
  when Array
179
- lhs.each { |var| add_variables_from_node(var, scope_stack) }
219
+ add_variable_list(lhs, scope_stack, allow_duplicates)
180
220
  when :mlhs_paren, :var_field, :rest_param, :blockarg
181
- add_variables_from_node(lhs[1], scope_stack)
221
+ add_variables_from_node(lhs[1], scope_stack, allow_duplicates)
182
222
  when :mlhs_add_star
183
223
  pre_star, star, post_star = lhs[1..3]
184
- pre_star.each { |var| add_variables_from_node(var, scope_stack) }
224
+ add_variable_list(pre_star, scope_stack, allow_duplicates)
185
225
  if star
186
- add_variables_from_node(star, scope_stack)
187
- end
188
- if post_star
189
- post_star.each { |var| add_variables_from_node(var, scope_stack) }
226
+ add_variables_from_node(star, scope_stack, allow_duplicates)
190
227
  end
228
+ add_variable_list(post_star, scope_stack, allow_duplicates) if post_star
191
229
  when :param_error
192
230
  raise InvalidArgumentError.new
193
231
  when :assign_error
@@ -214,25 +252,19 @@ module RipperPlus
214
252
  param_node = param_node[1] if param_node[0] == :paren
215
253
  if param_node
216
254
  positional_1, optional, rest, positional_2, block = param_node[1..5]
217
- if positional_1
218
- positional_1.each { |var| add_variables_from_node(var, scope_stack) }
219
- end
255
+ add_variable_list(positional_1, scope_stack, false) if positional_1
220
256
  if optional
221
257
  optional.each do |var, value|
222
258
  # MUST walk value first. (def foo(y=y); end) == (def foo(y=y()); end)
223
259
  transform_tree(value, scope_stack)
224
- add_variables_from_node(var, scope_stack)
260
+ add_variables_from_node(var, scope_stack, false)
225
261
  end
226
262
  end
227
263
  if rest && rest[1]
228
- add_variables_from_node(rest, scope_stack)
229
- end
230
- if positional_2
231
- positional_2.each { |var| add_variables_from_node(var, scope_stack) }
232
- end
233
- if block
234
- add_variables_from_node(block, scope_stack)
264
+ add_variables_from_node(rest, scope_stack, false)
235
265
  end
266
+ add_variable_list(positional_2, scope_stack, false) if positional_2
267
+ add_variables_from_node(block, scope_stack, false) if block
236
268
  end
237
269
  end
238
270
 
@@ -1,10 +1,14 @@
1
1
  module RipperPlus
2
2
  module Version
3
3
  MAJOR = 1
4
- MINOR = 1
4
+ MINOR = 2
5
5
  PATCH = 0
6
- BUILD = 'pre2'
6
+ BUILD = ''
7
7
 
8
- STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
8
+ if BUILD.empty?
9
+ STRING = [MAJOR, MINOR, PATCH].compact.join('.')
10
+ else
11
+ STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
12
+ end
9
13
  end
10
14
  end
@@ -28,5 +28,6 @@ def dfs_for_node_type(tree, type)
28
28
  end
29
29
 
30
30
  RSpec.configure do |config|
31
-
31
+ config.filter_run focus: true
32
+ config.run_all_when_everything_filtered = true
32
33
  end
@@ -248,6 +248,66 @@ describe RipperPlus::Transformer do
248
248
  input_tree.should transform_to(output_tree)
249
249
  end
250
250
 
251
+ it 'should transform respecting stabby-lambda arguments' do
252
+ input_tree =
253
+ [:program,
254
+ [[:assign,
255
+ [:var_field, [:@ident, "y", [1, 0]]],
256
+ [:lambda,
257
+ [:paren,
258
+ [:params,
259
+ [[:@ident, "a", [1, 7]]],
260
+ [[[:@ident, "b", [1, 10]], [:var_ref, [:@ident, "a", [1, 12]]]],
261
+ [[:@ident, "c", [1, 15]], [:var_ref, [:@ident, "d", [1, 17]]]]],
262
+ [:rest_param, [:@ident, "rest", [1, 21]]],
263
+ nil,
264
+ nil]],
265
+ [[:binary,
266
+ [:array,
267
+ [[:binary,
268
+ [:binary,
269
+ [:binary,
270
+ [:var_ref, [:@ident, "a", [1, 30]]],
271
+ :*,
272
+ [:var_ref, [:@ident, "b", [1, 34]]]],
273
+ :*,
274
+ [:var_ref, [:@ident, "c", [1, 38]]]],
275
+ :*,
276
+ [:var_ref, [:@ident, "d", [1, 42]]]]]],
277
+ :+,
278
+ [:var_ref, [:@ident, "rest", [1, 47]]]]]]],
279
+ [:var_ref, [:@ident, "a", [1, 55]]]]]
280
+ output_tree =
281
+ [:program,
282
+ [[:assign,
283
+ [:var_field, [:@ident, "y", [1, 0]]],
284
+ [:lambda,
285
+ [:paren,
286
+ [:params,
287
+ [[:@ident, "a", [1, 7]]],
288
+ [[[:@ident, "b", [1, 10]], [:var_ref, [:@ident, "a", [1, 12]]]],
289
+ [[:@ident, "c", [1, 15]], [:zcall, [:@ident, "d", [1, 17]]]]],
290
+ [:rest_param, [:@ident, "rest", [1, 21]]],
291
+ nil,
292
+ nil]],
293
+ [[:binary,
294
+ [:array,
295
+ [[:binary,
296
+ [:binary,
297
+ [:binary,
298
+ [:var_ref, [:@ident, "a", [1, 30]]],
299
+ :*,
300
+ [:var_ref, [:@ident, "b", [1, 34]]]],
301
+ :*,
302
+ [:var_ref, [:@ident, "c", [1, 38]]]],
303
+ :*,
304
+ [:zcall, [:@ident, "d", [1, 42]]]]]],
305
+ :+,
306
+ [:var_ref, [:@ident, "rest", [1, 47]]]]]]],
307
+ [:zcall, [:@ident, "a", [1, 55]]]]]
308
+ input_tree.should transform_to output_tree
309
+ end
310
+
251
311
  it 'should transform respecting subassignments in block arguments' do
252
312
  input_tree =
253
313
  [:program,
@@ -563,6 +623,98 @@ describe RipperPlus::Transformer do
563
623
  [:args_add_block, [[:zcall, [:@ident, "err", [1, 78]]]], false]]]]]]]]
564
624
  input_tree.should transform_to output_tree
565
625
  end
626
+
627
+ it 'finds local variables created by named captures' do
628
+ input_tree =
629
+ [:program,
630
+ [[:def,
631
+ [:@ident, "foo", [1, 4]],
632
+ [:paren, [:params, [[:@ident, "a", [1, 8]]], nil, nil, nil, nil]],
633
+ [:bodystmt,
634
+ [[:void_stmt],
635
+ [:binary,
636
+ [:regexp_literal,
637
+ [[:@tstring_content, "name: (?<name>w+)", [1, 13]]],
638
+ [:@regexp_end, "/", [1, 30]]],
639
+ :=~,
640
+ [:var_ref, [:@ident, "a", [1, 35]]]],
641
+ [:command,
642
+ [:@ident, "p", [1, 38]],
643
+ [:args_add_block, [[:var_ref, [:@ident, "name", [1, 40]]]], false]],
644
+ [:var_ref, [:@ident, "named", [1, 46]]]],
645
+ nil,
646
+ nil,
647
+ nil]]]]
648
+ output_tree =
649
+ [:program,
650
+ [[:def,
651
+ [:@ident, "foo", [1, 4]],
652
+ [:paren, [:params, [[:@ident, "a", [1, 8]]], nil, nil, nil, nil]],
653
+ [:bodystmt,
654
+ [[:void_stmt],
655
+ [:binary,
656
+ [:regexp_literal,
657
+ [[:@tstring_content, "name: (?<name>w+)", [1, 13]]],
658
+ [:@regexp_end, "/", [1, 30]]],
659
+ :=~,
660
+ [:var_ref, [:@ident, "a", [1, 35]]]],
661
+ [:command,
662
+ [:@ident, "p", [1, 38]],
663
+ [:args_add_block, [[:var_ref, [:@ident, "name", [1, 40]]]], false]],
664
+ [:zcall, [:@ident, "named", [1, 46]]]],
665
+ nil,
666
+ nil,
667
+ nil]]]]
668
+ input_tree.should transform_to output_tree
669
+ end
670
+
671
+ it 'finds local variables created by named captures in a paren LHS' do
672
+ input_tree =
673
+ [:program,
674
+ [[:def,
675
+ [:@ident, "abc", [1, 4]],
676
+ [:paren, [:params, [[:@ident, "a", [1, 8]]], nil, nil, nil, nil]],
677
+ [:bodystmt,
678
+ [[:void_stmt],
679
+ [:binary,
680
+ [:paren,
681
+ [[:var_ref, [:@ident, "foo", [1, 13]]],
682
+ [:var_ref, [:@ident, "bar", [1, 18]]],
683
+ [:regexp_literal,
684
+ [[:@tstring_content, "name: (?<name>w+) (?<numba>d+)", [1, 24]]],
685
+ [:@regexp_end, "/", [1, 54]]]]],
686
+ :=~,
687
+ [:var_ref, [:@ident, "a", [1, 60]]]],
688
+ [:var_ref, [:@ident, "name", [1, 63]]],
689
+ [:var_ref, [:@ident, "numba", [1, 69]]],
690
+ [:var_ref, [:@ident, "number", [1, 76]]]],
691
+ nil,
692
+ nil,
693
+ nil]]]]
694
+ output_tree =
695
+ [:program,
696
+ [[:def,
697
+ [:@ident, "abc", [1, 4]],
698
+ [:paren, [:params, [[:@ident, "a", [1, 8]]], nil, nil, nil, nil]],
699
+ [:bodystmt,
700
+ [[:void_stmt],
701
+ [:binary,
702
+ [:paren,
703
+ [[:zcall, [:@ident, "foo", [1, 13]]],
704
+ [:zcall, [:@ident, "bar", [1, 18]]],
705
+ [:regexp_literal,
706
+ [[:@tstring_content, "name: (?<name>w+) (?<numba>d+)", [1, 24]]],
707
+ [:@regexp_end, "/", [1, 54]]]]],
708
+ :=~,
709
+ [:var_ref, [:@ident, "a", [1, 60]]]],
710
+ [:var_ref, [:@ident, "name", [1, 63]]],
711
+ [:var_ref, [:@ident, "numba", [1, 69]]],
712
+ [:zcall, [:@ident, "number", [1, 76]]]],
713
+ nil,
714
+ nil,
715
+ nil]]]]
716
+ input_tree.should transform_to output_tree
717
+ end
566
718
  end
567
719
  describe 'error transformation' do
568
720
  it 'should push up module name errors' do
@@ -821,5 +973,183 @@ describe RipperPlus::Transformer do
821
973
  end
822
974
  end
823
975
 
976
+ it 'should wrap method definitions with duplicated arguments in error nodes' do
977
+ input_tree =
978
+ [:program,
979
+ [[:def,
980
+ [:@ident, "foo", [1, 4]],
981
+ [:paren,
982
+ [:params,
983
+ [[:@ident, "a", [1, 8]], [:@ident, "a", [1, 11]]],
984
+ nil,
985
+ nil,
986
+ nil,
987
+ nil]],
988
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
989
+ output_tree =
990
+ [:program,
991
+ [[:error,
992
+ [:def,
993
+ [:@ident, "foo", [1, 4]],
994
+ [:paren,
995
+ [:params,
996
+ [[:@ident, "a", [1, 8]], [:@ident, "a", [1, 11]]],
997
+ nil,
998
+ nil,
999
+ nil,
1000
+ nil]],
1001
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]]
1002
+ input_tree.should transform_to output_tree
1003
+ end
1004
+
1005
+ it 'should wrap singleton method definitions with duplicated arguments in error nodes' do
1006
+ input_tree =
1007
+ [:program,
1008
+ [[:defs,
1009
+ [:var_ref, [:@kw, "self", [1, 4]]],
1010
+ [:@period, ".", [1, 8]],
1011
+ [:@ident, "foo", [1, 4]],
1012
+ [:paren,
1013
+ [:params,
1014
+ [[:@ident, "a", [1, 8]], [:@ident, "a", [1, 11]]],
1015
+ nil,
1016
+ nil,
1017
+ nil,
1018
+ nil]],
1019
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]
1020
+ output_tree =
1021
+ [:program,
1022
+ [[:error,
1023
+ [:defs,
1024
+ [:var_ref, [:@kw, "self", [1, 4]]],
1025
+ [:@period, ".", [1, 8]],
1026
+ [:@ident, "foo", [1, 4]],
1027
+ [:paren,
1028
+ [:params,
1029
+ [[:@ident, "a", [1, 8]], [:@ident, "a", [1, 11]]],
1030
+ nil,
1031
+ nil,
1032
+ nil,
1033
+ nil]],
1034
+ [:bodystmt, [[:void_stmt]], nil, nil, nil]]]]]
1035
+ input_tree.should transform_to output_tree
1036
+ end
1037
+
1038
+ it 'should wrap stabby lambda definitions with duplicated arguments in error nodes' do
1039
+ input_tree =
1040
+ [:program,
1041
+ [[:lambda,
1042
+ [:paren,
1043
+ [:params,
1044
+ [[:@ident, "a", [1, 3]], [:@ident, "b", [1, 6]], [:@ident, "a", [1, 9]]],
1045
+ nil,
1046
+ nil,
1047
+ nil,
1048
+ nil]],
1049
+ [[:void_stmt]]]]]
1050
+ output_tree =
1051
+ [:program,
1052
+ [[:error,
1053
+ [:lambda,
1054
+ [:paren,
1055
+ [:params,
1056
+ [[:@ident, "a", [1, 3]], [:@ident, "b", [1, 6]], [:@ident, "a", [1, 9]]],
1057
+ nil,
1058
+ nil,
1059
+ nil,
1060
+ nil]],
1061
+ [[:void_stmt]]]]]]
1062
+ input_tree.should transform_to output_tree
1063
+ end
1064
+
1065
+ it 'should wrap block calls with duplicated block args in error nodes' do
1066
+ input_tree =
1067
+ [:program,
1068
+ [[:method_add_block,
1069
+ [:call,
1070
+ [:var_ref, [:@kw, "self", [1, 0]]],
1071
+ :".",
1072
+ [:@ident, "foo!", [1, 5]]],
1073
+ [:brace_block,
1074
+ [:block_var,
1075
+ [:params,
1076
+ [[:@ident, "a", [1, 13]]],
1077
+ [[[:@ident, "b", [1, 16]], [:@int, "3", [1, 18]]]],
1078
+ nil,
1079
+ [[:mlhs_paren,
1080
+ [:mlhs_add_star,
1081
+ [[:mlhs_paren, [:@ident, "c", [1, 22]]]],
1082
+ [:@ident, "a", [1, 26]]]]],
1083
+ nil],
1084
+ nil],
1085
+ [[:void_stmt]]]]]]
1086
+ output_tree =
1087
+ [:program,
1088
+ [[:error,
1089
+ [:method_add_block,
1090
+ [:call,
1091
+ [:var_ref, [:@kw, "self", [1, 0]]],
1092
+ :".",
1093
+ [:@ident, "foo!", [1, 5]]],
1094
+ [:brace_block,
1095
+ [:block_var,
1096
+ [:params,
1097
+ [[:@ident, "a", [1, 13]]],
1098
+ [[[:@ident, "b", [1, 16]], [:@int, "3", [1, 18]]]],
1099
+ nil,
1100
+ [[:mlhs_paren,
1101
+ [:mlhs_add_star,
1102
+ [[:mlhs_paren, [:@ident, "c", [1, 22]]]],
1103
+ [:@ident, "a", [1, 26]]]]],
1104
+ nil],
1105
+ nil],
1106
+ [[:void_stmt]]]]]]]
1107
+ input_tree.should transform_to output_tree
1108
+ end
1109
+
1110
+ it 'should wrap block calls with duplicated block-local args in error nodes' do
1111
+ input_tree =
1112
+ [:program,
1113
+ [[:method_add_block,
1114
+ [:call,
1115
+ [:var_ref, [:@kw, "self", [1, 0]]],
1116
+ :".",
1117
+ [:@ident, "foo!", [1, 5]]],
1118
+ [:brace_block,
1119
+ [:block_var,
1120
+ [:params,
1121
+ [[:@ident, "a", [1, 13]]],
1122
+ [[[:@ident, "b", [1, 16]], [:@int, "3", [1, 18]]]],
1123
+ nil,
1124
+ [[:mlhs_paren,
1125
+ [:mlhs_add_star,
1126
+ [[:mlhs_paren, [:@ident, "c", [1, 22]]]],
1127
+ [:@ident, "d", [1, 26]]]]],
1128
+ nil],
1129
+ [[:@ident, "e", [1, 30]], [:@ident, "a", [1, 33]]]],
1130
+ [[:void_stmt]]]]]]
1131
+ output_tree =
1132
+ [:program,
1133
+ [[:error,
1134
+ [:method_add_block,
1135
+ [:call,
1136
+ [:var_ref, [:@kw, "self", [1, 0]]],
1137
+ :".",
1138
+ [:@ident, "foo!", [1, 5]]],
1139
+ [:brace_block,
1140
+ [:block_var,
1141
+ [:params,
1142
+ [[:@ident, "a", [1, 13]]],
1143
+ [[[:@ident, "b", [1, 16]], [:@int, "3", [1, 18]]]],
1144
+ nil,
1145
+ [[:mlhs_paren,
1146
+ [:mlhs_add_star,
1147
+ [[:mlhs_paren, [:@ident, "c", [1, 22]]]],
1148
+ [:@ident, "d", [1, 26]]]]],
1149
+ nil],
1150
+ [[:@ident, "e", [1, 30]], [:@ident, "a", [1, 33]]]],
1151
+ [[:void_stmt]]]]]]]
1152
+ input_tree.should transform_to output_tree
1153
+ end
824
1154
  end
825
1155
  end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ripper-plus
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: 6
5
- version: 1.1.0.pre2
4
+ prerelease:
5
+ version: 1.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Michael Edgar
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-02 00:00:00 Z
13
+ date: 2011-05-04 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec
@@ -98,16 +98,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
- hash: 633908631425792624
101
+ hash: -1365054536111206632
102
102
  segments:
103
103
  - 0
104
104
  version: "0"
105
105
  required_rubygems_version: !ruby/object:Gem::Requirement
106
106
  none: false
107
107
  requirements:
108
- - - ">"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 1.3.1
110
+ version: "0"
111
111
  requirements: []
112
112
 
113
113
  rubyforge_project: