module Backtracer # An object representation of a stack frame. struct Backtrace::Frame @context_cache = {} of Int32 => Context # The method of this frame (such as `User.find`). getter method : String # The file name of this frame (such as `app/models/user.cr`). getter path : String? # The line number of this frame. getter lineno : Int32? # The column number of this frame. getter column : Int32? protected getter(configuration) { Backtracer.configuration } def initialize(@method, @path = nil, @lineno = nil, @column = nil, *, @configuration = nil) end def_equals_and_hash @method, @path, @lineno, @column # Reconstructs the frame in a readable fashion. def to_s(io : IO) : Nil io << '`' << @method << '`' if @path io << " at " << @path io << ':' << @lineno if @lineno io << ':' << @column if @column end end def inspect(io : IO) : Nil io << "Backtrace::Frame(" to_s(io) io << ')' end # Returns `true` if `path` of this frame is within # the `configuration.src_path`, `false` otherwise. # # See `Configuration#src_path` def under_src_path? : Bool return false unless src_path = configuration.src_path !!path.try(&.starts_with?(src_path)) end # Returns: # # - `path` as is, unless it's absolute - i.e. starts with `/` # - `path` relative to `configuration.src_path` when `under_src_path?` is `true` # - `nil` otherwise # # NOTE: returned path is not required to be `under_src_path?` - see point no. 1 # # See `Configuration#src_path` def relative_path : String? return unless path = @path return path unless path.starts_with?('/') return unless under_src_path? if prefix = configuration.src_path path[prefix.chomp(File::SEPARATOR).size + 1..] end end # Returns: # # - `path` as is, if it's absolute - i.e. starts with `/` # - `path` appended to `configuration.src_path` # - `nil` otherwise # # See `Configuration#src_path` def absolute_path : String? return unless path = @path return path if path.starts_with?('/') if prefix = configuration.src_path File.join(prefix, path) end end # Returns name of the shard from which this frame originated. # # See `Configuration#modules_path_pattern` def shard_name : String? relative_path .try(&.match(configuration.modules_path_pattern)) .try(&.["name"]) end # Returns `true` if this frame originated from the app source code, # `false` otherwise. # # See `Configuration#app_dirs_pattern` def in_app? : Bool !!(relative_path.try(&.matches?(configuration.app_dirs_pattern))) end # Returns `Context` record consisting of 3 elements - an array of context lines # before the `lineno`, line at `lineno`, and an array of context lines # after the `lineno`. In case of failure it returns `nil`. # # Amount of returned context lines is taken from the *context_lines* # argument if given, or `configuration.context_lines` otherwise. # # NOTE: amount of returned context lines might be lower than given # in cases where `lineno` is near the start or the end of the file. # # See `Configuration#context_lines` def context(context_lines : Int32? = nil) : Context? context_lines ||= configuration.context_lines return unless context_lines && (context_lines > 0) cached = @context_cache[context_lines]? return cached if cached return unless (lineno = @lineno) && (lineno > 0) return unless (path = @path) && File.readable?(path) context_line = nil pre_context, post_context = %w[], %w[] i = 0 File.each_line(path) do |line| case i += 1 when lineno - context_lines...lineno pre_context << line when lineno context_line = line when lineno + 1..lineno + context_lines post_context << line end end if context_line @context_cache[context_lines] = Context.new( lineno: lineno, pre: pre_context, line: context_line, post: post_context, ) end end end end