class JSInstrument::Instrumenter

Attributes

batch_text[RW]
commit_hash[RW]
data_compress_method[RW]
js_object_name[RW]
project_name[RW]
src_filename[RW]
upload_url[RW]

Public Instance Methods

instrument(src, filename) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 20
def instrument src, filename
  raise 'Filename not set' unless filename
  self.src_filename = filename
  ast = RKelly::Parser.new.parse src
  raise 'Parse source failed.' unless ast
  ast, instrumented_lines, instrumented_func_lines, instrumented_branch_lines = instrument_ast ast
  instrumented_src = ast.to_ecma

  inserting = File.open(File.dirname(__FILE__) + '/inserting.js').read

  bindings = {
    js_object_name: self.js_object_name,
    upload_url: self.upload_url,
    src_filename: self.src_filename,
    project_name: self.project_name,
    commit_hash: self.commit_hash,
    batch_text: self.batch_text,
    data_compress_method: self.data_compress_method,
    instrumented_lines: instrumented_lines,
    instrumented_func_lines: instrumented_func_lines,
    instrumented_branch_lines: instrumented_branch_lines
  }

  (inserting % bindings) + instrumented_src
end
instrument_ast(ast) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 46
def instrument_ast ast
  # build parent attr for all nodes in ast
  #
  ast = RKelly::Visitors::ParentBuilder.new.build ast
  instrumented_lines = Set.new
  instrumented_func_lines = Set.new
  instrumented_branch_lines = Set.new
  ast.each do |node|
    parent = node.parent
    line = node.line
    node_classname = get_node_classname node
    parent_classname = get_node_classname parent

    # instrument only if we can get the line no. and we haven't instrumented it
    
    # line coverage
    #
    if line and not instrumented_lines.member? line
      if ['ExpressionStatement', 'EmptyStatement', 'DoWhile', 'While', 'For', 'ForIn', 'Continue',
         'VarDecl', 'Switch', 'Break', 'Throw', 'Return', 'If', 'VarStatement'].index node_classname

        if 'DoWhile' == parent_classname
          if parent.left.eql? node
            instrumented_lines.add line
            parent.left = get_hitline_call_block node
          end
        # in RKelly, ConditionalNode extends IfNode
        # which is so annoying
        # I mean why they mix a expression and a statement?
        elsif ['If', 'While', 'For', 'ForIn', 'With'].index parent_classname
          if parent.value.eql? node
            instrumented_lines.add line
            parent.value = get_hitline_call_block node
          elsif (parent.respond_to? :else) and parent.else.eql? node
            instrumented_lines.add line
            parent.else = get_hitline_call_block node
          end
        elsif ['CaseBlock', 'Label'].index parent_classname
          # do nothing here
        elsif 'SourceElements' == parent_classname
          if insert_hitline_before node
            instrumented_lines.add line
          end
        end
      elsif ['With', 'Try'].index node_classname
        if 'SourceElements' == parent_classname
          if insert_hitline_before node
            instrumented_lines.add line
          end
        end
      end
    end

    # function coverage
    #
    if line and not instrumented_func_lines.member? line
      # function declaration and anonymous function
      if ['FunctionDecl', 'FunctionExpr'].index node_classname
        if node.function_body
          insert_hitfunc_in node.function_body, line
          instrumented_func_lines.add line
        end
      end
    end

    # branch coverage
    #
    if line and not instrumented_branch_lines.member? line
      # if else
      if 'Block' == node_classname
        if 'If' == parent_classname
          if parent.value.eql? node
            instrumented_branch_lines.add line
            insert_hitbranch_in node
          elsif parent.else.eql? node
            instrumented_branch_lines.add line
            insert_hitbranch_in node
          end
        end
      # switch case
      # CaseClause node has the same structure with BlockNode
      # so we can share the instrument logic
      elsif 'CaseClause' == node_classname
        if node.value
          instrumented_branch_lines.add line
          insert_hitbranch_in node
        end
      end
    end
  end
  [ast, instrumented_lines.sort, instrumented_func_lines.sort, instrumented_branch_lines.sort]
end

Private Instance Methods

get_hit_call(line, func) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 144
def get_hit_call line, func
  ExpressionStatementNode.new(
    FunctionCallNode.new(
      ResolveNode.new("#{self.js_object_name}.#{func}"),
      ArgumentsNode.new([
        StringNode.new("\"#{self.src_filename}\""),
        NumberNode.new(line)
      ])
    )
  )
end
get_hitline_call_block(node) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 156
def get_hitline_call_block node
  BlockNode.new(
    SourceElementsNode.new([
      get_hit_call(node.line, "hit_line"),
      node
    ])
  )
end
get_node_classname(node) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 140
def get_node_classname node
  node.class.name.split(/::/)[-1].sub('Node', '')
end
insert_hitbranch_in(block_node) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 188
def insert_hitbranch_in block_node
  # Block.value(SourceElement).value(array).unshift hit_func
  block_node.value.value.unshift get_hit_call(block_node.line, "hit_branch")
end
insert_hitfunc_in(function_body_node, line) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 183
def insert_hitfunc_in function_body_node, line
  # FunctionBody.value(SourceElement).value(array).unshift hit_func
  function_body_node.value.value.unshift get_hit_call(line, "hit_func")
end
insert_hitline_before(node) click to toggle source
# File lib/jsinstrument/instrumenter.rb, line 165
def insert_hitline_before node
  # get the original node line
  #
  line = node.line
  # backtrack until we can insert the func
  #
  while not SourceElementsNode === node.parent and node.parent != nil
    node = node.parent
  end
  if node.parent == nil
    false
  else
    index = node.parent.value.index{|x| x.eql? node}
    node.parent.value.insert index, get_hit_call(line, "hit_line")
    true
  end
end