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 +152 -59
- data/lib/ripper-plus.rb +1 -0
- data/lib/ripper-plus/ripper-plus.rb +6 -4
- data/lib/ripper-plus/scope_stack.rb +4 -1
- data/lib/ripper-plus/transformer.rb +147 -115
- data/lib/ripper-plus/version.rb +7 -3
- data/spec/spec_helper.rb +2 -1
- data/spec/transformer_spec.rb +330 -0
- metadata +6 -6
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
70
|
+
## Ripper's Mistake
|
35
71
|
|
36
|
-
|
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
|
-
|
74
|
+
I'd like to see this behavior rolled into Ripper proper. Until then, `ripper-plus` is a reasonable replacement.
|
39
75
|
|
40
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
96
|
+
# Error Handling
|
63
97
|
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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.
|
data/lib/ripper-plus.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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(
|
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
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
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
|
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
|
|
data/lib/ripper-plus/version.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
module RipperPlus
|
2
2
|
module Version
|
3
3
|
MAJOR = 1
|
4
|
-
MINOR =
|
4
|
+
MINOR = 2
|
5
5
|
PATCH = 0
|
6
|
-
BUILD = '
|
6
|
+
BUILD = ''
|
7
7
|
|
8
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
data/spec/transformer_spec.rb
CHANGED
@@ -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:
|
5
|
-
version: 1.
|
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-
|
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:
|
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:
|
110
|
+
version: "0"
|
111
111
|
requirements: []
|
112
112
|
|
113
113
|
rubyforge_project:
|