class DeepCover::Analyser::Ruby25LikeBranch::NodeCoverageExtrator

This is the class doing the work. Since everything is about the node, the class delegates missing methods to the node, simplifying the code.

Public Class Methods

new(node = nil) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 30
def initialize(node = nil)
  self.node = node
  @loc_index = 0
end

Public Instance Methods

branch_coverage(node) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 38
def branch_coverage(node)
  self.node = node
  case node
  when Node::Case
    handle_case
  when Node::Csend
    handle_csend
  when Node::If
    handle_if
  when Node::ShortCircuit
    handle_short_circuit
  when Node::Until, Node::While, Node::UntilPost, Node::WhilePost
    handle_until_while
  end
end
handle_case() click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 54
def handle_case
  cond_info = [:case, *node_loc_infos]

  sub_keys = [:when] * (branches.size - 1) + [:else]
  empty_fallbacks = whens.map { |w| wrap_rwhitespace_and_comments_if_ruby25(w.loc_hash[:begin] || w.loc_hash[:expression]).end }
  empty_fallbacks.map!(&:begin)

  if loc_hash[:else]
    empty_fallbacks << wrap_rwhitespace_and_comments_if_ruby25(loc_hash[:else]).end
  else
    # DeepCover manually inserts a `else` for Case when there isn't one for tracker purposes.
    # The normal behavior of ruby25's branch coverage when there is no else is to return the loc of the node
    # So we sent that fallback.
    empty_fallbacks << expression
  end

  branches_locs = whens.map do |when_node|
    next when_node.body if when_node.body.is_a?(Node::EmptyBody)

    start_at = when_node.loc_hash[:begin]
    start_at = start_at.wrap_rwhitespace_and_comments.end if start_at
    start_at ||= when_node.body.expression.begin

    end_at = when_node.body.expression.end
    start_at.with(end_pos: end_at.end_pos)
  end

  branches_locs << node.else
  clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallbacks, execution_counts: branches.map(&:execution_count))

  [cond_info, clauses_infos]
end
handle_csend() click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 87
def handle_csend
  # csend wraps the comment but not the newlines
  node_range = wrap_rwhitespace_and_comments_if_ruby25(node.expression, whitespaces: /\A[ \t\r\f]+/)
  cond_info = [:"&.", *node_loc_infos(node_range)]
  false_branch, true_branch = branches
  [cond_info, {[:then, *node_loc_infos(node_range)] => true_branch.execution_count,
               [:else, *node_loc_infos(node_range)] => false_branch.execution_count,
              },
  ]
end
handle_if() click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 98
def handle_if
  key = style == :unless ? :unless : :if

  node_range = extend_elsif_range
  cond_info = [key, *node_loc_infos(node_range)]

  sub_keys = [:then, :else]
  if style == :ternary
    empty_fallback_locs = [nil, nil]
  else
    first_clause_fallback = wrap_rwhitespace_and_comments_if_ruby25(loc_hash[:begin]) if loc_hash[:begin]
    first_clause_fallback ||= wrap_rwhitespace_and_comments_if_ruby25(condition.expression)
    # Ruby26 wraps the comments but only on the same line
    # No need for condition since Ruby25 wraps all of them
    first_clause_fallback = first_clause_fallback.wrap_final_comment.end
    else_loc = loc_hash[:else]
    if else_loc
      second_clause_fallback = wrap_rwhitespace_and_comments_if_ruby25(else_loc).end
    elsif !modifier?
      second_clause_fallback = root_if_node.loc_hash[:end].begin
    end

    empty_fallback_locs = [first_clause_fallback, second_clause_fallback]
  end
  # loc can be nil if the clause can't be empty, such as ternary and modifer if/unless

  if key == :unless
    sub_keys.reverse!
    empty_fallback_locs.reverse!
  end

  branches_locs = branches
  execution_counts = branches_locs.map(&:execution_count)
  if modifier?
    branches_locs = branches_locs.map do |branch|
      if branch.is_a?(Node::Kwbegin)
        if branch.instructions.empty?
          wrap_rwhitespace_and_comments_if_ruby25(branch.loc_hash[:begin]).end
        elsif branch.instructions.first.is_a?(Node::Ensure)
          # Kernel.binding.pry
          end_pos = wrap_rwhitespace_and_comments_if_ruby25(branch.instructions.last.expression).end_pos
          wrap_rwhitespace_and_comments_if_ruby25(branch.loc_hash[:begin]).end.with(end_pos: end_pos)
        else
          end_pos = branch.instructions.last.expression.end_pos
          branch.loc_hash[:begin].wrap_rwhitespace_and_comments.end.with(end_pos: end_pos)
        end
      else
        branch
      end
    end
  end

  branches_locs[1] = extend_elsif_range(branches_locs[1])

  clauses_infos = infos_for_branches(branches_locs, sub_keys, empty_fallback_locs, execution_counts: execution_counts, node_range: node_range)
  [cond_info, clauses_infos]
end
handle_short_circuit() click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 156
def handle_short_circuit
  cond_info = [operator, *node_loc_infos]
  sub_keys = [:then, :else]
  sub_keys.reverse! if node.is_a?(Node::Or)

  [cond_info, infos_for_branches(branches, sub_keys, [nil, nil])]
end
handle_until_while() click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 164
def handle_until_while
  key = loc_hash[:keyword].source.to_sym
  base_info = [key, *node_loc_infos]
  body_node = if node.is_a?(Node::WhilePost) || node.is_a?(Node::UntilPost)
                if !body.instructions.empty?
                  end_pos = body.instructions.last.expression.end_pos
                  body.instructions.first.expression.with(end_pos: end_pos)
                else
                  wrap_rwhitespace_and_comments_if_ruby25(body.loc_hash[:begin]).end
                end
              elsif body.is_a?(Node::Begin) && !body.expressions.empty?
                end_pos = body.expressions.last.expression.end_pos
                body.expressions.first.expression.with(end_pos: end_pos)
              elsif body.is_a?(Node::EmptyBody)
                wrap_rwhitespace_and_comments_if_ruby25(condition.loc_hash[:expression]).end
              else
                body
              end

  [base_info, {[:body, *node_loc_infos(body_node)] => body.execution_count}]
end

Protected Instance Methods

extend_elsif_range(possible_elsif = node) click to toggle source

If the actual else clause (final one) of an if…elsif…end is empty, then Ruby25 wraps the final whitespace

# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 189
def extend_elsif_range(possible_elsif = node)
  return possible_elsif unless possible_elsif.is_a?(Node::If) && possible_elsif.style == :elsif
  deepest_if = possible_elsif.deepest_elsif_node
  if deepest_if.false_branch.is_a?(Node::EmptyBody)
    return wrap_rwhitespace_and_comments_if_ruby25(possible_elsif.expression)
  end
  possible_elsif
end
infos_for_branch(branch, key, empty_fallback_loc, execution_count: nil, node_range: node) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 198
def infos_for_branch(branch, key, empty_fallback_loc, execution_count: nil, node_range: node)
  if !branch.is_a?(Node::EmptyBody)
    loc = branch
  elsif branch.expression
    # There is clause, but it is empty
    loc = empty_fallback_loc
  else
    # There is no clause
    loc = node_range
  end

  execution_count ||= branch.execution_count
  [[key, *node_loc_infos(loc)], execution_count]
end
infos_for_branches(branches, keys, empty_fallback_locs, execution_counts: [], node_range: node) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 213
def infos_for_branches(branches, keys, empty_fallback_locs, execution_counts: [], node_range: node)
  branches_infos = branches.map.with_index do |branch, i|
    infos_for_branch(branch, keys[i], empty_fallback_locs[i], execution_count: execution_counts[i], node_range: node_range)
  end
  branches_infos.to_h
end
node_loc_infos(node_or_range = node) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 220
def node_loc_infos(node_or_range = node)
  source_range = node_or_range.is_a?(Node) ? node_or_range.expression : node_or_range

  @loc_index += 1
  [@loc_index, source_range.line, source_range.column, source_range.last_line, source_range.last_column]
end
wrap_rwhitespace_and_comments_if_ruby25(expression, whitespaces: /\A\s+/) click to toggle source
# File lib/deep_cover/analyser/ruby25_like_branch.rb, line 227
def wrap_rwhitespace_and_comments_if_ruby25(expression, whitespaces: /\A\s+/)
  if RUBY_VERSION.start_with?('2.5')
    expression.wrap_rwhitespace_and_comments(whitespaces: whitespaces)
  else
    expression
  end
end