Skip to content

Thread::Backtrace::Location#absolute_path returns relative path for files loaded via load with bare relative path #9245

@myronmarston

Description

@myronmarston

Environment

  • JRuby 10.0.2.0 (Ruby 3.4.2 compat)
  • macOS / aarch64

Summary

Thread::Backtrace::Location#absolute_path returns a relative path (identical to #path) when the source file was loaded via Kernel#load with a bare relative path that is resolved via cwd (e.g. load "file.rb"). On MRI, absolute_path correctly resolves to the full absolute path.

The bug is specific to the case where load resolves the file via the current working directory. It does not occur when:

  • load is given a ./ prefix (e.g. load "./file.rb")
  • load is given an absolute path
  • load resolves the file via $LOAD_PATH
  • require is used (always resolves via $LOAD_PATH)
  • require_relative is used

Reproduction Script

#!/usr/bin/env ruby
# frozen_string_literal: true

# Reproduction: Thread::Backtrace::Location#absolute_path returns a relative path
# when the source file was loaded via `load` with a bare relative path.
#
# Expected: absolute_path always returns an absolute path (or nil).
# Actual on JRuby: absolute_path returns the same relative string as #path.

require "tmpdir"

class Capturer
  attr_reader :location

  def capture
    @location = caller_locations(1, 1).to_a.first
  end
end

capturer = Capturer.new
dir = Dir.mktmpdir
begin
  Dir.chdir(dir) do
    File.write("test_file.rb", <<~'RUBY')
      $capturer.capture
    RUBY
    $capturer = capturer

    puts "RUBY_ENGINE: #{RUBY_ENGINE}"
    puts "RUBY_VERSION: #{RUBY_VERSION}"
    puts "JRUBY_VERSION: #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : 'N/A'}"
    puts

    # Helper to check result
    check = ->(loc) do
      abs = loc.absolute_path
      if abs && !abs.start_with?("/")
        puts "  BUG: absolute_path is not absolute!"
      else
        puts "  OK: absolute_path is absolute"
      end
    end

    # --- load ---

    # Case 1: load with bare relative path (BUG on JRuby)
    load "test_file.rb"
    loc = capturer.location
    puts '=== load "test_file.rb" (bare relative, cwd resolution) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    puts

    # Case 2: load with bare relative path, dir in $LOAD_PATH (OK)
    $LOAD_PATH.unshift(dir)
    load "test_file.rb"
    loc = capturer.location
    puts '=== load "test_file.rb" (bare relative, $LOAD_PATH resolution) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    $LOAD_PATH.delete(dir)
    puts

    # Case 3: load with ./ relative path (OK)
    load "./test_file.rb"
    loc = capturer.location
    puts '=== load "./test_file.rb" (./ relative path) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    puts

    # Case 4: load with absolute path (OK)
    load File.join(dir, "test_file.rb")
    loc = capturer.location
    puts '=== load "/absolute/path/test_file.rb" ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    puts

    # --- require ---

    # Case 5: require via $LOAD_PATH (OK)
    $LOAD_PATH.unshift(dir)
    $LOADED_FEATURES.delete_if { |f| f.include?("test_file") }
    require "test_file"
    loc = capturer.location
    puts '=== require "test_file" (via $LOAD_PATH) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    $LOAD_PATH.delete(dir)
    puts

    # Case 6: require with ./ relative path (OK)
    $LOADED_FEATURES.delete_if { |f| f.include?("test_file") }
    require "./test_file"
    loc = capturer.location
    puts '=== require "./test_file" (./ relative path) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
    puts

    # Case 7: require_relative (OK)
    File.write("wrapper.rb", "require_relative 'test_file'\n")
    $LOADED_FEATURES.delete_if { |f| f.include?("test_file") }
    load "wrapper.rb"
    loc = capturer.location
    puts '=== require_relative "test_file" (via wrapper) ==='
    puts "  path:          #{loc.path.inspect}"
    puts "  absolute_path: #{loc.absolute_path.inspect}"
    check.call(loc)
  end
ensure
  FileUtils.rm_rf(dir)
end

Expected Behavior

Here's what that script produces on MRI 3.4.5:

RUBY_ENGINE: ruby
RUBY_VERSION: 3.4.5
JRUBY_VERSION: N/A

=== load "test_file.rb" (bare relative, cwd resolution) ===
  path:          "test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== load "test_file.rb" (bare relative, $LOAD_PATH resolution) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== load "./test_file.rb" (./ relative path) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== load "/absolute/path/test_file.rb" ===
  path:          "/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== require "test_file" (via $LOAD_PATH) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== require "./test_file" (./ relative path) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

=== require_relative "test_file" (via wrapper) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-34796-kjeupa/test_file.rb"
  OK: absolute_path is absolute

Actual Behavior

Here's the result on JRuby:

RUBY_ENGINE: jruby
RUBY_VERSION: 3.4.2
JRUBY_VERSION: 10.0.2.0

=== load "test_file.rb" (bare relative, cwd resolution) ===
  path:          "test_file.rb"
  absolute_path: "test_file.rb"
  BUG: absolute_path is not absolute!

=== load "test_file.rb" (bare relative, $LOAD_PATH resolution) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

=== load "./test_file.rb" (./ relative path) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

=== load "/absolute/path/test_file.rb" ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

=== require "test_file" (via $LOAD_PATH) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

=== require "./test_file" (./ relative path) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

=== require_relative "test_file" (via wrapper) ===
  path:          "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  absolute_path: "/private/var/folders/t0/0v26g3fs33gc0vk7hydx3wzw0000gp/T/d20260216-33652-d19nm7/test_file.rb"
  OK: absolute_path is absolute

Scope of the bug

Method Affected?
load "file.rb" (cwd resolution) YES
load "file.rb" ($LOAD_PATH resolution) no
load "./file.rb" no
load "/absolute/path/file.rb" no
require "file" (via $LOAD_PATH) no
require "./file" no
require_relative "file" no

Impact

Code that relies on absolute_path being absolute (e.g. Pathname#relative_path_from) fails with ArgumentError: different prefix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions