Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

ConstantResolver resolves a scope's own name over a same-named nested constant

Đang mở
#3,224 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
72/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
ruby
Lĩnh vực
backend

Hướng nghiên cứu

Start with lib/rbs/resolver/constant_resolver.rb, especially load_context_constants and the three methods that merge ancestor, lexical, and scope constants. Read test/rbs/resolver/constant_resolver_test.rb, including test_reference_constant_context_self, then run that test file. Done means the reproduction resolves C to ::M::C::C, while the existing self-reference test still passes.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Summary

RBS::Resolver::ConstantResolver#resolve resolves the innermost scope's own name to the scope itself, even when that scope has a nested constant of the same name. Ruby resolves the name to the nested constant, so type checkers built on the resolver report a false UnknownConstant.

Ruby behavior

module M
  class C
    class C
      CONST = 1
    end

    def run
      C::CONST
    end
  end
end

p M::C.new.run #=> 1

Inside def run, C refers to M::C::C: Ruby searches the constant table of each lexical scope from the innermost outward, and M::C's table has C.

Steep behavior

Steep 2.1.0 reports an error for the same code (checked with inline: true in the Steepfile, so the Ruby file also provides the declarations):

sample.rb:8:9: [error] Cannot find the declaration of constant: `CONST`

Renaming the inner class to D (and C::CONST to D::CONST) makes the error go away, so the name collision is the only trigger.

Reproduction with rbs alone

require "rbs"

source = <<~RBS
  module M
    class C
      class C
        CONST: Integer
      end
    end
  end
RBS

loader = RBS::EnvironmentLoader.new
env = RBS::Environment.from_loader(loader)
buffer = RBS::Buffer.new(name: "a.rbs", content: source)
_, directives, decls = RBS::Parser.parse_signature(buffer)
env.add_source(RBS::Source::RBS.new(buffer, directives, decls))
env = env.resolve_type_names

resolver = RBS::Resolver::ConstantResolver.new(builder: RBS::DefinitionBuilder.new(env: env))

# Lexical context of a method body inside `module M; class C; ...; end; end`
context = [[nil, RBS::TypeName.parse("::M")], RBS::TypeName.parse("::M::C")]

puts resolver.resolve(:C, context: context).name

Expected: ::M::C::C
Actual: ::M::C

Cause

load_context_constants merges three layers into one hash, and a later merge wins:

constants_from_ancestors(last, constants: consts)       # 1
constants_from_context(context, constants: consts)      # 2
constants_itself(context, constants: consts)            # 3

After step 2 the hash already holds the right answer, because the children of ::M::C contain C => ::M::C::C. Step 3 then overwrites it with C => ::M::C:

https://github.com/ruby/rbs/blob/8575f87dab1605320e257830505b0db49b4367ab/lib/rbs/resolver/constant_resolver.rb#L213

Possible fix

Run constants_itself between the ancestor layer and the lexical layer:

def load_context_constants(context)
  consts = {}

  if last = context&.[](1)
    constants_from_ancestors(last, constants: consts)
  else
    constants_from_ancestors(BuiltinNames::Object.name, constants: consts)
  end

  constants_itself(context, constants: consts)
  constants_from_context(context, constants: consts) or return

  context_constants_cache[context] = consts
end

With this order:

  • The reproduction above resolves C to ::M::C::C.
  • The scope's own name still wins over an ancestor constant. test_reference_constant_context_self (class Foo::Bar with a top-level ::Bar resolves Bar to ::Foo::Bar) keeps passing.
  • test/rbs/resolver/constant_resolver_test.rb passes, and the rest of test/rbs shows no new failures.

I checked this by prepending the patched method onto ConstantResolver, not by editing the gem.

A simpler change, constants[typename.name] ||= constant in constants_itself, fixes the reproduction but breaks test_reference_constant_context_self, because the top-level ::Bar comes in through the ancestor layer before step 3 runs.

Note: the compact-style case differs from Ruby too

test_reference_constant_context_self asserts behavior that Ruby does not have:

class Foo; end
class Bar; end
class Foo::Bar
  p Bar #=> Bar (the top-level ::Bar, not Foo::Bar)
end

module N; end
class N::D
  D #=> NameError: uninitialized constant N::D::D
end

In Ruby, the scope's own name is visible only through an enclosing lexical scope's constant table. That table is already covered by constants_from_context. So constants_itself takes effect only in the compact style (class Foo::Bar), and there it differs from Ruby.

The fix above keeps that compact-style behavior as it is. Whether it is intended is a separate question, and I am not proposing to change it here.

The new order also changes one more case, again toward Ruby: a compact-style class inside a module that has a same-named constant.

class Foo; end
module X
  class Bar; end
  class Foo::Bar
    p Bar #=> X::Bar
  end
end

With context [[nil, ::X], ::Foo::Bar], resolve(:Bar) returns ::Foo::Bar today and ::X::Bar with the new order.

Environment

  • rbs: master (8575f87dab1605320e257830505b0db49b4367ab). The line above is the same on the current master.
  • Steep: 2.1.0
  • Ruby: 4.0.6
Ngôn ngữ chính
Ruby
Star
2.2k
Fork
258
Merge trung bình
1 ngày 4 giờ
Pull request đã merge (30 ngày)
52

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của ruby/rbs

Tất cả issue của ruby/rbs

Issue tương tự

Thêm issue về Ruby

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.