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