Shopify / Shopify/rubydex

Rubydex ignores Zeitwerk implicit namespaces during constant resolution

Open
#997 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
355
Forks
24
Avg merge
2d 6h
Merged PRs (30d)
17

Description

Summary

Rubydex does not model namespaces that Zeitwerk autovivifies from directories.

If a subclass has an implicit namespace whose name is already defined by its parent class, Rubydex resolves the namespace through inheritance. It then merges methods from two distinct runtime classes.

This causes false positives in RuboCop 1.89’s project-index-backed Lint/DuplicateMethods.

Reproduction

# repro.rb
require "fileutils"
require "rubydex"
require "tmpdir"
require "zeitwerk"

Dir.mktmpdir do |directory|
  root = File.join(directory, "lib")

  files = {
    "parent.rb" => <<~RUBY,
      class Parent
      end
    RUBY
    "parent/tasks.rb" => <<~RUBY,
      class Parent::Tasks
      end
    RUBY
    "child.rb" => <<~RUBY,
      class Child < Parent
      end
    RUBY
    "parent/tasks/worker.rb" => <<~RUBY,
      class Parent::Tasks::Worker
        def run
        end
      end
    RUBY
    "child/tasks/worker.rb" => <<~RUBY,
      class Child::Tasks::Worker
        def run
        end
      end
    RUBY
  }

  files.each do |path, source|
    full_path = File.join(root, path)
    FileUtils.mkdir_p(File.dirname(full_path))
    File.write(full_path, source)
  end

  loader = Zeitwerk::Loader.new
  loader.push_dir(root)
  loader.setup

  puts "Zeitwerk:"
  puts Child::Tasks.name
  puts Child::Tasks::Worker.name

  graph = Rubydex::Graph.new
  graph.index_all([root])
  graph.resolve

  puts "Rubydex:"
  %w[
    Parent::Tasks::Worker#run()
    Child::Tasks::Worker#run()
  ].each do |name|
    declaration = graph[name]
    paths = declaration&.definitions&.map do |definition|
      definition.location.to_file_path
    end || []

    puts "#{name}: #{paths.empty? ? "<missing>" : paths.join(", ")}"
  end
end

Run with:

bundle add rubydex --version 0.3.0
bundle add zeitwerk
bundle exec ruby repro.rb

Expected result

Zeitwerk creates Child::Tasks from the child/tasks directory. It is independent from Parent::Tasks, despite Child < Parent.

Zeitwerk:
Child::Tasks
Child::Tasks::Worker

Rubydex:
Parent::Tasks::Worker#run():
  .../parent/tasks/worker.rb
Child::Tasks::Worker#run():
  .../child/tasks/worker.rb

Actual result

Zeitwerk resolves both classes correctly, but Rubydex indexes Child::Tasks::Worker#run as Parent::Tasks::Worker#run:

Zeitwerk:
Child::Tasks
Child::Tasks::Worker

Rubydex:
Parent::Tasks::Worker#run():
  .../child/tasks/worker.rb, .../parent/tasks/worker.rb
Child::Tasks::Worker#run():
  <missing>

Environment

  • Rubydex 0.3.0
  • Ruby 3.4.10
  • Linux

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the repro.rb flow that builds Rubydex::Graph, calls index_all([root]), and then calls resolve; inspect how graph[name] returns declarations and definitions. Reproduce the Zeitwerk comparison first, then trace namespace and inheritance resolution. Done means Child::Tasks::Worker#run maps only to child/tasks/worker.rb while Parent::Tasks::Worker#run remains separate.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.