zombie-killer 0.2
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 +7 -0
- data/LICENSE +22 -0
- data/README.md +28 -0
- data/bin/zk +45 -0
- data/lib/zombie_killer.rb +4 -0
- data/lib/zombie_killer/code_histogram.rb +50 -0
- data/lib/zombie_killer/killer.rb +35 -0
- data/lib/zombie_killer/niceness.rb +60 -0
- data/lib/zombie_killer/node_type_counter.rb +29 -0
- data/lib/zombie_killer/rewriter.rb +303 -0
- data/lib/zombie_killer/variable_scope.rb +62 -0
- data/lib/zombie_killer/version.rb +3 -0
- data/spec/rspec_renderer.rb +172 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/zombie_killer_spec.md +1101 -0
- data/spec/zombie_killer_spec.rb +1148 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 45c9c420e1362bc5ed2354d40b1a7c5951530f50
|
4
|
+
data.tar.gz: 8a2d7b00e756ebc906b73120c139bda02d734912
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 157976c4b7e1cc94cc8c0384b91d69bd5909eefc1ffb15569497e9fef8fff8046a5c326b9a211e703cc4192601d1ea79a977c9c3618a33370bb6ddeba1bf2922
|
7
|
+
data.tar.gz: a8e31e77e9bab7316b6fac67ce1d36eafbdfb301aa60ae56f5239f3f8005e8e44bc501a3681235a62ddb449225b7609c24c867e8227cd9d1de1b617ba0f3f672
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 SUSE LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
Zombie Killer
|
2
|
+
=============
|
3
|
+
|
4
|
+
In [YaST][y] we have tons of Ruby code which is ugly, because it was
|
5
|
+
[translated from a legacy language][yk], striving for bug-compatibility.
|
6
|
+
|
7
|
+
Zombie Killer analyzes the code for situations where it is safe
|
8
|
+
to replace the ugly variants with nice ones.
|
9
|
+
|
10
|
+
See the [runnable specification][spec] for details.
|
11
|
+
|
12
|
+
[y]: https://github.com/yast
|
13
|
+
[yk]: http://mvidner.blogspot.cz/2013/08/yast-in-ruby.html
|
14
|
+
[spec]: spec/zombie_killer_spec.md
|
15
|
+
|
16
|
+
Installation
|
17
|
+
------------
|
18
|
+
|
19
|
+
Source: clone the git repository.
|
20
|
+
|
21
|
+
Dependencies: run `bundle`.
|
22
|
+
(On openSUSE, most dependencies are packaged as rubygem-*.rpm except `unparser`)
|
23
|
+
|
24
|
+
Usage
|
25
|
+
-----
|
26
|
+
|
27
|
+
`zk [FILES...]` works in place, so it is best to use in a Git checkout.
|
28
|
+
By default it finds all `*.rb` files under the current directory.
|
data/bin/zk
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "docopt"
|
4
|
+
|
5
|
+
require_relative "../lib/zombie_killer"
|
6
|
+
|
7
|
+
doc = <<-EOT
|
8
|
+
Zombie Killer -- tool to kill YCP zombies
|
9
|
+
|
10
|
+
Usage: zk [options] [FILES...]
|
11
|
+
|
12
|
+
Arguments:
|
13
|
+
FILES Files to operate on, patterns allowed [default: **/*.rb]
|
14
|
+
Options:
|
15
|
+
-u, --unsafe Translate even constructs not known to be safe.
|
16
|
+
-s, --stats Also print statistics about node types.
|
17
|
+
-v, --version Print version information and exit.
|
18
|
+
-h, --help Print help and exit.
|
19
|
+
EOT
|
20
|
+
|
21
|
+
begin
|
22
|
+
options = Docopt.docopt(doc, help: true, version: ZombieKiller::VERSION)
|
23
|
+
|
24
|
+
killer = ZombieKiller.new
|
25
|
+
|
26
|
+
files = options["FILES"]
|
27
|
+
files << "**/*.rb" if files.empty?
|
28
|
+
files = files.flat_map do |pattern|
|
29
|
+
if pattern.include? "*"
|
30
|
+
Dir[pattern]
|
31
|
+
else
|
32
|
+
pattern
|
33
|
+
end
|
34
|
+
end
|
35
|
+
files.each do |file|
|
36
|
+
killer.kill_file(file, file, unsafe: options["--unsafe"])
|
37
|
+
|
38
|
+
if options["--stats"]
|
39
|
+
counter = NodeTypeCounter.new(file)
|
40
|
+
counter.print($stderr)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue Docopt::Exit => e
|
44
|
+
abort e.message
|
45
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class CodeHistogram
|
2
|
+
attr_reader :counts
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@counts = Hash.new do |hash, key|
|
6
|
+
hash[key] = 0
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def increment(key, value = 1)
|
11
|
+
@counts[key] += value
|
12
|
+
end
|
13
|
+
|
14
|
+
def print_by_frequency(io)
|
15
|
+
count_to_methods = invert_hash_preserving_duplicates(@counts)
|
16
|
+
|
17
|
+
count_to_methods.keys.sort.each do |c|
|
18
|
+
count_to_methods[c].sort.each do |method|
|
19
|
+
io.printf("%4d %s\n", c, method)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse_by_frequency(lines)
|
25
|
+
histogram = CodeHistogram.new
|
26
|
+
lines.each do |line|
|
27
|
+
/^\s*(\d*)\s*(.*)/.match(line.chomp) do |m|
|
28
|
+
histogram.increment(m[2], m[1].to_i)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
histogram
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge!(other)
|
35
|
+
counts.merge!(other.counts) do |key, count, other_count|
|
36
|
+
count + other_count
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def invert_hash_preserving_duplicates(h)
|
43
|
+
ih = {}
|
44
|
+
h.each do |k, v|
|
45
|
+
ih[v] = [] unless ih.has_key?(v)
|
46
|
+
ih[v] << k
|
47
|
+
end
|
48
|
+
ih
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "parser"
|
2
|
+
require "parser/current"
|
3
|
+
|
4
|
+
require_relative "rewriter"
|
5
|
+
|
6
|
+
class ZombieKiller
|
7
|
+
# @returns new string
|
8
|
+
def kill_string(code, filename = "(inline code)", unsafe: false)
|
9
|
+
fixed_point(code) do |c|
|
10
|
+
parser = Parser::CurrentRuby.new
|
11
|
+
rewriter = ZombieKillerRewriter.new(unsafe: unsafe)
|
12
|
+
buffer = Parser::Source::Buffer.new(filename)
|
13
|
+
buffer.source = c
|
14
|
+
rewriter.rewrite(buffer, parser.parse(buffer))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
alias_method :kill, :kill_string
|
18
|
+
|
19
|
+
# @param new_filename may be the same as *filename*
|
20
|
+
def kill_file(filename, new_filename, unsafe: false)
|
21
|
+
new_string = kill_string(File.read(filename), filename, unsafe: unsafe)
|
22
|
+
|
23
|
+
File.write(new_filename, new_string)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def fixed_point(x, &lambda_x)
|
29
|
+
while true
|
30
|
+
y = lambda_x.call(x)
|
31
|
+
return y if y == x
|
32
|
+
x = y
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
# Niceness of a node means that it cannot be nil.
|
4
|
+
#
|
5
|
+
# Note that the module depends on the includer
|
6
|
+
# to provide #scope (for #nice_variable)
|
7
|
+
module Niceness
|
8
|
+
# Literals are nice, except the nil literal.
|
9
|
+
NICE_LITERAL_NODE_TYPES = [
|
10
|
+
:self,
|
11
|
+
:false, :true,
|
12
|
+
:int, :float,
|
13
|
+
:str, :sym, :regexp,
|
14
|
+
:array, :hash, :pair, :irange, # may contain nils but they are not nil
|
15
|
+
:dstr, # "String #{interpolation}" mixes :str, :begin
|
16
|
+
:dsym # :"#{foo}"
|
17
|
+
].to_set
|
18
|
+
|
19
|
+
def nice(node)
|
20
|
+
nice_literal(node) || nice_variable(node) || nice_send(node) ||
|
21
|
+
nice_begin(node)
|
22
|
+
end
|
23
|
+
|
24
|
+
def nice_literal(node)
|
25
|
+
NICE_LITERAL_NODE_TYPES.include? node.type
|
26
|
+
end
|
27
|
+
|
28
|
+
def nice_variable(node)
|
29
|
+
node.type == :lvar && scope[node.children.first].nice
|
30
|
+
end
|
31
|
+
|
32
|
+
# Methods that preserve niceness if all their arguments are nice
|
33
|
+
# These are global, called with a nil receiver
|
34
|
+
NICE_GLOBAL_METHODS = {
|
35
|
+
# message, number of arguments
|
36
|
+
:_ => 1,
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
NICE_OPERATORS = {
|
40
|
+
# message, number of arguments (other than receiver)
|
41
|
+
:+ => 1,
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
def nice_send(node)
|
45
|
+
return false unless node.type == :send
|
46
|
+
receiver, message, *args = *node
|
47
|
+
|
48
|
+
if receiver.nil?
|
49
|
+
arity = NICE_GLOBAL_METHODS.fetch(message, -1)
|
50
|
+
else
|
51
|
+
return false unless nice(receiver)
|
52
|
+
arity = NICE_OPERATORS.fetch(message, -1)
|
53
|
+
end
|
54
|
+
return args.size == arity && args.all?{ |a| nice(a) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def nice_begin(node)
|
58
|
+
node.type == :begin && nice(node.children.last)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "parser"
|
2
|
+
|
3
|
+
require_relative "code_histogram"
|
4
|
+
|
5
|
+
class NodeTypeCounter < Parser::Rewriter
|
6
|
+
attr_reader :node_types
|
7
|
+
|
8
|
+
def initialize(filename)
|
9
|
+
@node_types = CodeHistogram.new
|
10
|
+
@filename = filename
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(node)
|
14
|
+
return if node.nil?
|
15
|
+
@node_types.increment(node.type)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def print(io)
|
20
|
+
parser = Parser::CurrentRuby.new
|
21
|
+
buffer = Parser::Source::Buffer.new(@filename)
|
22
|
+
buffer.read
|
23
|
+
ast = parser.parse(buffer)
|
24
|
+
|
25
|
+
process(ast)
|
26
|
+
|
27
|
+
@node_types.print_by_frequency(io)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
require "parser"
|
2
|
+
require "parser/current"
|
3
|
+
require "set"
|
4
|
+
require "unparser"
|
5
|
+
|
6
|
+
require_relative "niceness"
|
7
|
+
require_relative "variable_scope"
|
8
|
+
|
9
|
+
# We have encountered code that does satisfy our simplifying assumptions,
|
10
|
+
# translating it would not be correct.
|
11
|
+
class TooComplexToTranslateError < Exception
|
12
|
+
end
|
13
|
+
|
14
|
+
class ZombieKillerRewriter < Parser::Rewriter
|
15
|
+
include Niceness
|
16
|
+
|
17
|
+
attr_reader :scopes
|
18
|
+
|
19
|
+
def initialize(unsafe: false)
|
20
|
+
@scopes = VariableScopeStack.new
|
21
|
+
@unsafe = unsafe
|
22
|
+
end
|
23
|
+
|
24
|
+
def warning(message)
|
25
|
+
$stderr.puts message if $VERBOSE
|
26
|
+
end
|
27
|
+
|
28
|
+
def rewrite(buffer, ast)
|
29
|
+
super
|
30
|
+
rescue TooComplexToTranslateError
|
31
|
+
warning "(outer scope) is too complex to translate"
|
32
|
+
buffer.source
|
33
|
+
end
|
34
|
+
|
35
|
+
# FIXME
|
36
|
+
# How can we ensure that code modifications do not make some unhandled again?
|
37
|
+
HANDLED_NODE_TYPES = [
|
38
|
+
:alias, # Method alias
|
39
|
+
:and, # &&
|
40
|
+
:and_asgn, # &&=
|
41
|
+
:arg, # One argument
|
42
|
+
:args, # All arguments
|
43
|
+
:back_ref, # Regexp backreference, $`; $&; $'
|
44
|
+
:begin, # A simple sequence
|
45
|
+
:block, # A closure, not just any scope
|
46
|
+
:block_pass, # Pass &foo as an arg which is a block, &:foo
|
47
|
+
:blockarg, # An argument initialized with a block def m(&b)
|
48
|
+
:break, # Break statement
|
49
|
+
:case, # Case statement
|
50
|
+
:casgn, # Constant assignment/definition
|
51
|
+
:cbase, # Base/root of constant tree, ::Foo
|
52
|
+
:class, # Class body
|
53
|
+
:cvar, # Class @@variable
|
54
|
+
:cvasgn, # Class @@variable = assignment
|
55
|
+
:const, # Name of a class/module or name of a value
|
56
|
+
:def, # Method definition
|
57
|
+
:defined?, # defined? statement
|
58
|
+
:defs, # Method definition on self
|
59
|
+
:ensure, # Exception ensuring
|
60
|
+
:for, # For v in enum;
|
61
|
+
:gvar, # Global $variable
|
62
|
+
:gvasgn, # Global $variable = assignment
|
63
|
+
:if, # If and Unless
|
64
|
+
:ivar, # Instance variable value
|
65
|
+
:ivasgn, # Instance variable assignment
|
66
|
+
:kwbegin, # A variant of begin; for rescue and while_post
|
67
|
+
:kwoptarg, # Keyword optional argument, def m(a: 1)
|
68
|
+
:lvar, # Local variable value
|
69
|
+
:lvasgn, # Local variable assignment
|
70
|
+
:masgn, # Multiple assigment: a, b = c, d
|
71
|
+
:mlhs, # Left-hand side of a multiple assigment: a, b = c, d
|
72
|
+
:module, # Module body
|
73
|
+
:next, # Next statement
|
74
|
+
:nil, # nil literal
|
75
|
+
:nth_ref, # Regexp back references: $1, $2...
|
76
|
+
:op_asgn, # a %= b where % is any operator except || &&
|
77
|
+
:optarg, # Optional argument
|
78
|
+
:or, # ||
|
79
|
+
:or_asgn, # ||=
|
80
|
+
:postexe, # END { }
|
81
|
+
:regopt, # options tacked on a :regexp
|
82
|
+
:resbody, # One rescue clause in a :rescue construct
|
83
|
+
:rescue, # Groups the begin and :resbody
|
84
|
+
:restarg, # Rest of arguments, (..., *args)
|
85
|
+
:retry, # Retry a begin-rescue block
|
86
|
+
:return, # Method return
|
87
|
+
:sclass, # Singleton class, class << foo
|
88
|
+
:send, # Send a message AKA Call a method
|
89
|
+
:splat, # Array *splatting
|
90
|
+
:super, # Call the ancestor method
|
91
|
+
:unless, # Unless AKA If-Not
|
92
|
+
:until, # Until AKA While-Not
|
93
|
+
:until_post, # Until with post-condtion
|
94
|
+
:when, # When branch of an Case statement
|
95
|
+
:while, # While loop
|
96
|
+
:while_post, # While loop with post-condition
|
97
|
+
:xstr, # Executed `string`, backticks
|
98
|
+
:yield, # Call the unnamed block
|
99
|
+
:zsuper # Zero argument :super
|
100
|
+
].to_set + NICE_LITERAL_NODE_TYPES
|
101
|
+
|
102
|
+
def process(node)
|
103
|
+
return if node.nil?
|
104
|
+
if ! @unsafe
|
105
|
+
oops(node, RuntimeError.new("Unknown node type #{node.type}")) unless
|
106
|
+
HANDLED_NODE_TYPES.include? node.type
|
107
|
+
end
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
# currently visible scope
|
112
|
+
def scope
|
113
|
+
scopes.innermost
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_new_scope_rescuing_oops(&block)
|
117
|
+
scopes.with_new do
|
118
|
+
block.call
|
119
|
+
end
|
120
|
+
rescue => e
|
121
|
+
oops(node, e)
|
122
|
+
end
|
123
|
+
|
124
|
+
def on_def(node)
|
125
|
+
with_new_scope_rescuing_oops { super }
|
126
|
+
end
|
127
|
+
|
128
|
+
def on_defs(node)
|
129
|
+
with_new_scope_rescuing_oops { super }
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_module(node)
|
133
|
+
with_new_scope_rescuing_oops { super }
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_class(node)
|
137
|
+
with_new_scope_rescuing_oops { super }
|
138
|
+
end
|
139
|
+
|
140
|
+
def on_sclass(node)
|
141
|
+
with_new_scope_rescuing_oops { super }
|
142
|
+
end
|
143
|
+
|
144
|
+
def on_if(node)
|
145
|
+
cond, then_body, else_body = *node
|
146
|
+
process(cond)
|
147
|
+
|
148
|
+
scopes.with_copy do
|
149
|
+
process(then_body)
|
150
|
+
end
|
151
|
+
|
152
|
+
scopes.with_copy do
|
153
|
+
process(else_body)
|
154
|
+
end
|
155
|
+
|
156
|
+
# clean slate
|
157
|
+
scope.clear
|
158
|
+
end
|
159
|
+
|
160
|
+
# def on_unless
|
161
|
+
# Does not exist.
|
162
|
+
# `unless` is parsed as an `if` with then_body and else_body swapped.
|
163
|
+
# Compare with `while` and `until` which cannot do that and thus need
|
164
|
+
# distinct node types.
|
165
|
+
# end
|
166
|
+
|
167
|
+
def on_case(node)
|
168
|
+
expr, *cases = *node
|
169
|
+
process(expr)
|
170
|
+
|
171
|
+
cases.each do |case_|
|
172
|
+
scopes.with_copy do
|
173
|
+
process(case_)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# clean slate
|
178
|
+
scope.clear
|
179
|
+
end
|
180
|
+
|
181
|
+
def on_lvasgn(node)
|
182
|
+
super
|
183
|
+
name, value = * node
|
184
|
+
return if value.nil? # and-asgn, or-asgn, resbody do this
|
185
|
+
scope[name].nice = nice(value)
|
186
|
+
end
|
187
|
+
|
188
|
+
def on_and_asgn(node)
|
189
|
+
super
|
190
|
+
var, value = * node
|
191
|
+
return if var.type != :lvasgn
|
192
|
+
name = var.children[0]
|
193
|
+
|
194
|
+
scope[name].nice &&= nice(value)
|
195
|
+
end
|
196
|
+
|
197
|
+
def on_or_asgn(node)
|
198
|
+
super
|
199
|
+
var, value = * node
|
200
|
+
return if var.type != :lvasgn
|
201
|
+
name = var.children[0]
|
202
|
+
|
203
|
+
scope[name].nice ||= nice(value)
|
204
|
+
end
|
205
|
+
|
206
|
+
def on_send(node)
|
207
|
+
super
|
208
|
+
if is_call(node, :Ops, :add)
|
209
|
+
new_op = :+
|
210
|
+
|
211
|
+
_ops, _add, a, b = *node
|
212
|
+
if nice(a) && nice(b)
|
213
|
+
replace_node node, Parser::AST::Node.new(:send, [a, new_op, b])
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def on_block(node)
|
219
|
+
# ignore body, clean slate
|
220
|
+
scope.clear
|
221
|
+
end
|
222
|
+
alias_method :on_for, :on_block
|
223
|
+
|
224
|
+
def on_while(node)
|
225
|
+
# ignore both condition and body,
|
226
|
+
# with a simplistic scope we cannot handle them
|
227
|
+
|
228
|
+
# clean slate
|
229
|
+
scope.clear
|
230
|
+
end
|
231
|
+
alias_method :on_until, :on_while
|
232
|
+
|
233
|
+
# Exceptions:
|
234
|
+
# `raise` is an ordinary :send for the parser
|
235
|
+
|
236
|
+
def on_rescue(node)
|
237
|
+
# (:rescue, begin-block, resbody..., else-block-or-nil)
|
238
|
+
begin_body, *rescue_bodies, else_body = *node
|
239
|
+
|
240
|
+
@source_rewriter.transaction do
|
241
|
+
process(begin_body)
|
242
|
+
process(else_body)
|
243
|
+
rescue_bodies.each do |r|
|
244
|
+
process(r)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
rescue TooComplexToTranslateError
|
248
|
+
warning "begin-rescue is too complex to translate due to a retry"
|
249
|
+
end
|
250
|
+
|
251
|
+
def on_resbody(node)
|
252
|
+
# How it is parsed:
|
253
|
+
# (:resbody, exception-types-or-nil, exception-variable-or-nil, body)
|
254
|
+
# exception-types is an :array
|
255
|
+
# exception-variable is a (:lvasgn, name), without a value
|
256
|
+
|
257
|
+
# A rescue means that *some* previous code was skipped. We know nothing.
|
258
|
+
# We could process the resbodies individually,
|
259
|
+
# and join begin-block with else-block, but it is little worth
|
260
|
+
# because they will contain few zombies.
|
261
|
+
scope.clear
|
262
|
+
super
|
263
|
+
end
|
264
|
+
|
265
|
+
def on_ensure(node)
|
266
|
+
# (:ensure, guarded-code, ensuring-code)
|
267
|
+
# guarded-code may be a :rescue or not
|
268
|
+
|
269
|
+
scope.clear
|
270
|
+
end
|
271
|
+
|
272
|
+
def on_retry(node)
|
273
|
+
# that makes the :rescue a loop, top-down data-flow fails
|
274
|
+
raise TooComplexToTranslateError
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def oops(node, exception)
|
280
|
+
puts "Node exception @ #{node.loc.expression}"
|
281
|
+
puts "Offending node: #{node.inspect}"
|
282
|
+
raise exception
|
283
|
+
end
|
284
|
+
|
285
|
+
def is_call(node, namespace, message)
|
286
|
+
n_receiver, n_message = *node
|
287
|
+
n_receiver && n_receiver.type == :const &&
|
288
|
+
n_receiver.children[0] == nil &&
|
289
|
+
n_receiver.children[1] == namespace &&
|
290
|
+
n_message == message
|
291
|
+
end
|
292
|
+
|
293
|
+
def replace_node(old_node, new_node)
|
294
|
+
source_range = old_node.loc.expression
|
295
|
+
if !contains_comment?(source_range.source)
|
296
|
+
replace(source_range, Unparser.unparse(new_node))
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def contains_comment?(string)
|
301
|
+
/^[^'"\n]*#/.match(string)
|
302
|
+
end
|
303
|
+
end
|