class RuboCop::Cop::Style::EvalWithLocation

Ensures that eval methods (`eval`, `instance_eval`, `class_eval` and `module_eval`) are given filename and line number values (`__FILE__` and `__LINE__`). This data is used to ensure that any errors raised within the evaluated code will be given the correct identification in a backtrace.

The cop also checks that the line number given relative to `__LINE__` is correct.

This cop will autocorrect incorrect or missing filename and line number values. However, if `eval` is called without a binding argument, the cop will not attempt to automatically add a binding, or add filename and line values.

@example

# bad
eval <<-RUBY
  def do_something
  end
RUBY

# bad
C.class_eval <<-RUBY
  def do_something
  end
RUBY

# good
eval <<-RUBY, binding, __FILE__, __LINE__ + 1
  def do_something
  end
RUBY

# good
C.class_eval <<-RUBY, __FILE__, __LINE__ + 1
  def do_something
  end
RUBY

This cop works only when a string literal is given as a code string. No offense is reported if a string variable is given as below:

@example

# not checked
code = <<-RUBY
  def do_something
  end
RUBY
eval code

Constants

MSG
MSG_EVAL
MSG_INCORRECT_FILE
MSG_INCORRECT_LINE
RESTRICT_ON_SEND

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 81
def on_send(node)
  # Classes should not redefine eval, but in case one does, it shouldn't
  # register an offense. Only `eval` without a receiver and `Kernel.eval`
  # are considered.
  return if node.method?(:eval) && !valid_eval_receiver?(node.receiver)

  code = node.arguments.first
  return unless code && (code.str_type? || code.dstr_type?)

  check_location(node, code)
end

Private Instance Methods

add_offense_for_different_line(node, line_node, line_diff) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 195
def add_offense_for_different_line(node, line_node, line_diff)
  sign = line_diff.positive? ? :+ : :-
  return if line_with_offset?(line_node, sign, line_diff.abs)

  add_offense_for_incorrect_line(node.method_name, line_node, sign, line_diff.abs)
end
add_offense_for_incorrect_line(method_name, line_node, sign, line_diff) click to toggle source

rubocop:enable Style/ConditionalAssignment

# File lib/rubocop/cop/style/eval_with_location.rb, line 142
def add_offense_for_incorrect_line(method_name, line_node, sign, line_diff)
  expected = expected_line(sign, line_diff)
  message = format(MSG_INCORRECT_LINE,
                   method_name: method_name,
                   actual: line_node.source,
                   expected: expected)

  add_offense(line_node.loc.expression, message: message) do |corrector|
    corrector.replace(line_node, expected)
  end
end
add_offense_for_missing_line(node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 210
def add_offense_for_missing_line(node, code)
  register_offense(node) do |corrector|
    line_str = missing_line(node, code)
    corrector.insert_after(node.loc.expression.end, ", #{line_str}")
  end
end
add_offense_for_missing_location(node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 217
def add_offense_for_missing_location(node, code)
  if node.method?(:eval) && !with_binding?(node)
    register_offense(node)
    return
  end

  register_offense(node) do |corrector|
    line_str = missing_line(node, code)
    corrector.insert_after(node.last_argument.source_range.end, ", __FILE__, #{line_str}")
  end
end
add_offense_for_same_line(node, line_node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 189
def add_offense_for_same_line(node, line_node)
  return if special_line_keyword?(line_node)

  add_offense_for_incorrect_line(node.method_name, line_node, nil, 0)
end
check_file(node, file_node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 154
def check_file(node, file_node)
  return true if special_file_keyword?(file_node)

  message = format(MSG_INCORRECT_FILE,
                   method_name: node.method_name,
                   expected: '__FILE__',
                   actual: file_node.source)

  add_offense(file_node, message: message) do |corrector|
    corrector.replace(file_node, '__FILE__')
  end
end
check_line(node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 167
def check_line(node, code)
  line_node = node.arguments.last
  line_diff = line_difference(line_node, code)
  if line_diff.zero?
    add_offense_for_same_line(node, line_node)
  else
    add_offense_for_different_line(node, line_node, line_diff)
  end
end
check_location(node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 95
def check_location(node, code)
  file, line = file_and_line(node)

  if line
    check_file(node, file)
    check_line(node, code)
  elsif file
    check_file(node, file)
    add_offense_for_missing_line(node, code)
  else
    add_offense_for_missing_location(node, code)
  end
end
expected_line(sign, line_diff) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 202
def expected_line(sign, line_diff)
  if line_diff.zero?
    '__LINE__'
  else
    "__LINE__ #{sign} #{line_diff.abs}"
  end
end
file_and_line(node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 122
def file_and_line(node)
  base = node.method?(:eval) ? 2 : 1
  [node.arguments[base], node.arguments[base + 1]]
end
line_difference(line_node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 177
def line_difference(line_node, code)
  string_first_line(code) - line_node.loc.expression.first_line
end
missing_line(node, code) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 229
def missing_line(node, code)
  line_diff = line_difference(node.arguments.last, code)
  sign = line_diff.positive? ? :+ : :-
  expected_line(sign, line_diff)
end
register_offense(node, &block) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 109
def register_offense(node, &block)
  msg = node.method?(:eval) ? MSG_EVAL : format(MSG, method_name: node.method_name)
  add_offense(node, message: msg, &block)
end
special_file_keyword?(node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 114
def special_file_keyword?(node)
  node.str_type? && node.source == '__FILE__'
end
special_line_keyword?(node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 118
def special_line_keyword?(node)
  node.int_type? && node.source == '__LINE__'
end
string_first_line(str_node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 181
def string_first_line(str_node)
  if str_node.heredoc?
    str_node.loc.heredoc_body.first_line
  else
    str_node.loc.expression.first_line
  end
end
with_binding?(node) click to toggle source
# File lib/rubocop/cop/style/eval_with_location.rb, line 127
def with_binding?(node)
  node.method?(:eval) ? node.arguments.size >= 2 : true
end
with_lineno?(node) click to toggle source

FIXME: It's a Style/ConditionalAssignment's false positive. rubocop:disable Style/ConditionalAssignment

# File lib/rubocop/cop/style/eval_with_location.rb, line 133
def with_lineno?(node)
  if node.method?(:eval)
    node.arguments.size == 4
  else
    node.arguments.size == 3
  end
end