Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

**RDoc** is Ruby's default documentation generation tool that produces HTML and command-line documentation for Ruby projects. It parses Ruby source code, C extensions, and markup files to generate documentation.
**RDoc** is Ruby's default documentation generation tool that produces HTML and command-line documentation for Ruby projects. It parses Ruby source code, C extensions, RBS signature files, and markup files to generate documentation.

- **Repository:** https://github.com/ruby/rdoc
- **Homepage:** https://ruby.github.io/rdoc
Expand Down Expand Up @@ -177,9 +177,10 @@ lib/rdoc/
├── rdoc.rb # Main entry point (RDoc::RDoc class)
├── version.rb # Version constant
├── task.rb # Rake task integration
├── parser/ # Source code parsers (Ruby, C, Markdown, RD)
├── parser/ # Source code parsers (Ruby, C, RBS, Markdown, RD)
│ ├── ruby.rb # Prism-based Ruby parser
│ ├── c.rb # C extension parser
│ ├── rbs.rb # RBS signature parser
│ └── ...
├── server.rb # Live-reloading preview server (rdoc --server)
├── generator/ # Documentation generators
Expand Down Expand Up @@ -235,10 +236,16 @@ exe/

### Parsers and Generators

- **Parsers:** Prism-based Ruby (`RDoc::Parser::Ruby`), C, Markdown, RD
- **Parsers:** Prism-based Ruby (`RDoc::Parser::Ruby`), C, RBS (`RDoc::Parser::RBS`), Markdown, RD
- **Generators:** HTML/Aliki (default), HTML/Darkfish (deprecated), RI, POT (gettext), JSON, Markup

Parser tests live in the `RDocParserRubyTest` class (`test/rdoc/parser/ruby_test.rb`).
Parser tests live under `test/rdoc/parser/`, including `RDocParserRubyTest` (`test/rdoc/parser/ruby_test.rb`) and `RDocParserRBSTest` (`test/rdoc/parser/rbs_test.rb`).

### RBS Documentation Input and Signature Merging

Selected `.rbs` files are first-class documentation input through `RDoc::Parser::RBS`. RBS declarations can create documentation for classes, modules, methods, attributes, and constants, or extend objects already documented from Ruby source.

RBS files under the project's `sig/` directory are also auto-discovered by `RDoc::RDoc` for type signature merging and live preview bookkeeping. Keep this distinction clear: `.rbs` parsing builds documentation objects, while `sig/**/*.rbs` auto-discovery feeds the existing RBS type-signature merge path.

### Code Object Model and Constant Aliases

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ rdoc --main README.md

You'll find information on the various formatting tricks you can use in comment blocks in the documentation this generates.

RDoc uses file extensions to determine how to process each file. File names ending `.rb` and `.rbw` are assumed to be Ruby source. Files ending `.c` are parsed as C files. All other files are assumed to contain just Markup-style markup (with or without leading `#` comment markers). If directory names are passed to RDoc, they are scanned recursively for C and Ruby source files only.
RDoc uses file extensions to determine how to process each file. File names ending `.rb` and `.rbw` are assumed to be Ruby source. Files ending `.c` are parsed as C files. Files ending `.rbs` are parsed as RBS signature files. All other files are assumed to contain just Markup-style markup (with or without leading `#` comment markers). If directory names are passed to RDoc, they are scanned recursively for C, Ruby, and RBS source files.

RBS files can document classes, modules, methods, attributes, and constants. When RBS declarations match objects already documented from Ruby source, their comments and type signatures extend the existing documentation.

To generate documentation using `rake` see [RDoc::Task](https://ruby.github.io/rdoc/RDoc/Task.html).

Expand Down
39 changes: 32 additions & 7 deletions lib/rdoc/code_object/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,7 @@ def add_class(class_type, given_name, superclass = '::Object')
@store.modules_hash[given_name]
return enclosing if enclosing
# not found: create the parent(s)
names = ename.split('::')
enclosing = self
names.each do |n|
enclosing = enclosing.classes_hash[n] ||
enclosing.modules_hash[n] ||
enclosing.add_module(RDoc::NormalModule, n)
end
enclosing = find_or_create_module_path ename
end
else
name = full_name
Expand Down Expand Up @@ -500,11 +494,42 @@ def add_method(method)
method
end

##
# Returns the owner context and local name for +constant_path+, creating
# missing namespace modules. A leading +::+ resolves from the top-level.

def find_or_create_constant_owner_name(constant_path) # :nodoc:
constant_path = constant_path.to_s
owner = constant_path.start_with?('::') ? top_level : self
constant_path = constant_path.delete_prefix('::')

owner_path, separator, name = constant_path.rpartition('::')
owner = owner.find_or_create_module_path owner_path unless separator.empty?

[owner, name]
end

##
# Finds or creates the module path under this context.

def find_or_create_module_path(path) # :nodoc:
path.to_s.split('::').inject(self) do |owner, name|
owner.classes_hash[name] ||
owner.modules_hash[name] ||
owner.add_module(RDoc::NormalModule, name)
end
end

##
# Adds a module named +name+. If RDoc already knows +name+ is a class then
# that class is returned instead. See also #add_class.

def add_module(class_type, name)
if name.to_s.include?('::')
owner, name = find_or_create_constant_owner_name name
return owner.add_module class_type, name unless owner == self
end

mod = @classes[name] || @modules[name]
return mod if mod

Expand Down
1 change: 1 addition & 0 deletions lib/rdoc/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -293,5 +293,6 @@ def handle_tab_width(body)
require_relative 'parser/changelog'
require_relative 'parser/markdown'
require_relative 'parser/rd'
require_relative 'parser/rbs'
require_relative 'parser/ruby'
require_relative 'parser/ruby_colorizer'
276 changes: 276 additions & 0 deletions lib/rdoc/parser/rbs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# frozen_string_literal: true

require 'rbs'

##
# Parse RBS signature files as first-class RDoc input.

class RDoc::Parser::RBS < RDoc::Parser
RBS_FILE_EXTENSION = /\.rbs$/

parse_files_matching RBS_FILE_EXTENSION

def scan
_, _, decls = ::RBS::Parser.parse_signature(@content)
decls.each do |decl|
parse_decl decl, @top_level
end
@top_level
end

private

def record_object_location(object, location)
object.line = location.start_line if location

if RDoc::ClassModule === object
@top_level.add_to_classes_or_modules object unless
@top_level.classes_or_modules.include? object
object.record_location @top_level
else
object.record_location @top_level
end

object
end

def rdoc_comment_for(decl, context)
rbs_comment = decl.comment if decl.respond_to?(:comment)
return unless rbs_comment

# TODO: Run RBS comments through RDoc's directive preprocessor so
# directives like :nodoc: affect the documented object.
comment = RDoc::Comment.new rbs_comment.string, context
comment.format = 'markdown'
comment
end

def local_module_name(type_name, namespace)
name = type_name.to_s
return name if name.start_with?('::')

namespace_names = namespace ? namespace.to_s.split('::') : []

namespace_names.length.downto(1) do |length|
qualified_name = namespace_names.take(length).join('::')
if module_name = @top_level.find_module_named("#{qualified_name}::#{name}")
return module_name.full_name
end
end

name
end

def merge_attr_rw(existing_rw, new_rw)
rw = +''
rw << 'R' if existing_rw.include?('R') || new_rw.include?('R')
rw << 'W' if existing_rw.include?('W') || new_rw.include?('W')
rw
end

def merge_documentation(object, comment, type_signature_lines)
if comment
object.comment = if object.comment.empty?
comment
else
merge_comments object, comment
end
end

# TODO: Track RBS-owned documentation overlays so incremental reparsing can
# replace stale comments and signatures from the previous RBS parse.
object.type_signature_lines ||= type_signature_lines
end

def merge_comments(object, comment)
document = RDoc::Markup::Document.new
document.concat object.parse(object.comment).parts
document << RDoc::Markup::Rule.new(1)
document.concat comment.parse.parts

merged_comment = RDoc::Comment.new "#{object.comment}\n---\n#{comment}", comment.location
merged_comment.document = document
merged_comment
end

def merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines)
method_names = []
method_names << name if rw.include?('R')
method_names << "#{name}=" if rw.include?('W')

methods = method_names.map { |method_name| context.find_method(method_name, singleton) }
methods.compact.each do |method|
merge_documentation method, comment, type_signature_lines
end

methods.all?
end

def rdoc_method_name(decl)
rbs_constructor_decl?(decl) ? 'new' : decl.name.to_s
end

def rdoc_method_singleton?(decl)
# TODO: RBS `self?` methods are :singleton_instance and should add both a
# singleton method and a private instance method.
rbs_constructor_decl?(decl) || decl.singleton?
end

def rdoc_method_visibility(decl)
rbs_constructor_decl?(decl) ? :public : decl.visibility
end

def rbs_constructor_decl?(decl)
decl.kind == :instance && decl.name == :initialize
end

def parse_attr_decl(decl, context)
rw = case decl
when ::RBS::AST::Members::AttrReader
'R'
when ::RBS::AST::Members::AttrWriter
'W'
when ::RBS::AST::Members::AttrAccessor
'RW'
end

comment = rdoc_comment_for(decl, context)
type_signature_lines = [decl.type.to_s]
name = decl.name.to_s
singleton = decl.kind == :singleton
if attribute = context.find_attribute(name, singleton)
merge_documentation attribute, comment, type_signature_lines
attribute.rw = merge_attr_rw attribute.rw, rw
return
end

if merge_attribute_methods(context, name, rw, singleton, comment, type_signature_lines)
return
end

attribute = RDoc::Attr.new(
name,
rw,
comment,
singleton: singleton
)
record_object_location attribute, decl.location
attribute.type_signature_lines = type_signature_lines
context.add_attribute attribute
attribute.visibility = decl.visibility if decl.visibility
end

def parse_class_decl(decl, context, _namespace)
owner, name = context.find_or_create_constant_owner_name decl.name
superclass = decl.super_class&.name&.to_s || '::Object'
klass = owner.add_class RDoc::NormalClass, name, superclass
record_object_location klass, decl.location
klass.add_comment rdoc_comment_for(decl, @top_level), @top_level if decl.comment

decl.members.each { |member| parse_decl member, klass, klass.full_name }
end

def parse_constant_decl(decl, context)
constant = RDoc::Constant.new decl.name.to_s, decl.type.to_s,
rdoc_comment_for(decl, context)
record_object_location constant, decl.location
context.add_constant constant
end

def parse_decl(decl, context, namespace = nil)
case decl
when ::RBS::AST::Declarations::Class
parse_class_decl decl, context, namespace
when ::RBS::AST::Declarations::Module, ::RBS::AST::Declarations::Interface
parse_module_decl decl, context, namespace
when ::RBS::AST::Declarations::ClassAlias,
::RBS::AST::Declarations::ModuleAlias
# TODO: Add RBS class and module aliases to the RDoc store.
nil
else
parse_member_decl decl, context, namespace
end
end

def parse_extend_decl(decl, context, namespace)
extend_decl = RDoc::Extend.new local_module_name(decl.name, namespace),
rdoc_comment_for(decl, context)
record_object_location extend_decl, decl.location
context.add_extend extend_decl
end

def parse_include_decl(decl, context, namespace)
include_decl = RDoc::Include.new local_module_name(decl.name, namespace),
rdoc_comment_for(decl, context)
record_object_location include_decl, decl.location
context.add_include include_decl
end

def parse_member_decl(decl, context, namespace)
case decl
when ::RBS::AST::Declarations::Constant
parse_constant_decl decl, context
when ::RBS::AST::Members::MethodDefinition
parse_method_decl decl, context
when ::RBS::AST::Members::Alias
parse_method_alias_decl decl, context
when ::RBS::AST::Members::AttrReader,
::RBS::AST::Members::AttrWriter,
::RBS::AST::Members::AttrAccessor
parse_attr_decl decl, context
when ::RBS::AST::Members::Include
parse_include_decl decl, context, namespace
when ::RBS::AST::Members::Extend
parse_extend_decl decl, context, namespace
when ::RBS::AST::Members::Private,
::RBS::AST::Members::Public
# TODO: Track standalone RBS visibility members.
nil
end
end

def parse_method_alias_decl(decl, context)
alias_def = RDoc::Alias.new(
decl.old_name.to_s,
decl.new_name.to_s,
rdoc_comment_for(decl, context),
singleton: decl.kind == :singleton
)
record_object_location alias_def, decl.location
context.add_alias alias_def
end

def parse_method_decl(decl, context)
comment = rdoc_comment_for(decl, context)
type_signature_lines = decl.overloads.map { |overload| overload.method_type.to_s }
method_name = rdoc_method_name(decl)
singleton = rdoc_method_singleton?(decl)
visibility = rdoc_method_visibility(decl)

if method = context.find_method(method_name, singleton)
merge_documentation method, comment, type_signature_lines
return
end

method = RDoc::AnyMethod.new method_name, singleton: singleton
record_object_location method, decl.location
method.type_signature_lines = type_signature_lines

if loc = decl.location
method.start_collecting_tokens :ruby
method.add_token line_no: loc.start_line, char_no: 1, text: loc.source
end

method.comment = comment if decl.comment
context.add_method method
method.visibility = visibility if visibility
end

def parse_module_decl(decl, context, _namespace)
mod = context.add_module RDoc::NormalModule, decl.name.to_s
record_object_location mod, decl.location
mod.add_comment rdoc_comment_for(decl, @top_level), @top_level if decl.comment

decl.members.each { |member| parse_decl member, mod, mod.full_name }
end
end
Loading