ConstantResolver resolves a scope's own name over a same-named nested constant
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 72/100
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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:
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
Cto::M::C::C. - The scope's own name still wins over an ancestor constant.
test_reference_constant_context_self(class Foo::Barwith a top-level::BarresolvesBarto::Foo::Bar) keeps passing. test/rbs/resolver/constant_resolver_test.rbpasses, and the rest oftest/rbsshows 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
- Lenguaje dominante
- Ruby
- Estrellas
- 2.2k
- Forks
- 258
- Merge medio
- 1 d 4 h
- PR fusionados (30 d)
- 52
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de ruby/rbs
-
InlineParser: `@rbs @ivar` annotation is reported as unused unless it is the first thing in the class bodyPosiblemente ocupada @ksss la tomó hace 16 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 57/100
Los mantenedores suelen responder en 1 día
Issues similares
-
NoMethodError in auth#createAbiertobugsnag
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
JoeMcB/improvcoaches#827 ·
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
TheOdinProject/curriculum#31465 ·
Los mantenedores suelen responder en 1 día
-
area: docs eco: ruby good first issue help wanted lane: free-agent roadmap size: XS type: docs
Dificultad 1/5 Menos de una hora Aptitud para principiantes 66/100
arminazimi/evergreen#465 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
simp/puppetsync#120 ·
Los mantenedores suelen responder en 2 días