module SolidErrors # adapted from: https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger/backtrace.rb class BacktraceLine # Backtrace line regexp (optionally allowing leading X: for windows support). INPUT_FORMAT = %r{^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$} STRING_EMPTY = "".freeze GEM_ROOT = "[GEM_ROOT]".freeze PROJECT_ROOT = "[PROJECT_ROOT]".freeze PROJECT_ROOT_CACHE = {} GEM_ROOT_CACHE = {} RELATIVE_ROOT = Regexp.new('^\.\/').freeze RAILS_ROOT = ::Rails.root.to_s.dup.freeze ROOT_REGEXP = Regexp.new("^#{Regexp.escape(RAILS_ROOT)}").freeze BACKTRACE_FILTERS = [ lambda { |line| return line unless defined?(Gem) GEM_ROOT_CACHE[line] ||= Gem.path.reduce(line) do |line, path| line.sub(path, GEM_ROOT) end }, lambda { |line| c = (PROJECT_ROOT_CACHE[RAILS_ROOT] ||= {}) return c[line] if c.has_key?(line) c[line] ||= line.sub(ROOT_REGEXP, PROJECT_ROOT) }, lambda { |line| line.sub(RELATIVE_ROOT, STRING_EMPTY) } ].freeze attr_reader :file attr_reader :number attr_reader :method attr_reader :filtered_file, :filtered_number, :filtered_method, :unparsed_line # Parses a single line of a given backtrace # # @param [String] unparsed_line The raw line from +caller+ or some backtrace. # # @return The parsed backtrace line. def self.parse(unparsed_line, opts = {}) filtered_line = BACKTRACE_FILTERS.reduce(unparsed_line) do |line, proc| proc.call(line) end if filtered_line match = unparsed_line.match(INPUT_FORMAT) || [].freeze fmatch = filtered_line.match(INPUT_FORMAT) || [].freeze file, number, method = match[1], match[2], match[3] filtered_args = [fmatch[1], fmatch[2], fmatch[3]] new(unparsed_line, file, number, method, *filtered_args, opts.fetch(:source_radius, 2)) end end def initialize(unparsed_line, file, number, method, filtered_file = file, filtered_number = number, filtered_method = method, source_radius = 2) self.unparsed_line = unparsed_line self.filtered_file = filtered_file self.filtered_number = filtered_number self.filtered_method = filtered_method self.file = file self.number = number self.method = method self.source_radius = source_radius end # Reconstructs the line in a readable fashion. def to_s "#{filtered_file}:#{filtered_number}:in `#{filtered_method}'" end def ==(other) to_s == other.to_s end def inspect "" end # Determines if this line is part of the application trace or not. def application? (filtered_file =~ /^\[PROJECT_ROOT\]/i) && !(filtered_file =~ /^\[PROJECT_ROOT\]\/vendor/i) end def source @source ||= get_source(file, number, source_radius) end private attr_writer :file, :number, :method, :filtered_file, :filtered_number, :filtered_method, :unparsed_line attr_accessor :source_radius # Open source file and read line(s). # # Returns an array of line(s) from source file. def get_source(file, number, radius = 2) return {} unless file && File.exist?(file) before = after = radius start = (number.to_i - 1) - before start = 0 and before = 1 if start <= 0 duration = before + 1 + after l = 0 File.open(file) do |f| start.times { f.gets l += 1 } return duration.times.map { (line = f.gets) ? [(l += 1), line] : nil }.compact.to_h end end end end