zombie-killer 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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,4 @@
1
+ require_relative "zombie_killer/killer"
2
+ require_relative "zombie_killer/node_type_counter"
3
+ require_relative "zombie_killer/rewriter"
4
+ require_relative "zombie_killer/version"
@@ -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