Skip to content

Support class << self in inline parser - #2972

Open
ksss wants to merge 4 commits into
ruby:masterfrom
ksss:ksss/inline-class-self
Open

ksss wants to merge 4 commits into
ruby:masterfrom
ksss:ksss/inline-class-self

Conversation

@ksss

@ksss ksss commented May 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add class << self support to the inline parser by introducing an AST node for the singleton class body.

Approach

class << self in a class or module body becomes AST::Ruby::Declarations::SingletonClassDecl, a member of the enclosing ClassDecl / ModuleDecl. The defs inside are kept as written (instance methods of the singleton class), and the consumers interpret them:

  • MethodBuilder#build_singleton builds them as singleton methods of the enclosing class
  • rbs validate validates their method types
  • Environment resolves type names in them, and registers no constant for the singleton class itself

The parser pushes SingletonClassDecl onto module_nesting, so current_module alone tells whether the current scope is a singleton class.

class Foo
  class << self
    # @rbs () -> Integer
    def zero = 0   # => Foo.zero
  end
end

Diagnostics

RBS has no notation for the singleton class of a singleton class, an anonymous singleton class, or constants on a singleton class. These are reported:

  • NestedSingletonScope: nested class << self, and def self.x inside class << self
  • TopLevelSingletonScope: class << self at top level (the singleton class of main)
  • NonSelfSingletonScope: class << expr for non-self expr
  • ConstantInSingletonScope: class, module, and constant assignments inside class << self. At runtime they are defined on the singleton class (Foo::Inner raises NameError; it is Foo.singleton_class::Inner), so they are not flattened into the enclosing namespace.

Annotations inside a rejected construct are also reported as UnusedInlineAnnotation, because they are not read.

Out of scope

Mixin (include / extend / prepend) and attribute (attr_*) calls inside class << self remain NotImplementedYet. They can be supported later by adding the members to SingletonClassDecl and handling them in the builders.

🤖 Generated with Claude Code

Following Steep, preserve the lexical nesting through `class << self`
and rebind only the method kind via a `@singleton_class_depth` flag
(reset on entry to a nested class/module body). `def x` becomes a
singleton method of the enclosing class; `class Inner` / `FOO = 1`
flatten via lexical resolution. Constructs with no RBS representation
(`NestedSingletonScope`, `TopLevelSingletonScope`,
`NonSelfSingletonScope`) are diagnosed. Mixin / attr stay
`NotImplementedYet`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ksss ksss added this to the RBS 4.1 milestone May 23, 2026
@soutaro soutaro modified the milestones: RBS 4.1, RBS 4.2 Jul 27, 2026
ksss and others added 3 commits September 28, 2026 22:29
Instead of rewriting `def` inside `class << self` into a singleton
DefMember at parse time, keep the source structure in the AST:
`SingletonClassDecl` is a member of the enclosing class/module and holds
its `def`s as written. MethodBuilder and `rbs validate` interpret them as
singleton methods of the enclosing class.

Classes, modules, and constants inside `class << self` belong to the
singleton class at runtime, so they are now reported with
`ConstantInSingletonScope` instead of being flattened into the enclosing
namespace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants