Class: ZombieKillerRewriter

Inherits:
Parser::Rewriter
  • Object
show all
Includes:
Niceness
Defined in:
lib/zombie_killer/rewriter.rb

Overview

The main rewriter

Constant Summary collapse

HANDLED_NODE_TYPES =

FIXME How can we ensure that code modifications do not make some unhandled again?

[
  :alias,                     # Method alias
  :and,                       # &&
  :and_asgn,                  # &&=
  :arg,                       # One argument
  :args,                      # All arguments
  :back_ref,                  # Regexp backreference, $`; $&; $'
  :begin,                     # A simple sequence
  :block,                     # A closure, not just any scope
  :block_pass,                # Pass &foo as an arg which is a block, &:foo
  :blockarg,                  # An argument initialized with a block def m(&b)
  :break,                     # Break statement
  :case,                      # Case statement
  :casgn,                     # Constant assignment/definition
  :cbase,                     # Base/root of constant tree, ::Foo
  :class,                     # Class body
  :cvar,                      # Class @@variable
  :cvasgn,                    # Class @@variable = assignment
  :const,                     # Name of a class/module or name of a value
  :def,                       # Method definition
  :defined?,                  # defined? statement
  :defs,                      # Method definition on self
  :ensure,                    # Exception ensuring
  :for,                       # For v in enum;
  :gvar,                      # Global $variable
  :gvasgn,                    # Global $variable = assignment
  :if,                        # If and Unless
  :ivar,                      # Instance variable value
  :ivasgn,                    # Instance variable assignment
  :kwarg,                     # Keyword argument, def m(a:)
  :kwbegin,                   # A variant of begin; for rescue and while_post
  :kwoptarg,                  # Keyword optional argument, def m(a: 1)
  :kwrestarg,                 # Rest of keyword arguments, def m(**kwargs)
  :kwsplat,                   # Hash **splatting
  :lvar,                      # Local variable value
  :lvasgn,                    # Local variable assignment
  :match_with_lvasgn,         # /regex/ =~ value
  :masgn,                     # Multiple assigment: a, b = c, d
  :mlhs,                      # Left-hand side of a multiple assigment: a, b = c, d
  :module,                    # Module body
  :next,                      # Next statement
  :nil,                       # nil literal
  :nth_ref,                   # Regexp back references: $1, $2...
  :op_asgn,                   # a %= b where % is any operator except || &&
  :optarg,                    # Optional argument
  :or,                        # ||
  :or_asgn,                   # ||=
  :postexe,                   # END { }
  :regopt,                    # options tacked on a :regexp
  :resbody,                   # One rescue clause in a :rescue construct
  :rescue,                    # Groups the begin and :resbody
  :restarg,                   # Rest of arguments, (..., *args)
  :retry,                     # Retry a begin-rescue block
  :return,                    # Method return
  :sclass,                    # Singleton class, class << foo
  :send,                      # Send a message AKA Call a method
  :splat,                     # Array *splatting
  :super,                     # Call the ancestor method
  :unless,                    # Unless AKA If-Not
  :until,                     # Until AKA While-Not
  :until_post,                # Until with post-condtion
  :when,                      # When branch of an Case statement
  :while,                     # While loop
  :while_post,                # While loop with post-condition
  :xstr,                      # Executed `string`, backticks
  :yield,                     # Call the unnamed block
  :zsuper                     # Zero argument :super
].to_set + NICE_LITERAL_NODE_TYPES

Constants included from Niceness

Niceness::NICE_GLOBAL_METHODS, Niceness::NICE_LITERAL_NODE_TYPES, Niceness::NICE_OPERATORS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Niceness

#nice, #nice_begin, #nice_literal, #nice_send, #nice_variable

Constructor Details

#initialize(unsafe: false) ⇒ ZombieKillerRewriter

Returns a new instance of ZombieKillerRewriter.



30
31
32
33
# File 'lib/zombie_killer/rewriter.rb', line 30

def initialize(unsafe: false)
  @scopes = VariableScopeStack.new
  @unsafe = unsafe
end

Instance Attribute Details

#scopesObject (readonly)

Returns the value of attribute scopes.



28
29
30
# File 'lib/zombie_killer/rewriter.rb', line 28

def scopes
  @scopes
end

Instance Method Details

#on_and_asgn(node) ⇒ Object



205
206
207
208
209
210
211
212
# File 'lib/zombie_killer/rewriter.rb', line 205

def on_and_asgn(node)
  super
  var, value = * node
  return if var.type != :lvasgn
  name = var.children[0]

  scope[name].nice &&= nice(value)
end

#on_block(node) ⇒ Object Also known as: on_for



235
236
237
238
# File 'lib/zombie_killer/rewriter.rb', line 235

def on_block(node)
  # ignore body, clean slate
  scope.clear
end

#on_case(node) ⇒ Object

end



184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/zombie_killer/rewriter.rb', line 184

def on_case(node)
  expr, *cases = *node
  process(expr)

  cases.each do |case_|
    scopes.with_copy do
      process(case_)
    end
  end

  # clean slate
  scope.clear
end

#on_class(node) ⇒ Object



153
154
155
# File 'lib/zombie_killer/rewriter.rb', line 153

def on_class(node)
  with_new_scope_rescuing_oops { super }
end

#on_def(node) ⇒ Object



141
142
143
# File 'lib/zombie_killer/rewriter.rb', line 141

def on_def(node)
  with_new_scope_rescuing_oops { super }
end

#on_defs(node) ⇒ Object



145
146
147
# File 'lib/zombie_killer/rewriter.rb', line 145

def on_defs(node)
  with_new_scope_rescuing_oops { super }
end

#on_ensure(node) ⇒ Object



282
283
284
285
286
287
# File 'lib/zombie_killer/rewriter.rb', line 282

def on_ensure(node)
  # (:ensure, guarded-code, ensuring-code)
  # guarded-code may be a :rescue or not

  scope.clear
end

#on_if(node) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/zombie_killer/rewriter.rb', line 161

def on_if(node)
  cond, then_body, else_body = *node
  process(cond)

  scopes.with_copy do
    process(then_body)
  end

  scopes.with_copy do
    process(else_body)
  end

  # clean slate
  scope.clear
end

#on_lvasgn(node) ⇒ Object



198
199
200
201
202
203
# File 'lib/zombie_killer/rewriter.rb', line 198

def on_lvasgn(node)
  super
  name, value = * node
  return if value.nil? # and-asgn, or-asgn, resbody do this
  scope[name].nice = nice(value)
end

#on_module(node) ⇒ Object



149
150
151
# File 'lib/zombie_killer/rewriter.rb', line 149

def on_module(node)
  with_new_scope_rescuing_oops { super }
end

#on_or_asgn(node) ⇒ Object



214
215
216
217
218
219
220
221
# File 'lib/zombie_killer/rewriter.rb', line 214

def on_or_asgn(node)
  super
  var, value = * node
  return if var.type != :lvasgn
  name = var.children[0]

  scope[name].nice ||= nice(value)
end

#on_resbody(node) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/zombie_killer/rewriter.rb', line 268

def on_resbody(node)
  # How it is parsed:
  # (:resbody, exception-types-or-nil, exception-variable-or-nil, body)
  # exception-types is an :array
  # exception-variable is a (:lvasgn, name), without a value

  # A rescue means that *some* previous code was skipped. We know nothing.
  # We could process the resbodies individually,
  # and join begin-block with else-block, but it is little worth
  # because they will contain few zombies.
  scope.clear
  super
end

#on_rescue(node) ⇒ Object

Exceptions: ‘raise` is an ordinary :send for the parser



253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/zombie_killer/rewriter.rb', line 253

def on_rescue(node)
  # (:rescue, begin-block, resbody..., else-block-or-nil)
  begin_body, *rescue_bodies, else_body = *node

  @source_rewriter.transaction do
    process(begin_body)
    process(else_body)
    rescue_bodies.each do |r|
      process(r)
    end
  end
rescue TooComplexToTranslateError
  warning "begin-rescue is too complex to translate due to a retry"
end

#on_retry(node) ⇒ Object



289
290
291
292
# File 'lib/zombie_killer/rewriter.rb', line 289

def on_retry(node)
  # that makes the :rescue a loop, top-down data-flow fails
  raise TooComplexToTranslateError
end

#on_sclass(node) ⇒ Object



157
158
159
# File 'lib/zombie_killer/rewriter.rb', line 157

def on_sclass(node)
  with_new_scope_rescuing_oops { super }
end

#on_send(node) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
# File 'lib/zombie_killer/rewriter.rb', line 223

def on_send(node)
  super
  if is_call(node, :Ops, :add)
    new_op = :+

    _ops, _add, a, b = *node
    if nice(a) && nice(b)
      replace_node node, Parser::AST::Node.new(:send, [a, new_op, b])
    end
  end
end

#on_while(node) ⇒ Object Also known as: on_until



241
242
243
244
245
246
247
# File 'lib/zombie_killer/rewriter.rb', line 241

def on_while(node)
  # ignore both condition and body,
  # with a simplistic scope we cannot handle them

  # clean slate
  scope.clear
end

#process(node) ⇒ Object



117
118
119
120
121
122
123
124
# File 'lib/zombie_killer/rewriter.rb', line 117

def process(node)
  return if node.nil?
  unless @unsafe
    raise NodeError.new("Unknown node type #{node.type}", node) unless
      HANDLED_NODE_TYPES.include? node.type
  end
  super
end

#rewrite(buffer, ast) ⇒ Object



39
40
41
42
43
44
# File 'lib/zombie_killer/rewriter.rb', line 39

def rewrite(buffer, ast)
  super
rescue TooComplexToTranslateError
  warning "(outer scope) is too complex to translate"
  buffer.source
end

#scopeObject

currently visible scope



127
128
129
# File 'lib/zombie_killer/rewriter.rb', line 127

def scope
  scopes.innermost
end

#warning(message) ⇒ Object



35
36
37
# File 'lib/zombie_killer/rewriter.rb', line 35

def warning(message)
  $stderr.puts message if $VERBOSE
end

#with_new_scope_rescuing_oops(&block) ⇒ Object



131
132
133
134
135
136
137
138
139
# File 'lib/zombie_killer/rewriter.rb', line 131

def with_new_scope_rescuing_oops(&block)
  scopes.with_new do
    block.call
  end
rescue NodeError => e
  puts e
  puts "Node exception @ #{e.node.loc.expression}"
  puts "Offending node: #{e.node.inspect}"
end