Skip to content

module_function without arguments is not applied to subsequent method definitions #1823

Open

Description

@thyresias

RDoc 8 appears not to handle correctly the no-argument form of Module#module_function.

In Ruby, this:

module M
  module_function

  def foo
  end
end

defines both:

  • M.foo, as a public singleton method;
  • M#foo, as a private instance method.

However, RDoc appears to document foo only as an instance method and does not create/document the corresponding module method M.foo.

After generating the documentation, I would expect M.foo to appear as a module method.

It does not.

Real-world example

This occurs in json 3.0.2.

In lib/json/common.rb, JSON.parse is defined using the no-argument form of module_function:

module JSON
  # ...

  module_function

  # ...

  def parse(source, on_load: nil, object_class: nil, array_class: nil, **options)
    # ...
  end
end

Ruby therefore exposes this method as:

JSON.parse(...)

but RDoc 8 does not appear to document JSON.parse.

Likely cause

RDoc already seems to handle the explicit form:

module_function :foo

by converting an existing method to a module function.

The no-argument form has different semantics: it changes the state of the module so that methods defined subsequently become module functions.

RDoc therefore needs to remember that module_function was called without arguments and apply that state to later def nodes, until the relevant scope ends or the state is otherwise changed.

Conceptually:

module_function

def foo
end

should result in RDoc recording both:

# private instance method
M#foo

# public singleton/module method
M.foo

Notes

This does not appear to be a Prism parsing issue itself: the syntax is parsed normally. The missing part seems to be in RDoc's tracking of the semantic state introduced by a no-argument module_function.

I encountered this while generating documentation for json 3.0.2 with RDoc 8.

Activity

  1. skatkov commented on Sep 16, 2026

    @skatkov
    Contributor

    By default, rdoc documents only public and protected methods. Did you try changing visibility?

    rdoc --visibility=private?

  2. thyresias commented on Sep 16, 2026

    @thyresias
    Author

    By default, rdoc documents only public and protected methods. Did you try changing visibility?

    rdoc --visibility=private?

    The module method is public. And it is not documented.

  3. tompng commented on Sep 16, 2026

    @tompng
    Member

    Currently, only module_function with argument is supported.

    module_function :name1, :name2

    module_function without argument is not supported nor implemented yet.
    I think it wasn't implemented in the old ripper-based parser too.

    Here's a sample code that may be related (found by Claude)

    module M
      private
    
      # private instance method, not documented
      def helper; end
    
      # Old ripper based parser wrongly treat this as `public`
      module_function
    
      # Documented as public "instance" method in old RDoc, not documented in current RDoc
      # Actually: private instance method and public singleton method
      def foo; end
    end
  4. thyresias commented on Sep 16, 2026

    @thyresias
    Author

    Currently, only module_function with argument is supported.

    module_function :name1, :name2

    module_function without argument is not supported nor implemented yet. I think it wasn't implemented in the old ripper-based parser too.

    Here's a sample code that may be related (found by Claude)

    module M
      private
    
      # private instance method, not documented
      def helper; end
    
      # Old ripper based parser wrongly treat this as `public`
      module_function
    
      # Documented as public "instance" method in old RDoc, not documented in current RDoc
      # Actually: private instance method and public singleton method
      def foo; end
    end

    I have patched RDoc to support module_function without argument : do you want me to fork & create a pull request ?

  5. tompng commented on Sep 16, 2026

    @tompng
    Member

    @thyresias
    Yes, please! I'd be happy to review it.

    To make the review smooth, here is what I'd expect

    module M
      # visibility changed to special :module_function
      module_function
    
      # attr will be private instance method
      attr_reader :module_function_attr
    
      # other instance method def will make it private and copies to public singleton_method
      def module_function_method; end
    
      # visibility reset to :private
      private
      def private_instance_method1; end
    
      # visibility reset to :public
      public
    
      def public_instance_method1; end
    end

    module_function can be reset with other visibility method, so treating as it creates a special visibility state @visibility = :module_function which is different from :private | :public | :protected seems a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions