rubinius/rubinius

View on GitHub
core/exception.rb

Summary

Maintainability
D
1 day
Test Coverage
class Exception
  attr_accessor :locations
  attr_accessor :cause
  attr_accessor :custom_backtrace

  def initialize(message = nil)
    @reason_message = message
    @locations = nil
    @backtrace = nil
    @custom_backtrace = nil
  end

  private :initialize

  def capture_backtrace!(offset=1)
    @locations = Rubinius::VM.backtrace offset
  end

  def ==(other)
    other.instance_of?(__class__) &&
      message == other.message &&
      backtrace == other.backtrace
  end

  def to_s
    if @reason_message
      @reason_message.to_s
    else
      self.class.to_s
    end
  end

  # This is here rather than in yaml.rb because it contains "private"
  # information, ie, the list of ivars. Putting it over in the yaml
  # source means it's easy to forget about.
  def to_yaml_properties
    list = super
    list.delete :@backtrace
    list.delete :@custom_backtrace
    return list
  end

  def message
    @reason_message
  end

  # Needed to properly implement #exception, which must clone and call
  # #initialize again, BUT not a subclasses initialize.
  alias_method :__initialize__, :initialize

  def backtrace
    return @custom_backtrace if @custom_backtrace

    if backtrace?
      awesome_backtrace.to_mri
    else
      nil
    end
  end

  # Indicates if the Exception has a backtrace set
  def backtrace?
    (@backtrace || @locations) ? true : false
  end

  def awesome_backtrace
    @backtrace ||= Rubinius::Backtrace.backtrace(@locations)
  end

  def render(header="An exception occurred", io=STDERR, color=true)
    exception = self
    exceptions = []

    while exception
      exceptions.unshift exception
      exception = exception.cause
    end

    while exception = exceptions.shift
      if custom_trace = exception.custom_backtrace
        io.puts "User-defined backtrace:", "\n"

        custom_trace.reverse_each { |line| io.puts line }

        io.puts "\nBacktrace:", "\n"
      end

      io.puts exception.awesome_backtrace.show("\n", color)

      io.puts "\n#{exception.message} (#{exception.class})"

      io.puts "\nCausing:", "\n" unless exceptions.empty?
    end

    message_lines = message.to_s.split("\n")

    io.puts header
    io.puts
    io.puts "    #{message_lines.shift} (#{self.class})"

    message_lines.each do |line|
      io.puts "    #{line}"
    end
  end

  def set_backtrace(bt)
    if bt.kind_of? Rubinius::Backtrace
      @backtrace = bt
    else
      # See if we stashed a Backtrace object away, and use it.
      if hidden_bt = Rubinius::Backtrace.detect_backtrace(bt)
        @backtrace = hidden_bt
      else
        type_error = TypeError.new "backtrace must be Array of String"
        case bt
        when Array
          if bt.all? { |s| s.kind_of? String }
            @custom_backtrace = bt
          else
            raise type_error
          end
        when String
          @custom_backtrace = [bt]
        when nil
          @custom_backtrace = nil
        else
          raise type_error
        end
      end
    end
  end

  # This is important, because I subclass can just override #to_s and calling
  # #message will call it. Using an alias doesn't achieve that.
  def message
    to_s
  end

  def inspect
    s = self.to_s
    if s.empty?
      self.class.name
    else
      "#<#{self.class.name}: #{s}>"
    end
  end

  class << self
    alias_method :exception, :new
  end

  def exception(message=nil)
    if message
      unless message.equal? self
        # As strange as this might seem, this IS actually the protocol
        # that MRI implements for this. The explicit call to
        # Exception#initialize (via __initialize__) is exactly what MRI
        # does.
        e = clone
        Rubinius.privately { e.__initialize__(message) }
        return e
      end
    end

    self
  end

  def location
    [context.file.to_s, context.line]
  end
end

class PrimitiveFailure < Exception
end

class ScriptError < Exception
end

class StandardError < Exception
end

class SignalException < Exception
end

class NoMemoryError < Exception
end

class ZeroDivisionError < StandardError
end

class ArgumentError < StandardError
  def to_s
    if @given and @expected
      if @method_name
        "method '#{@method_name}': given #{@given}, expected #{@expected}"
      else
        "given #{@given}, expected #{@expected}"
      end
    else
      super
    end
  end
end

class UncaughtThrowError < ArgumentError
  attr_accessor :tag
end

class IndexError < StandardError
end

class StopIteration < IndexError
end

class RangeError < StandardError
end

class FloatDomainError < RangeError
end

class LocalJumpError < StandardError
end

class NameError < StandardError
  attr_reader :name

  def initialize(*args, receiver: nil)
    super(args.shift)

    @name = args.shift
    @receiver = receiver
  end

  private :initialize

  def receiver
    if @receiver
      @receiver
    else
      raise ArgumentError, 'no receiver is available'
    end
  end
end

class NoMethodError < NameError
  attr_reader :name
  attr_reader :args

  def initialize(*arguments, **options)
    super(arguments.shift, **options)
    @name = arguments.shift
    @args = arguments.shift
  end

  private :initialize
end

class RuntimeError < StandardError
end

class SecurityError < StandardError
end

class ThreadError < StandardError
end

class FiberError < StandardError
end

class TypeError < StandardError
end

class FloatDomainError < RangeError
end

class RegexpError < StandardError
end

class LoadError < ScriptError
  attr_accessor :path

  def initialize(message=nil, path: nil)
    super message
    @path = path
  end

  private :initialize

  class InvalidExtensionError < LoadError
  end

  class MRIExtensionError < InvalidExtensionError
  end
end

class NotImplementedError < ScriptError
end

class Interrupt < SignalException
  def initialize(*args)
    super(args.shift)
    @name = args.shift
  end

  private :initialize
end

class IOError < StandardError
end

class EOFError < IOError
end

class LocalJumpError < StandardError
end

class SyntaxError < ScriptError
  attr_accessor :column
  attr_accessor :line
  attr_accessor :file
  attr_accessor :code

  def self.from(message, column, line, code, file)
    exc = new message
    exc.file = file
    exc.line = line
    exc.column = column
    exc.code = code
    exc
  end

  def reason
    @reason_message
  end
end

class SystemExit < Exception

  ##
  # Process exit status if this exception is raised

  attr_reader :status

  ##
  # Creates a SystemExit exception with optional status and message.  If the
  # status is omitted, Process::EXIT_SUCCESS is used.
  #--
  # *args is used to simulate optional prepended argument like MRI

  def initialize(first=nil, *args)
    if first.kind_of?(Fixnum)
      status = first
      super(*args)
    else
      status = Process::EXIT_SUCCESS
      super
    end

    @status = status
  end

  private :initialize

  ##
  # Returns true is exiting successfully, false if not. A successful exit is
  # one with a status equal to 0 (zero). Any other status is considered a
  # unsuccessful exit.

  def success?
    status == Process::EXIT_SUCCESS
  end

end


class SystemCallError < StandardError

  attr_reader :errno

  def self.errno_error(message, errno, location)
    Rubinius.primitive :exception_errno_error
    raise PrimitiveFailure, "SystemCallError.errno_error failed"
  end

  # We use .new here because when errno is set, we attempt to
  # lookup and return a subclass of SystemCallError, specificly,
  # one of the Errno subclasses.
  def self.new(*args)
    # This method is used 2 completely different ways. One is when it's called
    # on SystemCallError, in which case it tries to construct a Errno subclass
    # or makes a generic instead of itself.
    #
    # Otherwise it's called on a Errno subclass and just helps setup
    # a instance of the subclass
    if self.equal? SystemCallError
      case args.size
      when 1
        if args.first.kind_of?(Fixnum)
          errno = args.first
          message = nil
        else
          errno = nil
          message = StringValue(args.first)
        end
        location = nil
      when 2
        message, errno = args
        location = nil
      when 3
        message, errno, location = args
      else
        raise ArgumentError, "wrong number of arguments (#{args.size} for 1..3)"
      end

      # If it corresponds to a known Errno class, create and return it now
      if errno && error = SystemCallError.errno_error(message, errno, location)
        return error
      else
        return super(message, errno, location)
      end
    else
      case args.size
      when 0
        message = nil
        location = nil
      when 1
        message = StringValue(args.first)
        location = nil
      when 2
        message, location = args
      else
        raise ArgumentError, "wrong number of arguments (#{args.size} for 0..2)"
      end

      if defined?(self::Errno) && self::Errno.kind_of?(Fixnum)
        errno = self::Errno
        error = SystemCallError.errno_error(message, self::Errno, location)
        if error && error.class.equal?(self)
          return error
        end
      end

      error = allocate
      Rubinius::Unsafe.set_class error, self
      Rubinius.privately { error.initialize(*args) }
      return error
    end
  end

  # Must do this here because we have a unique new and otherwise .exception will
  # call Exception.new because of the alias in Exception.
  class << self
    alias_method :exception, :new
  end

  # Use splat args here so that arity returns -1 to match MRI.
  def initialize(*args)
    kls = self.class
    message, errno, location = args
    @errno = errno

    msg = "unknown error"
    msg << " @ #{StringValue(location)}" if location
    msg << " - #{StringValue(message)}" if message
    super(msg)
  end

  private :initialize
end

class KeyError < IndexError
end

class SignalException < Exception

  attr_reader :signo
  attr_reader :signm

  def initialize(signo = nil, signm = nil)
    # MRI overrides this behavior just for SignalException itself
    # but not for anything that inherits from it, therefore we
    # need this ugly check to make sure it works as intented.
    return super(signo) unless self.class == SignalException
    if signo.is_a? Integer
      unless @signm = Signal::Numbers[signo]
        raise ArgumentError, "invalid signal number #{signo}"
      end
      @signo = signo
      @signm = signm || "SIG#{@signm}"
    elsif signo
      if signm
        raise ArgumentError, "wrong number of arguments (2 for 1)"
      end
      signm = signo
      if signo.kind_of?(Symbol)
        signm = signm.to_s
      else
        signm = StringValue(signm)
      end
      signm = signm[3..-1] if signm.prefix? "SIG"
      unless @signo = Signal::Names[signm]
        raise ArgumentError, "invalid signal name #{signm}"
      end
      @signm = "SIG#{signm}"
    end
    super(@signm)
  end

  private :initialize
end

class StopIteration
  attr_accessor :result
  private :result=
end

##
# Base class for various exceptions raised in the VM.

class Rubinius::MachineException < Exception
end

class Rubinius::ConcurrentUpdateError < Rubinius::MachineException
end

class Rubinius::VMException < Exception
end

##
# Raised in the VM when an assertion fails.

class Rubinius::AssertionError < Rubinius::VMException
end

##
# Raised in the VM when attempting to read/write outside
# the bounds of an object.

class Rubinius::ObjectBoundsExceededError < Rubinius::VMException
end

# Defined by the VM itself
class Rubinius::InvalidBytecode < Rubinius::Internal
  attr_reader :compiled_code
  attr_reader :ip

  def message
    if @compiled_code
      if @ip and @ip >= 0
        "#{super} - at #{@compiled_code.name}+#{@ip}"
      else
        "#{super} - method #{@compiled_code.name}"
      end
    else
      super
    end
  end
end

class InterpreterError < Exception

end

class DeadlockError < Exception

end

# MRI has an Exception class named "fatal" that is raised
# by the rb_fatal function. The class is not accessible from
# ruby because the name is begins with a lower-case letter.
# Also, the exception cannot be rescued.
#
# To support rb_fatal in the C-API, Rubinius provides the
# following FatalError class. If it clashes with code in
# the wild, we can rename it.

class FatalError < Exception
end