Skip to content

Name a class written from the top level as Ruby does, with --experimental-parser - #82

Merged
dduugg merged 5 commits into
mainfrom
experimental-top-level-names
Oct 13, 2026
Merged

dduugg merged 5 commits into
mainfrom
experimental-top-level-names

Conversation

@dduugg

@dduugg dduugg commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With --experimental-parser, pks named a class or module written from the top level as if it nested in the namespaces around it. class ::Taco was defined as ::::Taco, class ::Burrito inside module Outer as ::Outer::::Burrito, and a constant assigned in one, as ::::Taco::FILLING. The resolver built its guesses the same way, so a reference to one of these was found only from inside another class ::Taco written the same way, and then under the :::: name. The rest were missed. So was Shell written inside class ::Taco, looked up as ::::Taco::Shell and then ::Shell, when another pack defines ::Taco::Shell as class Taco::Shell.

  • nest_constant_name starts the name again from the top level at a namespace that begins with ::, as Ruby does.
  • The experimental parser names its definitions with it, and the experimental resolver makes its guesses with it. Inside module Outer; class ::Foo, Bar is now looked up as ::Foo::Bar, then ::Outer::Bar, then ::Bar, the order of Ruby's Module.nesting.
  • A reference that was found under a :::: name now has the plain one, as ::Taco::FILLING. A violation that a package_todo.yml records under the :::: name is reported again under the new name, and the old entry as stale, until pks update is run. The CHANGELOG says so.
  • Without --experimental-parser, nothing changes. packwerk names class ::Taco ::::Taco among a file's local definitions too, and its constant_resolver looks Shell up as ::Taco::Shell against names that have no leading ::, so it misses the same references, or takes Shell for a top-level ::Shell. The packwerk parser and the Zeitwerk resolver keep that lookup, and the CHANGELOG says so.
  • A class reopened this way, as class ::String inside module CoreExt, is now ::String, so with --experimental-parser, expose-monkey-patches lists it, and a reference to String from another pack is a reference to the pack that reopens it, as for a class String at the top level of a file. The CHANGELOG says this too.

The cache key includes pks's version, so a release doesn't serve the old names from the cache.

Test plan

  • cargo test (378 passed, and 379 on a merge with main, which adds one), cargo fmt --check, and cargo clippy --all-targets --all-features -- -Dwarnings.
  • definitions_from_the_top_level and resolve_in_a_namespace_from_the_top_level cover the parser's names and the resolver's lookup, and test_list_references_to_a_class_defined_from_the_top_level lists a reference to class ::Taco inside module Outer in another pack. All three fail when nest_constant_name joins the old way.
  • definitions_from_the_top_level also has a module ::Foo and nestings like module A; class ::B; module C, and test_list_references_inside_a_class_defined_from_the_top_level looks Shell up inside module Outer; class ::Taco. They fail when visit_module_node names a module the old way, when only the resolver joins the old way, and when nest_constant_name starts again at the first :: namespace in place of the last. No test caught those before.
  • On a copy of simple_app with class ::Taco and module Outer; class ::Burrito, -e list-definitions gives ::Taco, ::Taco::FILLING and ::Burrito, and -e list-references now lists ::Taco, ::Taco::FILLING, ::Burrito, and ::Taco::Shell from inside class ::Taco. The default parser's output is unchanged.
  • With class ::Taco; FILLING = 1; end in packs/bar and FILLING written inside a class ::Taco in packs/foo, the binary before this change lists ::::Taco::FILLING, and this one ::Taco::FILLING. After pks -e update with the old binary, pks -e check with this one reports the two violations under the new name and the old todo entry as stale.

…ntal-parser

The experimental parser joined a class or module's name to the
namespaces around it with `::`, so `class ::Taco` was defined as
`::::Taco`, and `class ::Burrito` inside `module Outer` as
`::Outer::::Burrito`. The constants assigned in them got the same
prefix, as `::::Taco::FILLING`. No reference resolves to a name with
`::::`, so references to these constants were missed. The experimental
resolver built its guesses the same way, so `Shell` written inside
`class ::Taco` was looked up as `::::Taco::Shell` and then `::Shell`,
and a `::Taco::Shell` that another pack defines was missed too.

`nest_constant_name` starts the name again from the top level at a
namespace that begins with `::`, as Ruby does. The experimental parser
names its definitions with it, and the experimental resolver makes its
guesses with it, so inside `module Outer; class ::Foo`, `Bar` is
`::Foo::Bar`, then `::Outer::Bar`, then `::Bar`, the order of Ruby's
`Module.nesting`.

The packwerk parser and the Zeitwerk resolver keep
`combine_namespace_with_constant_name` and `get_definition_from`, which
match packwerk. packwerk names `class ::Taco` `::::Taco` among a file's
local definitions too, and its constant_resolver looks `Shell` up as
`::Taco::Shell` against names that have no leading `::`, so it misses
the same references. The CHANGELOG says so.

The cache key includes pks's version, so a release doesn't serve the
old names from the cache.
@dduugg
dduugg requested a review from a team as a code owner October 13, 2026 22:05
The test cases and `nest_constant_name` already say what they did.
The first commit says that no reference resolves to a name with `::::`.
One written inside another `class ::Taco`, as the definition is, did.
The resolver built its guess the same way, so `FILLING` there resolved
to `::::Taco::FILLING`. The rest were missed. That reference is now
named `::Taco::FILLING`, so a violation that a package_todo.yml records
under the `::::` name is reported again under the new name, and the old
entry as stale, until `pks update` is run. The CHANGELOG now says so.
No test caught the experimental parser naming `module ::Foo` the old
way, or `nest_constant_name` starting again at the first namespace
written from the top level in place of the last, and the integration
test didn't reach the resolver. `definitions_from_the_top_level` now
has a `module ::Foo`, and nestings with a namespace from the top level
in the middle, as `module A; class ::B; module C`. A new integration
test looks up `Shell` inside `module Outer; class ::Taco`, and finds
`::Taco::Shell` in another pack.

`nest_constant_name` builds the name in one String with a loop, in
place of a fold that made a String for each part. Its doc comment says
that a constant assigned from the top level starts again too, and the
resolver's comment on its lookup order covers a namespace from the top
level.
With `class ::String` inside `module CoreExt` now named `::String`,
`expose-monkey-patches` lists it, and a reference to `String` from
another pack is a reference to the pack that reopens it, as it is for a
`class String` at the top level of a file. The CHANGELOG says so, and
that `::BAR = 1` inside `module Outer` was `::Outer::::BAR`.

Without `--experimental-parser`, a constant written inside `class ::Foo`
isn't only missed: packwerk, and pks with it, can take `Bar` there for
a top-level `::Bar`. The CHANGELOG says that in place of "misses".
@dduugg
dduugg merged commit 7810fb4 into main Oct 13, 2026
21 checks passed
@dduugg
dduugg deleted the experimental-top-level-names branch October 13, 2026 22:47
dduugg added a commit that referenced this pull request Oct 14, 2026
bc0ad33 said that only a definition written from the top level, at the
top level of a file, has a name that starts with `::::`, so nothing else
changes. That isn't so. The constants defined in such a class, as
`::::Taco::Shell` in `class ::Taco; class Shell`, start with it too, as
does the `::::Taco` pks takes from `class ::Taco::Shell`, and a `Taco`
written inside `class ::Taco` was dropped the same way. pks now counts
each of these as a reference, as packwerk 3.3.1 does, and the CHANGELOG
says so. Tests cover a `Taco` inside `class ::Taco`, `::Taco = 1` at the
top level of a file, and `class ::Burrito` in `module Outer` and
`class ::Cee` in `module Aa; module Bb`, from the file to the references
pks lists.

e9a6092 said that for a reference written `Object::Bar`, the resolver
looks `Object` up as Ruby does. Inside `module CoreExt` it tries
`::CoreExt::Object::Bar` first, as Ruby would, but where `CoreExt`
defines an `Object` with no `Bar`, Ruby raises a NameError, and pks goes
on to `::Bar`, as it does for `Foo::Bar` when `CoreExt::Foo` has no
`Bar`. New tests name `module Object::Bar`, the constants in
`class Object::Bar`, `::Object::Object::BAR` and `ObjectSpace::Bar`, and
find constants defined through `Object` from another pack.

Comments now say that only the experimental parser and resolver drop a
leading `::Object`, and that the empty names come off the end as in
constant_resolver.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant