alias_processor.rb 12.6 KB
Newer Older
J
Justin Collins 已提交
1
require 'brakeman/util'
2
require 'ruby_parser/bm_sexp_processor'
J
Justin Collins 已提交
3
require 'brakeman/processors/lib/processor_helper'
J
Justin 已提交
4 5 6 7

#Returns an s-expression with aliases replaced with their value.
#This does not preserve semantics (due to side effects, etc.), but it makes
#processing easier when searching for various things.
8
class Brakeman::AliasProcessor < Brakeman::SexpProcessor
J
Justin Collins 已提交
9 10
  include Brakeman::ProcessorHelper
  include Brakeman::Util
J
Justin 已提交
11 12 13 14 15 16 17 18

  attr_reader :result

  #Returns a new AliasProcessor with an empty environment.
  #
  #The recommended usage is:
  #
  # AliasProcessor.new.process_safely src
19
  def initialize tracker = nil
J
Justin 已提交
20 21
    super()
    @env = SexpProcessor::Environment.new
J
Justin Collins 已提交
22
    @inside_if = false
23
    @ignore_ifs = false
24
    @exp_context = []
J
Justin Collins 已提交
25
    @current_module = nil
26
    @tracker = tracker #set in subclass as necessary
J
Justin 已提交
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
    set_env_defaults
  end

  #This method processes the given Sexp, but copies it first so
  #the original argument will not be modified.
  #
  #_set_env_ should be an instance of SexpProcessor::Environment. If provided,
  #it will be used as the starting environment.
  #
  #This method returns a new Sexp with variables replaced with their values,
  #where possible.
  def process_safely src, set_env = nil
    @env = Marshal.load(Marshal.dump(set_env)) if set_env
    @result = src.deep_clone
    process @result

    #Process again to propogate replaced variables and process more.
    #For example,
    #  x = [1,2]
    #  y = [3,4]
    #  z = x + y
    #
    #After first pass:
    #
    #  z = [1,2] + [3,4]
    #
    #After second pass:
    #
    #  z = [1,2,3,4]
    if set_env
      @env = set_env
    else
      @env = SexpProcessor::Environment.new
    end

    process @result

    @result
  end

  #Process a Sexp. If the Sexp has a value associated with it in the
  #environment, that value will be returned. 
  def process_default exp
70 71
    @exp_context.push exp

J
Justin 已提交
72 73
    begin
      exp.each_with_index do |e, i|
74 75
        next if i == 0

J
Justin 已提交
76 77 78 79 80 81 82 83 84 85 86
        if sexp? e and not e.empty?
          exp[i] = process e
        else
          e
        end
      end
    rescue Exception => err
      @tracker.error err if @tracker
    end

    #Generic replace
87 88
    if replacement = env[exp] and not duplicate? replacement
      result = set_line replacement.deep_clone, exp.line
J
Justin 已提交
89
    else
90
      result = exp
J
Justin 已提交
91
    end
92 93 94 95

    @exp_context.pop

    result
J
Justin 已提交
96 97 98 99
  end

  #Process a method call.
  def process_call exp
J
Justin Collins 已提交
100
    target_var = exp.target
J
Justin 已提交
101 102 103
    exp = process_default exp

    #In case it is replaced with something else
104 105 106
    unless call? exp
      return exp
    end
J
Justin 已提交
107

J
Justin Collins 已提交
108 109
    target = exp.target
    method = exp.method
J
Justin 已提交
110
    args = exp[3]
111
    first_arg = exp.first_arg
J
Justin 已提交
112 113 114 115 116

    #See if it is possible to simplify some basic cases
    #of addition/concatenation.
    case method
    when :+
J
Justin Collins 已提交
117 118
      if array? target and array? first_arg
        joined = join_arrays target, first_arg 
J
Justin 已提交
119 120
        joined.line(exp.line)
        exp = joined
J
Justin Collins 已提交
121
      elsif string? first_arg
122
        if string? target # "blah" + "blah"
J
Justin Collins 已提交
123
          joined = join_strings target, first_arg
124 125
          joined.line(exp.line)
          exp = joined
126 127
        elsif call? target and target.method == :+ and string? target.first_arg
          joined = join_strings target.first_arg, first_arg
128
          joined.line(exp.line)
129
          target.first_arg = joined
130 131
          exp = target
        end
J
Justin Collins 已提交
132
      elsif number? first_arg
133
        if number? target
J
Justin Collins 已提交
134
          exp = Sexp.new(:lit, target.value + first_arg.value)
135
        elsif call? target and target.method == :+ and number? target.first_arg
136
          target.first_arg = Sexp.new(:lit, target.first_arg.value + first_arg.value)
137 138
          exp = target
        end
J
Justin 已提交
139
      end
J
Justin Collins 已提交
140
    when :-
J
Justin Collins 已提交
141 142
      if number? target and number? first_arg
        exp = Sexp.new(:lit, target.value - first_arg.value)
J
Justin Collins 已提交
143 144
      end
    when :*
J
Justin Collins 已提交
145 146
      if number? target and number? first_arg
        exp = Sexp.new(:lit, target.value * first_arg.value)
J
Justin Collins 已提交
147 148
      end
    when :/
J
Justin Collins 已提交
149 150
      if number? target and number? first_arg
        exp = Sexp.new(:lit, target.value / first_arg.value)
J
Justin Collins 已提交
151
      end
J
Justin 已提交
152 153
    when :[]
      if array? target
J
Justin Collins 已提交
154
        temp_exp = process_array_access target, exp.args
J
Justin 已提交
155 156
        exp = temp_exp if temp_exp
      elsif hash? target
J
Justin Collins 已提交
157
        temp_exp = process_hash_access target, exp.args
J
Justin 已提交
158 159 160
        exp = temp_exp if temp_exp
      end
    when :merge!, :update
J
Justin Collins 已提交
161 162
      if hash? target and hash? first_arg
         target = process_hash_merge! target, first_arg
J
Justin 已提交
163 164 165 166
         env[target_var] = target
         return target
      end
    when :merge
J
Justin Collins 已提交
167 168
      if hash? target and hash? first_arg
        return process_hash_merge(target, first_arg)
J
Justin 已提交
169
      end
J
Justin Collins 已提交
170
    when :<<
J
Justin Collins 已提交
171 172
      if string? target and string? first_arg
        target.value << first_arg.value
J
Justin Collins 已提交
173 174 175
        env[target_var] = target
        return target
      elsif array? target
J
Justin Collins 已提交
176
        target << first_arg
J
Justin Collins 已提交
177 178 179 180
        env[target_var] = target
        return target
      else
        target = find_push_target exp
J
Justin Collins 已提交
181
        env[target] = exp unless target.nil? #Happens in TemplateAliasProcessor
J
Justin Collins 已提交
182
      end
J
Justin 已提交
183 184 185 186 187 188 189 190
    end

    exp
  end

  #Process a new scope.
  def process_scope exp
    env.scope do
J
Justin Collins 已提交
191
      process exp.block
J
Justin 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    end
    exp
  end

  #Start new scope for block.
  def process_block exp
    env.scope do
      process_default exp
    end
  end

  #Process a method definition.
  def process_methdef exp
    env.scope do
      set_env_defaults
J
Justin Collins 已提交
207
      process exp.body
J
Justin 已提交
208 209 210 211 212 213 214 215
    end
    exp
  end

  #Process a method definition on self.
  def process_selfdef exp
    env.scope do
      set_env_defaults
J
Justin Collins 已提交
216
      process exp.body
J
Justin 已提交
217 218 219 220 221 222 223 224 225 226
    end
    exp
  end

  alias process_defn process_methdef
  alias process_defs process_selfdef

  #Local assignment
  # x = 1
  def process_lasgn exp
J
Justin Collins 已提交
227
    exp.rhs = process exp.rhs if sexp? exp.rhs
J
Justin Collins 已提交
228
    return exp if exp.rhs.nil?
229

J
Justin Collins 已提交
230
    local = Sexp.new(:lvar, exp.lhs).line(exp.line || -2)
231

J
Justin Collins 已提交
232
    if @inside_if and val = env[local]
233
      #avoid setting to value it already is (e.g. "1 or 1")
J
Justin Collins 已提交
234 235
      if val != exp.rhs and val[1] != exp.rhs and val[2] != exp.rhs
        env[local] = Sexp.new(:or, val, exp.rhs).line(exp.line || -2)
J
Justin Collins 已提交
236
      end
237
    else
J
Justin Collins 已提交
238
      env[local] = exp.rhs
239 240
    end

J
Justin 已提交
241 242 243 244 245 246
    exp
  end

  #Instance variable assignment
  # @x = 1
  def process_iasgn exp
J
Justin Collins 已提交
247 248
    exp.rhs = process exp.rhs
    ivar = Sexp.new(:ivar, exp.lhs).line(exp.line)
249

J
Justin Collins 已提交
250
    if @inside_if and val = env[ivar]
J
Justin Collins 已提交
251 252
      if val != exp.rhs
        env[ivar] = Sexp.new(:or, val, exp.rhs).line(exp.line)
J
Justin Collins 已提交
253
      end
254
    else
J
Justin Collins 已提交
255
      env[ivar] = exp.rhs
256 257
    end

J
Justin 已提交
258 259 260 261 262 263
    exp
  end

  #Global assignment
  # $x = 1
  def process_gasgn exp
J
Justin Collins 已提交
264
    match = Sexp.new(:gvar, exp.lhs)
J
Justin Collins 已提交
265
    value = exp.rhs = process(exp.rhs)
266

J
Justin Collins 已提交
267 268 269 270
    if @inside_if and val = env[match]
      if val != value
        env[match] = Sexp.new(:or, env[match], value)
      end
271 272 273 274
    else
      env[match] = value
    end

J
Justin 已提交
275 276 277 278 279 280
    exp
  end

  #Class variable assignment
  # @@x = 1
  def process_cvdecl exp
J
Justin Collins 已提交
281 282
    match = Sexp.new(:cvar, exp.lhs)
    value = exp.rhs = process(exp.rhs)
283
    
J
Justin Collins 已提交
284 285 286 287
    if @inside_if and val = env[match]
      if val != value
        env[match] = Sexp.new(:or, env[match], value)
      end
288 289 290 291
    else
      env[match] = value
    end

J
Justin 已提交
292 293 294 295 296 297 298 299
    exp
  end

  #'Attribute' assignment
  # x.y = 1
  #or
  # x[:y] = 1
  def process_attrasgn exp
J
Justin Collins 已提交
300
    tar_variable = exp.target
J
Justin Collins 已提交
301
    target = exp.target = process(exp.target)
J
Justin Collins 已提交
302 303
    method = exp.method
    args = exp.args
J
Spacing  
Justin Collins 已提交
304

J
Justin 已提交
305
    if method == :[]=
306 307
      index = exp.first_arg = process(args.first)
      value = exp.second_arg = process(args.second)
J
Justin 已提交
308 309 310 311 312 313 314
      match = Sexp.new(:call, target, :[], Sexp.new(:arglist, index))
      env[match] = value

      if hash? target
        env[tar_variable] = hash_insert target.deep_clone, index, value
      end
    elsif method.to_s[-1,1] == "="
315
      value = exp.first_arg = process(args.first)
J
Justin 已提交
316 317
      #This is what we'll replace with the value
      match = Sexp.new(:call, target, method.to_s[0..-2].to_sym, Sexp.new(:arglist))
318

J
Justin Collins 已提交
319 320 321 322
      if @inside_if and val = env[match]
        if val != value
          env[match] = Sexp.new(:or, env[match], value)
        end
323 324 325
      else
        env[match] = value
      end
J
Justin 已提交
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
    else
      raise "Unrecognized assignment: #{exp}"
    end
    exp
  end

  #Merge values into hash when processing
  #
  # h.merge! :something => "value"
  def process_hash_merge! hash, args
    hash = hash.deep_clone
    hash_iterate args do |key, replacement|
      hash_insert hash, key, replacement
      match = Sexp.new(:call, hash, :[], Sexp.new(:arglist, key))
      env[match] = replacement
    end
    hash
  end

  #Return a new hash Sexp with the given values merged into it.
  #
  #+args+ should be a hash Sexp as well.
  def process_hash_merge hash, args
    hash = hash.deep_clone
    hash_iterate args do |key, replacement|
      hash_insert hash, key, replacement
    end
    hash
  end

  #Assignments like this
  # x[:y] ||= 1
  def process_op_asgn1 exp
    return process_default(exp) if exp[3] != :"||"

    target = exp[1] = process(exp[1])
    index = exp[2][1] = process(exp[2][1])
    value = exp[4] = process(exp[4])
    match = Sexp.new(:call, target, :[], Sexp.new(:arglist, index))

    unless env[match]
367 368 369 370 371
      if request_value? target
        env[match] = Sexp.new(:or, match, value)
      else
        env[match] = value
      end
J
Justin 已提交
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
    end

    exp
  end

  #Assignments like this
  # x.y ||= 1
  def process_op_asgn2 exp
    return process_default(exp) if exp[3] != :"||"

    target = exp[1] = process(exp[1])
    value = exp[4] = process(exp[4])
    method = exp[2]

    match = Sexp.new(:call, target, method.to_s[0..-2].to_sym, Sexp.new(:arglist))

    unless env[match]
      env[match] = value
    end

    exp
  end

J
Justin Collins 已提交
395 396 397 398
  def process_svalue exp
    exp[1]
  end

J
Justin 已提交
399 400 401
  #Constant assignments like
  # BIG_CONSTANT = 234810983
  def process_cdecl exp
J
Justin Collins 已提交
402 403
    if sexp? exp.rhs
      exp.rhs = process exp.rhs
J
Justin 已提交
404 405
    end

J
Justin Collins 已提交
406 407
    if exp.lhs.is_a? Symbol
      match = Sexp.new(:const, exp.lhs)
J
Justin 已提交
408
    else
J
Justin Collins 已提交
409
      match = exp.lhs
J
Justin 已提交
410 411
    end

J
Justin Collins 已提交
412
    env[match] = exp.rhs
J
Justin 已提交
413 414 415 416

    exp
  end

417 418
  #Sets @inside_if = true
  def process_if exp
419 420
    @ignore_ifs ||= @tracker && @tracker.options[:ignore_ifs]

J
Justin Collins 已提交
421
    condition = process exp.condition
422 423

    if true? condition
J
Justin Collins 已提交
424
      exps = [exp.then_clause]
425 426 427 428 429
    elsif false? condition
      exps = exp[3..-1]
    else
      exps = exp[2..-1]
    end
430

J
Justin Collins 已提交
431
    was_inside = @inside_if
432
    @inside_if = !@ignore_ifs
J
Justin Collins 已提交
433

434
    exps.each do |e|
435 436 437 438 439 440 441
      if sexp? e
        if e.node_type == :block
          process_default e #avoid creating new scope
        else
          process e
        end
      end
442 443 444 445 446 447 448
    end

    @inside_if = was_inside

    exp
  end

J
Justin 已提交
449 450 451 452
  #Process single integer access to an array. 
  #
  #Returns the value inside the array, if possible.
  def process_array_access target, args
453 454
    if args.length == 1 and integer? args.first
      index = args.first.value
455 456

      #Have to do this because first element is :array and we have to skip it
457
      target[1..-1][index]
J
Justin 已提交
458 459 460 461 462 463 464 465 466 467 468
    else
      nil
    end
  end

  #Process hash access by returning the value associated
  #with the given arguments.
  def process_hash_access target, args
    if args.length == 1
      index = args[0]

469 470 471 472
      hash_access(target, index)
    else
      nil
    end
J
Justin 已提交
473 474 475 476 477 478 479 480 481 482 483 484 485
  end

  #Join two array literals into one.
  def join_arrays array1, array2
    result = Sexp.new(:array)
    result.concat array1[1..-1]
    result.concat array2[1..-1]
  end

  #Join two string literals into one.
  def join_strings string1, string2
    result = Sexp.new(:str)
    result[1] = string1[1] + string2[1]
486 487 488 489 490
    if result[1].length > 50
      string1
    else
      result
    end
J
Justin 已提交
491 492 493 494
  end

  #Returns a new SexpProcessor::Environment containing only instance variables.
  #This is useful, for example, when processing views.
495
  def only_ivars include_request_vars = false
J
Justin 已提交
496
    res = SexpProcessor::Environment.new
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511

    if include_request_vars
      env.all.each do |k, v|
        #TODO Why would this have nil values?
        if (k.node_type == :ivar or request_value? k) and not v.nil?
          res[k] = v.dup
        end
      end
    else
      env.all.each do |k, v|
        #TODO Why would this have nil values?
        if k.node_type == :ivar and not v.nil?
          res[k] = v.dup
        end
      end
J
Justin 已提交
512
    end
513

J
Justin 已提交
514 515 516 517 518 519 520
    res
  end

  #Set line nunber for +exp+ and every Sexp it contains. Used when replacing
  #expressions, so warnings indicate the correct line.
  def set_line exp, line_number
    if sexp? exp
521 522
      exp.original_line(exp.original_line || exp.line)
      exp.line line_number
J
Justin 已提交
523 524 525 526 527 528 529
      exp.each do |e|
        set_line e, line_number
      end
    end

    exp
  end
J
Justin Collins 已提交
530 531 532 533 534 535 536 537 538

  #Finds the inner most call target which is not the target of a call to <<
  def find_push_target exp
    if call? exp and exp[2] == :<<
      find_push_target exp[1]
    else
      exp
    end
  end
539

540 541 542
  def duplicate? exp
    @exp_context[0..-2].reverse_each do |e|
      return true if exp == e 
543 544 545 546
    end

    false
  end
J
Justin 已提交
547
end