Documentation with RDoc and YARD
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18), which ships RDoc 7.0.4 and the ri command; features newer than Ruby 3.0 name the version that introduced them.
Ruby has two documentation tools that read the same kind of comments.
RDoc is the original: it is maintained by the Ruby team, the documentation of Ruby’s own classes is written in its markup, and it feeds ri, the command-line documentation viewer.
YARD is a community tool that reads RDoc-style comments and adds @tags (@param, @return, @example and so on) with optional type declarations, a plugin system, and the hosted service rubydoc.info.
This page explains both, shows how to write comments that either tool can use, how to generate and publish the result, how to check documentation coverage, and how the newest idea, RBS comments written next to the code, relates to both.
Everything here builds on the shelf gem used throughout this section: its Catalog, Loan and Store::Memory classes are the documented examples.
The gem and its packaging are the subject of Creating and Publishing Gems, and Rake; installing the tools into a project is covered in RubyGems and Bundler; the interactive way to read documentation (show_doc) is in IRB and Debugging.
RuboCop’s documentation cops are in Style, Linting and Static Analysis, the type signatures that the RBS section touches are in Type Checking with RBS, TypeProf, Steep and Sorbet, and generated methods (the other half of "documenting generated code") are created in Dynamic Dispatch and Method Definition and Hooks, eval and DSLs.
For the same ideas in other languages see Javadoc, Python docstrings, Rust rustdoc and Scaladoc, and the Go section for Go doc comments.
How the examples were checked.
Every Ruby listing was syntax-checked with ruby -wc, run on Ruby 4.0.7 with warnings enabled (the ones that execute print no warnings), and linted with RuboCop 1.92 using the configuration of the shelf gem (double-quoted strings, NewCops: enable, Style/Documentation off) plus two Layout/LeadingCommentSpace options that the RBS and YARD comments need (shown in the RBS section).
RDoc 7.0.4 and ri were run for real, and the output blocks are the real output; absolute paths of the machine that produced them are abbreviated (rid is the directory that rdoc --ri wrote).
YARD is a gem that is not part of Ruby, and it was not installed to produce this page: the YARD listings are comment-only Ruby files that were checked for syntax and style, and the YARD behaviour is described from its official documentation (YARD 0.9.45 at the time of writing), never from a run.
The example.com addresses and Example Author are placeholders.
The documentation tools at a glance
| Tool | Role |
|---|---|
RDoc ( |
Reads Ruby (and C, Markdown, RD and RBS) files, takes the comments that precede definitions, and generates HTML (the Aliki theme in RDoc 7, Darkfish before), |
|
Reads the |
YARD ( |
Community alternative. Understands RDoc markup and Markdown, adds |
RBS inline comments |
|
RuboCop documentation cops |
|
The flow is the same for every tool: the comments next to the code are parsed, stored in an intermediate database, and rendered by a generator.
YARD reads plain RDoc comments, and RDoc does not understand @tags (it prints them as text, as shown below), so a project that wants good ri output and YARD tags at once has to choose. A decision guide is at the end of the page.
Writing documentation comments
Where a comment attaches
RDoc and YARD attach a comment to the definition that follows it: a class, a module, a method, a constant, an attr_* call, or an alias.
Put the comment directly above the definition, as consecutive # lines.
Both tools are forgiving about blank lines, and that has a consequence for file headers:
-
RDoc 7.0.4 uses the nearest comment block above the definition, even when one or two blank lines separate them. If a class has a comment of its own, a copyright header above it is ignored; if it has none, the header becomes the class documentation (observed: a two-line copyright header turned into the description of the class below it).
-
A statement between the header and the definition (a
require, say) detaches it. The explicit, readable way is to wrap the header in#--and#++lines, which RDoc skips. -
YARD behaves the same way for headers, which is why it added the
#-separator (see the YARD section).
The conventions that make a comment useful:
-
The first sentence or paragraph is the summary; tools show it in lists and indexes, so make it a complete sentence that stands alone.
-
Name a parameter in the text (for example as
+name+in RDoc markup) so that the reader andrdoc -C1can match it with the signature. -
Say what the method returns when it is not obvious, what it raises, and what it yields.
-
Start comments for generated or metaprogrammed methods with
##(two hashes): RDoc uses that spelling to recognize comments that are not attached to ordinary definitions (see Documenting generated code). -
A
# frozen_string_literal: truemagic comment (and the other magic comments described in Ruby 4.0 documentation: Comments) belongs on the first lines; it is not part of the documentation.
A license header that RDoc must skip looks like this:
# frozen_string_literal: true
#--
# Copyright (c) Example Author
# Licensed under the MIT License.
#++
# Looks books up by author.
class AuthorIndex
def initialize
@titles = Hash.new { |hash, name| hash[name] = [] }
end
# Returns the titles of one author.
def titles_by(name)
@titles[name]
end
end
The shelf gem’s catalogue, documented for RDoc, shows the basic forms: a class comment with an example, a heading, a list and cross-references; a method that names its parameter in monofont markup; and a :call-seq: override for a method whose signature is not informative.
# frozen_string_literal: true
module Shelf
# The catalogue of books, guarded by a mutex.
#
# A Catalog stores books through a *store* object and exposes them through
# Enumerable, so every Enumerable method works on it:
#
# catalog = Shelf::Catalog.new
# catalog.add(book).add(other)
# catalog.map(&:title)
#
# == Stores
#
# Any object that responds to +all+ and +add+ can be used. The default is
# Shelf::Store::Memory.
#
# == See also
#
# * Shelf::Store::Memory
# * Shelf::Loan
class Catalog
include Enumerable
# Creates an empty catalogue backed by +store+.
def initialize(store: Shelf.config.store)
@store = store
@mutex = Thread::Mutex.new
end
# Adds +book+ to the catalogue.
#
# Returns the catalogue itself, so calls can be chained.
def add(book)
@mutex.synchronize { @store.add(book) }
self
end
# :call-seq:
# each { |book| ... } -> catalog
# each -> enumerator
#
# Yields every book, or returns an Enumerator when no block is given.
def each(&block)
return enum_for(:each) unless block
@mutex.synchronize { @store.all }.each(&block)
self
end
end
end
Source: RDoc documentation: RDoc::Markup
Reading the comment with ri shows the result of rdoc --ri for the class and for the method that used :call-seq:. The class output starts with the superclass and the included modules, then the text of the comment, then the lists of methods.
# Shelf::Catalog < Object
---
# Includes:
Enumerable (from rid)
(from rid)
---
The catalogue of books, guarded by a mutex.
A Catalog stores books through a **store** object and exposes them
through Enumerable, so every Enumerable method works on it:
catalog = Shelf::Catalog.new
catalog.add(book).add(other)
catalog.map(&:title)
## Stores
Any object that responds to `all` and `add` can be used. The default is
Shelf::Store::Memory.
## See also
* Shelf::Store::Memory
* Shelf::Loan
---
# Class methods:
new
# Instance methods:
add
each
# Shelf::Catalog#each
(from rid)
### Implementation from Catalog
---
each { |book| ... } -> catalog
each -> enumerator
---
Yields every book, or returns an Enumerator when no block is given.
Source: RDoc documentation: ri
RDoc also documents what it can read from the code without any comment: the class name and superclass, included modules, the constants, the attributes (with their [R], [W] or [RW] type), the methods with their parameter lists and the arguments that a method yields, and the aliases. A method without a comment is still listed, and the coverage report names it.
Derived documentation and what a comment adds
| RDoc derives from the code | A comment adds |
|---|---|
Class or module name, superclass, |
The purpose of the type, its invariants, an example of use. |
Method name and parameter list, including keyword parameters and defaults ( |
What each parameter means (name it as |
The arguments passed to |
What the block receives and what it must return; override with |
Attribute names and access ( |
What the attribute holds and its type. |
Constants and their names |
What the constant is for. |
Aliases ( |
Nothing is required: RDoc links the alias to the original. |
RDoc markup
RDoc’s own markup is a light, indentation-based format close to a wiki. The comment markers (#) are stripped, and the rest is read by block type.
A block’s kind is decided by its indentation and by its first characters.
The RDoc version matters here: the online reference documents the current RDoc (8.x), while Ruby 4.0.7 ships 7.0.4; the table marks what 7.0.4 does not understand.
Blocks
| Block | Written as |
|---|---|
Paragraph |
Consecutive non-empty lines at the same margin; a blank line starts a new paragraph. |
Heading |
One to six |
Bullet list |
|
Numbered list |
|
Lettered list |
|
Labelled list |
|
Verbatim block |
Text indented more than the current margin. Whitespace is kept and markup inside is not interpreted. If it parses as Ruby it is syntax-highlighted; if it does not (a typo, or a pseudo-code snippet) it is shown plain, which is the usual reason for "my example is not highlighted". |
Block quote |
|
Horizontal rule |
A line of three or more hyphens and nothing else. |
Directive |
A line of the form |
Text markup
| Written as | Result | Notes |
|---|---|---|
|
bold |
The shorthand works for a single word; use the HTML tag for a phrase. |
|
italic |
Single word shorthand, or tags for phrases. |
|
|
The plus-sign shorthand works for one word of letters, digits, colons, dots, slashes, brackets and hyphens (for example the class name or the method reference). Use |
backticks around a word |
(7.0.4) typographic quotes |
In RDoc 7.0.4 a backtick is not monofont markup: it is converted to a typographic quote. The current online reference documents backticks as monofont, so check |
|
strike-through (newer RDoc only) |
In 7.0.4 |
|
the word, unlinked |
A backslash before a word that RDoc would turn into a cross-reference suppresses the link. A double backslash is a literal backslash. |
|
a copyright sign, en dash, em dash, ellipsis, typographic quotes |
Typographic conversions are applied to paragraphs, lists and headings, not to verbatim blocks. |
The markup of the shelf documentation comments can be tried without running a whole generation: rdoc --pipe converts RDoc text read from standard input to HTML.
= Shelf
== Borrowing
A *bold* word, an _italic_ word, a +monofont+ word and <tt>Hash#fetch(:a)</tt>.
A -- dash, (c) and "quotes" are converted. \Shelf::Catalog is not linked.
* a bullet
* another bullet
continued on a second line
1. first step
2. second step
[isbn] a labelled item
title:: a same-line label
def verbatim_code
:kept_as_written
end
See Shelf::Catalog#add, {the guide}[https://example.com/guide]
and Example[rdoc-ref:Shelf::Catalog].
<h1 id="label-Shelf">Shelf</h1>
<h2 id="label-Borrowing">Borrowing</h2>
<p>A <strong>bold</strong> word, an <em>italic</em> word, a <code>monofont</code> word and <code>Hash#fetch(:a)</code>. A – dash, © and “quotes” are converted. Shelf::Catalog is not linked.</p>
<ul><li>
<p>a bullet</p>
</li><li>
<p>another bullet continued on a second line</p>
</li></ul>
<ol><li>
<p>first step</p>
</li><li>
<p>second step</p>
</li></ol>
<dl class="rdoc-list label-list"><dt>isbn</dt>
<dd>
<p>a labelled item</p>
</dd></dl>
<dl class="rdoc-list note-list"><dt>title</dt>
<dd>
<p>a same-line label</p>
</dd></dl>
<pre><code>def verbatim_code
:kept_as_written
end
</code></pre>
<p>See Shelf::Catalog#add, <a href="https://example.com/guide">the guide</a> and <a href="rdoc-ref:Shelf::Catalog">Example</a>.</p>
The same conversion is available as a library, which is how tools embed RDoc markup (a documentation server, a chat bot, a README preview): RDoc::Markup::ToHtml renders a string, and RDoc::Markup.parse returns the document tree (RDoc::Markup::Document) so that a program can inspect the headings, paragraphs and lists.
# frozen_string_literal: true
require "rdoc"
text = <<~RDOC
= Loans
A loan is *lent* or +overdue+.
* lent
* overdue
RDOC
html = RDoc::Markup::ToHtml.new(RDoc::Options.new).convert(text)
puts html
document = RDoc::Markup.parse(text)
puts document.parts.map { |part| part.class.name.split("::").last }.inspect
puts RDoc::VERSION
<h1 id="label-Loans"><a href="#label-Loans">Loans</a></h1>
<p>A loan is <strong>lent</strong> or <code>overdue</code>.</p>
<ul><li>
<p>lent</p>
</li><li>
<p>overdue</p>
</li></ul>
["Heading", "BlankLine", "Paragraph", "BlankLine", "List"]
7.0.4
Source: RDoc documentation: RDoc::Markup
Links and cross-references
RDoc turns many strings into links without any markup, and offers explicit forms for the rest.
| Written as | Links to |
|---|---|
|
A class, an instance method, a class method, and (inside the same class) a method of the current class. An unknown name is left as plain text. |
|
The URL itself. A fully qualified image URL ( |
|
One-word link text. |
|
Multi-word link text. |
|
A documented object or file by name (class, module, method, constant, a page such as |
|
A label (an anchor on the page), optionally with a "return" label to link back. |
|
A file in the generated output, relative to the output root. |
|
An image that is also a link. |
|
A heading or section of another page (spaces written as plus signs). |
The first two rows are why most comments need no link syntax at all; the explicit forms are for pages (README, guides) and for headings. --warn-missing-rdoc-ref makes unresolved references loud, and it is on by default in the .rdoc_options written by 7.0.4 (shown later).
Directives
A directive is a line of the form :name: or :name: argument inside a comment (or a trailing comment on a definition line) that tells RDoc something it cannot read from the code. Directives are removed from the output text.
| Directive | Effect |
|---|---|
|
Trailing on a definition line: do not document this object. |
|
Trailing on a definition line: document this object even though RDoc would skip it (a private method, say). RuboCop’s |
|
On a line by themselves: skip everything between them. |
|
Trailing on |
|
Replaces the generated signature with one or more lines you write, including the return type ( |
|
Overrides the displayed argument list, trailing on the |
|
Overrides the displayed block parameters, trailing on the |
|
Starts a named section; the methods that follow are grouped under it until the next |
|
Puts the next method only into the named section, then returns to the current one. |
|
Includes a file at this point, indented like the directive (indent the directive to get a code block). The file is searched in the directory of the current file and then in the directories of |
|
Sets the markup for this comment ( |
|
Declares a method that the parser cannot see (see Documenting generated code). The name is optional when the next line is a call whose first argument is the name. |
|
Declares an attribute that the parser cannot see. |
|
Deprecated: set the first page or the title from inside a source file. RDoc 7.0.4 still honours them but prints a deprecation warning that names the replacement ( |
The shelf Loan class uses most of them in one file: a section for each group of methods, :nodoc: for bookkeeping, the #-- and #++ markers for implementation notes, :category: for one method, and :yields: for a method that yields.
# frozen_string_literal: true
module Shelf
# A loan of a book, with a state driven by pattern matching.
class Loan
# The states a loan can be in.
STATES = %i[available lent overdue returned].freeze
# :section: Accessors
# The borrowed book.
attr_reader :book
# The current state, one of STATES.
attr_reader :state
# :section: Construction
# Creates a loan for +book+ in the given +state+.
#
# Raises ArgumentError if the state is not in STATES.
def initialize(book, state: :available)
raise ArgumentError, "unknown state: #{state}" unless STATES.include?(state)
@book = book
@state = state
end
# Builds a loan that is already lent out.
def self.lent(book)
new(book, state: :lent)
end
# :section:
# Describes the loan in one sentence.
def describe
case { state:, book: }
in { state: :available } then "#{book.title} is on the shelf"
in { state: :lent | :overdue => s } then "#{book.title} is #{s}"
in { state: :returned } then "#{book.title} came back"
end
end
# Internal bookkeeping, hidden from the generated documentation.
def audit_trail # :nodoc:
[]
end
#--
# Implementation notes that must not appear in the output.
#++
# Returns true when the book is out.
def out?
%i[lent overdue].include?(state)
end
# :category: Predicates
# Returns true when the loan is overdue.
def overdue?
state == :overdue
end
# Yields the book when it is out.
#
# :yields: book
def each_lent
yield book if out?
end
end
end
ri shows what ended up in the output: audit_trail (the :nodoc: method) is gone, the implementation note between #-- and #++ is gone, and the class is described from its comment. The :section: and :category: names appear as headings in the HTML, not in ri.
# Shelf::Loan < Object
(from rid)
---
A loan of a book, with a state driven by pattern matching.
---
# Constants:
STATES
: The states a loan can be in.
# Class methods:
lent
new
# Instance methods:
book
describe
each_lent
out?
overdue?
state
# Attributes:
attr_reader book
attr_reader state
Source: RDoc documentation: ri
:nodoc: keeps the text of a class or method out of the output, :nodoc: all extends that to everything nested, :doc: forces a private method in, and :enddoc: drops what follows it. With the default --visibility=protected, private methods are not documented unless :doc: is used. A single file shows all of that; ri lists the methods that survived:
# frozen_string_literal: true
# Public namespace.
module Pub
# Hidden with children.
module Internal # :nodoc: all
# Child of a hidden module.
class Helper
# A constant, so the class is not empty.
LIMIT = 1
end
end
# Hidden only itself.
class Quiet # :nodoc:
# Visible child.
class Loud
# A constant, so the class is not empty.
LIMIT = 2
end
end
# Visible class.
class Shown
# Visible method.
def a; end
# Hidden method.
def b; end # :nodoc:
# Private but forced into the docs.
def c; end # :doc:
private :c
# Private, not documented by default.
def d; end
private :d
# :enddoc:
# Never documented, everything below is dropped.
def e; end
end
end
# Instance methods:
a
c
b (:nodoc:) and e (after :enddoc:) are missing, c is present because of :doc: although it is private, and d is missing because it is private. The only RuboCop offense in this listing is deliberate: Style/CommentedKeyword flags the trailing :doc: on the def c line, and RDoc needs the trailing position for that directive. Running rdoc --visibility=private (or -a) would show d too. Note that the classes marked :nodoc: still get a (nearly empty) page in the HTML output of 7.0.4; the directive removes their text, not their file.
Documenting generated code
Methods created with define_method, attr_accessor inside loops, a DSL, or method_missing are invisible to a static parser. RDoc and YARD each offer a way to declare them. (DiLeo’s chapter on metaprogramming raises the same advice: generated methods need documentation of their own, and tests that exercise them.)
For RDoc, put a stand-alone comment that begins with ## before the call, and use the directive that names the method. The comment is not attached to the following line; the directive creates the method.
# frozen_string_literal: true
module Shelf
# Predicate helpers for loans, generated from the list of states.
#
# One predicate is defined per state with define_method.
module States
# The state names, in lifecycle order.
LIST = %i[available lent overdue returned].freeze
##
# :method: lent?
#
# Returns true when the loan is in the +lent+ state.
#
# The predicates are generated by the loop below, so RDoc cannot see
# them. The :method: directive declares one of them for the reader.
LIST.each do |name|
define_method(:"#{name}?") { state == name }
end
# :stopdoc:
INTERNAL_LOOKUP = LIST.to_h { |s| [s, s.to_s] }.freeze
# :startdoc:
##
# :singleton-method: names
#
# Returns the state names as strings.
singleton_class.define_method(:names) { INTERNAL_LOOKUP.values }
end
end
# Shelf::States
(from rid)
---
Predicate helpers for loans, generated from the list of states.
One predicate is defined per state with define_method.
---
# Constants:
LIST
: The state names, in lifecycle order.
# Class methods:
names
# Instance methods:
lent?
# Shelf::States#lent?
(from rid)
### Implementation from States
---
lent?()
---
Returns true when the loan is in the `lent` state.
The predicates are generated by the loop below, so RDoc cannot see them.
The :method: directive declares one of them for the reader.
Notes that come from running this:
-
The
:method:directive documents one predicate,lent?. There is no loop variable in the comment, so write one directive for each generated method that matters, or document the family in the module comment. -
:stopdoc:and:startdoc:hide the lookup table from the output; the constantLISTkeeps its short comment. -
A directive is ignored when a standard rdocable item (a real
def,attr_*call or constant) follows it directly: a:attr_accessor:directive placed right above a realattr_accessorcreates nothing. The directives exist for declarations that RDoc cannot see; put them before a blank line and a statement that RDoc does not document, as above. -
A comment that begins with a single
#also worked for:method:here, but the documented and portable form is##.
YARD declares generated methods with the @!method, @!attribute and @!macro directives; the YARD section has the equivalent example.
Markdown in RDoc
RDoc reads Markdown as well as its own markup. There are three levels at which to choose the format:
-
per project:
--markup markdown(ormarkup: markdownin.rdoc_options, orrdoc.markup = "markdown"inRDoc::Task); -
per file or per comment: the
:markup: markdowndirective; -
implicitly, for files that are Markdown:
README.mdand other.mdfiles are parsed as Markdown.
The Markdown reader in RDoc 7.0.4 handles headings, emphasis, inline code, links, fenced code blocks, lists, block quotes and tables; it does not render ~~strike~~.
Cross-references written as [text](rdoc-ref:Shelf::Catalog) are resolved.
Formats can be mixed in one file: a comment can switch back with :markup: rdoc.
# frozen_string_literal: true
module Shelf
module Store
# :markup: markdown
#
# A store that keeps books in an array.
#
# ## Usage
#
# ```ruby
# store = Shelf::Store::Memory.new
# store.add(book).all # => [book]
# ```
#
# * `all` returns a copy
# * `add` returns the store
#
# See [the catalogue](rdoc-ref:Shelf::Catalog) for the public entry point.
class Memory
# Returns a copy of the books.
#
# :markup: rdoc
# Mixed formats are allowed: this comment is in *RDoc* markup.
def all
@books.dup
end
end
end
end
The class comment is Markdown (## Usage is a heading, the fenced block is code, the rdoc-ref link resolves to Catalog.html), and the method comment is RDoc markup, where *RDoc* is bold.
rdoc --pipe --markup markdown converts Markdown from standard input, which also shows what is supported:
$ printf '## Head\n\n* `a`\n* **b**\n\n```ruby\nx = 1\n```\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n~~del~~\n' | rdoc --pipe --markup markdown
<h2 id="label-Head">Head</h2>
<ul><li>
<p><code>a</code></p>
</li><li>
<p><strong>b</strong></p>
</li></ul>
<pre><code>x = 1
</code></pre>
<table role="table">
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>~del~</p>
The other two formats named by --markup are rd (the Ruby Document format) and tomdoc (a convention with Returns, Examples sections); both are rare today.
In YARD the equivalent switch is --markup/-m in .yardopts (see below).
The rdoc command
Generating documentation
rdoc [options] [names...] parses the named files and directories (the current directory when none is given), resolves cross-references between all of them, and writes the output (to doc/ by default).
A directory is traversed recursively for Ruby and C sources; other files such as README.md or .rdoc guide pages must be named on the command line (or listed in a .document file, below). The parser is chosen by extension: Ruby (.rb, .rbw), C, Markdown (.md), RD, RBS, and plain RDoc markup for anything else.
$ rdoc --main README.md --title "Shelf 0.1.0" -o doc README.md lib
The formats are chosen with -f/--format: aliki (the default theme in RDoc 7.0.4; it has search and a modern layout), darkfish (the previous default; the 7.0.4 README calls it deprecated and announces its removal in 8.0), ri (data for the ri command, like -r) and pot (gettext templates for translating the documentation).
Ruby 3.4, 3.3 and 3.2 ship RDoc 6.14.0, 6.6.3.1 and 6.5.1.1 respectively, and generate Darkfish pages by default.
The options that matter day to day:
| Option | Meaning |
|---|---|
|
Output directory (default |
|
The file, class or module shown on the first page, usually |
|
The title of the documentation set. |
|
Skip matching files (repeatable). A default exclude list (backup files, gemspecs) applies unless |
|
Minimum visibility to document: |
|
|
|
|
|
Print what is undocumented and exit without generating files. |
|
Output encoding. All input is converted to it (default UTF-8). |
|
Directories searched by |
|
Keep the leading |
|
Show line numbers in source listings. |
|
Show methods of included modules in the including class. |
|
Copy static files (images used by the pages) to the output. |
|
Root of the source tree, and the directory of guide pages that are not classes. |
|
Reparse everything even if nothing is newer than the flag file / write no files. |
|
Convert RDoc text on standard input to HTML (used above). |
|
Write the current options to |
|
Quiet, verbose and debug output. |
Two environment-related pitfalls from the real run:
-
RDoc converts everything to the output encoding, and in a shell whose locale is
C(a container, a CI job) the Aliki generator fails witherror generating index.html: "\xF0" on US-ASCII (Encoding::InvalidByteSequenceError). SettingLANG=en_US.UTF-8(or another UTF-8 locale) fixes it; passing-e UTF-8did not in 7.0.4, whereasRUBYOPT=-Eutf-8(which sets the default external encoding) did. -
RDoc is a bundled gem in Ruby 4.0. Under Bundler,
require "rdoc/task"in a Rakefile fails unless the Gemfile lists it (see below).
Coverage: finding undocumented code
rdoc -C is the documentation equivalent of a test coverage report. It lists the undocumented items by file and prints totals, which makes it usable as a CI gate (compare the printed percentage with a threshold in a script).
The following items are not documented:
In files:
* lib/shelf/catalog.rb
* lib/shelf/loan.rb
* lib/shelf/states.rb
* lib/shelf/store/memory.rb
module Shelf
end
In files:
* lib/shelf/store/memory.rb
module Shelf::Store
end
Files: 4
Classes: 3 (0 undocumented)
Modules: 3 (2 undocumented)
Constants: 2 (0 undocumented)
Attributes: 2 (0 undocumented)
Methods: 12 (0 undocumented)
Total: 22 (2 undocumented)
90.91% documented
Source: RDoc documentation: RDoc::Stats
-C1 adds the parameters that no comment mentions. In the shelf documentation book in Loan.lent and in each_lent is never named, so both are reported. The report works on the text of the parameter list, so in 7.0.4 a keyword parameter with a default (store: Shelf.config.store, state: :available) is reported even though the comment names it, and the :call-seq: text of each shows up as a pseudo-parameter; read the list as a hint, not as a verdict:
The following items are not documented:
In files:
* lib/shelf/catalog.rb
* lib/shelf/loan.rb
* lib/shelf/states.rb
* lib/shelf/store/memory.rb
module Shelf
end
class Shelf::Catalog # is documented
# in file lib/shelf/catalog.rb:26
# +store:Shelf.config.store+ is not documented
def self.new(store: Shelf.config.store); end
# in file lib/shelf/catalog.rb:44
# +each->enumerator+ is not documented
def each(&block); end
end
class Shelf::Loan # is documented
# in file lib/shelf/loan.rb:30
# +book+ is not documented
def self.lent(book); end
# in file lib/shelf/loan.rb:22
# +state::available+ is not documented
def self.new(book, state: :available); end
# in file lib/shelf/loan.rb:69
# +book+ is not documented
def each_lent(); end
end
In files:
* lib/shelf/store/memory.rb
module Shelf::Store
end
Configuration: .rdoc_options, .document, RDOCOPT and RDoc::Task
RDoc has four ways to be configured.
-
.rdoc_optionsis a YAML file at the project root that RDoc loads on every run. Create it from command-line flags with--write-options; commit it, and ship it in the gem so that the settings travel with the sources. Command-line flags override it. Some options cannot be stored in it (--dry-run,--force-output,--template). -
.documentis a plain-text file that lists which files to document in its directory (shell globs,#comments). When RDoc enters a directory that has one, only the listed patterns are processed. A.documentin a subdirectory narrows what is documented there. -
RDOCOPTis an environment variable whose content is prepended to the command line (export RDOCOPT="--show-hash"). It is machine-local and invisible to collaborators; use it only for personal preferences. -
RDoc::Taskis the Rake integration.
Writing the options for the shelf gem produces a file like this (YAML keys are the option names with underscores; main_page is --main, op_dir is --output):
---
encoding: UTF-8
static_path: []
rdoc_include: []
page_dir:
apply_default_exclude: true
autolink_excluded_words: []
canonical_root:
charset: UTF-8
class_module_path_prefix:
embed_mixins: false
exclude: []
file_path_prefix:
footer_content:
hyperlink_all: false
line_numbers: false
locale_dir: locale
locale_name:
main_page: README.md
markup: markdown
output_decoration: true
show_hash: false
skip_tests: true
tab_width: 8
template_stylesheets: []
title: Shelf 0.1.0
visibility: :public
warn_missing_rdoc_ref: true
webcvs:
The .document file below restricts documentation to lib/ and the README; in a directory with lib/, scripts/ and test/, rdoc -C reported only the lib and README files (and ri only the class in lib):
# What to document
lib
README.md
Source: RDoc documentation: RDoc::RDoc
RDoc::Task defines the tasks rdoc, rerdoc (rebuild), clobber_rdoc (remove the output) and rdoc:coverage in a Rakefile. (Newer RDoc releases add a live-reload rdoc:server; it is not in 7.0.4.)
# frozen_string_literal: true
require "rdoc/task"
RDoc::Task.new do |rdoc|
rdoc.title = "Shelf 0.1.0"
rdoc.main = "README.md"
rdoc.rdoc_dir = "doc"
rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
rdoc.options << "--visibility" << "public"
end
rake clobber_rdoc # Remove RDoc HTML files
rake rdoc # Build RDoc HTML files
rake rdoc:coverage # Print RDoc coverage report
rake rerdoc # Rebuild RDoc HTML files
Source: RDoc documentation: RDoc::Task
Because RDoc is no longer a default gem, a project that calls require "rdoc/task" must declare the dependency. Without it, bundle exec rake aborts with the following (path abbreviated):
Rakefile:3: warning: rdoc/task is found in rdoc, which is not part of the default gems since Ruby 4.0.0.
You can add rdoc to your Gemfile or gemspec to fix this error.
rake aborted!
LoadError: cannot load such file -- rdoc/task (LoadError)
Source: Ruby 4.0 NEWS
The fix is a development dependency, so that the documentation tasks are available to contributors but not required by the users of the gem:
# frozen_string_literal: true
source "https://rubygems.org"
gemspec
gem "rake", "~> 13.0"
gem "rdoc", "~> 7.0", require: false
Source: Ruby 4.0 NEWS
Parser pitfalls in RDoc 7.0.4
The Ruby parser that rdoc uses by default in 7.0.4 (and in 6.14 of Ruby 3.4 and 6.6 of Ruby 3.3) reads the source with its own token reader, and modern syntax can confuse it. Two cases were reproduced:
-
Endless methods (
def label = "second", Ruby 3.0). The signature is shown aslabel(= "second")in coverage reports, and, worse, the parser loses track of where the class ends, so the next class in the file is nested inside the first one:
# frozen_string_literal: true
module Shelf
# A book with an endless method.
class Edition
# Returns the label.
def label = "second"
end
# A class that follows it.
class Cover
# Returns the colour.
def colour
"red"
end
end
end
Shelf
Shelf::Edition
Shelf::Edition::Cover
-
Struct.newandData.define.Author = Data.define(:name) do ... endis documented as a constant, and the methods inside the block are not documented;class Publisher < Data.define(:name)gives a normal class page. The same holds forStruct.new(...) do ... end. Document the constant’s comment well, or use the subclass form for public value objects.
RDoc 7.0.4 contains an experimental Prism-based parser, enabled by setting the environment variable RDOC_USE_PRISM_PARSER. It prints a banner and produces the correct structure for the file above:
$ RDOC_USE_PRISM_PARSER=1 rdoc -q --ri -o rid endless.rb
=========================================================================
RDoc is using the experimental Prism parser to generate the documentation
=========================================================================
Shelf
Shelf::Cover
Shelf::Edition
Until that parser is the default, a project that documents with RDoc either avoids endless methods in public classes, or enables the Prism parser in CI and checks the result. Ruby 3.4’s RDoc 6.14 has the same two options (the parser variable exists there too); Ruby 3.3’s RDoc has the problem without the option.
The ri command
ri (Ruby Information) is the offline viewer for the data that RDoc generates. It is installed with Ruby together with the ri data of the core classes and the standard library (in the build used for this page, under share/ri/4.0.0/system), and the data of gems is added when gems are installed with documentation or when gem rdoc is run.
| Command | Shows |
|---|---|
|
A class or module. A prefix that is unique is enough ( |
|
An instance method ( |
|
The list of pages of the Ruby core documentation; |
|
The pages of an installed gem, when it was installed with |
|
Interactive mode with completion: type names until a blank line. |
|
All classes and modules; all the documentation for a class including its methods. |
|
The output format (the default is |
|
Serves the documentation over HTTP (default port 8214). |
|
Add or restrict the directories that are searched, and print them. |
$ ri -T -f markdown 'Array#map' | head -9
# Array#map
(from ruby core)
---
map()
---
With a block given, calls the block with each element of `self`; returns
The pager is chosen with RI_PAGER or PAGER. In IRB, show_doc opens the same documentation without leaving the session. ri --list-doc-dirs shows where ri looks: the system directory under the Ruby installation (share/ri/4.0.0/system), the site directory, and a per-user directory (~/.local/share/rdoc on the machine that produced this page).
Documentation for your own project is generated with rdoc --ri (to the user directory) or rdoc -f ri -o DIR, and read with ri -d DIR Name, which is how the ri outputs above were produced. For gems, gem rdoc GEMNAME builds the data after installation.
Source: RDoc documentation: ri
Documentation in gems
A gem carries documentation in four places, all of them declared in the gemspec of Creating and Publishing Gems, and Rake.
| Field | Purpose |
|---|---|
|
Must list the sources that carry the comments and the README; documentation generated on the user’s machine reads these installed files. |
|
Pages added to the documentation: |
|
Arguments passed to |
|
The link RubyGems.org shows as "Documentation". Related keys: |
# frozen_string_literal: true
require_relative "lib/shelf/version"
Gem::Specification.new do |spec|
spec.name = "shelf"
spec.version = Shelf::VERSION
spec.authors = ["Example Author"]
spec.summary = "A small library catalogue."
spec.homepage = "https://example.com/shelf"
spec.license = "MIT"
spec.required_ruby_version = ">= 4.0"
spec.metadata["documentation_uri"] = "https://example.com/shelf/docs"
spec.metadata["source_code_uri"] = "https://example.com/shelf/src"
spec.metadata["changelog_uri"] = "https://example.com/shelf/changelog"
spec.metadata["rubygems_mfa_required"] = "true"
spec.files = Dir["lib/**/*.rb", "exe/*", "sig/**/*.rbs", "README.md", "CHANGELOG.md", "LICENSE.txt"]
spec.extra_rdoc_files = ["README.md", "CHANGELOG.md"]
spec.rdoc_options = ["--main", "README.md", "--title", "Shelf #{Shelf::VERSION}"]
spec.bindir = "exe"
spec.executables = ["shelf"]
spec.require_paths = ["lib"]
end
gem build validates the file and gem specification shows what was recorded in the archive:
$ gem build shelf.gemspec
Successfully built RubyGem
Name: shelf
Version: 0.1.0
File: shelf-0.1.0.gem
$ gem specification shelf-0.1.0.gem rdoc_options
---
- "--main"
- README.md
- "--title"
- Shelf 0.1.0
$ gem specification shelf-0.1.0.gem extra_rdoc_files
---
- CHANGELOG.md
- README.md
What gem install does with documentation:
-
RubyGems 4 installs
ridata by default (the defaults printed bygem help installinclude--document=ri); HTML is generated only on request.gem install shelf --document=rdoc,ribuilds both,-N/--no-documentbuilds none, and the setting can be made permanent in~/.gemrc(gem: --no-documentis common on CI images, which is whyrishows nothing for gems there). -
gem rdoc GEMNAME(or--all) builds the documentation of an installed gem after the fact; its defaults are--ri --no-overwrite, and--rdocadds the HTML. -
gem server, which used to serve the RDoc HTML of installed gems, is a separate plugin gem (rubygems-server) since RubyGems 4;gem help commandsmarks it "requires rubygems-server". YARD’syard server --gemsfills that role for YARD-documented gems.
YARD
YARD ("Yay! A Ruby Documentation Tool") is a gem that parses the same comments as RDoc and adds a tag syntax for structured information.
It is not part of Ruby and not installed by default: add it to the development group of the Gemfile (gem "yard", require: false) or install it with gem install yard, and it provides the yard command (plus the legacy aliases yardoc for yard doc and yri for yard ri).
The current release is 0.9.45; 0.9.39 added Ruby 4.x syntax support, and 0.9.40 added a built-in Markdown renderer, .rbs signature files and the #- block separator.
Comments and tags
A YARD comment has free text first (summary paragraph, then details) and tags below it. A tag starts with @ at the beginning of a comment line. Lines indented by more than one space belong to the tag above them; the first line of @example, @overload, @!macro, @!method and @!attribute has a special meaning and cannot be split.
# frozen_string_literal: true
require "stringio"
# Reads titles, one per line.
class TitleReader
# @param io [#each_line] any object that responds to `each_line`
# (an indented line continues the tag above it)
# @return [Array<String>] the titles without line endings
def titles(io)
io.each_line.map(&:chomp)
end
end
TitleReader.new.titles(StringIO.new("Dune\nEmma\n")) # => ["Dune", "Emma"]
The shelf catalogue with YARD tags. The tags are what separates this from the RDoc version above: each parameter, return value, yielded value and exception is named and typed.
# frozen_string_literal: true
module Shelf
# The catalogue of books, guarded by a mutex.
#
# A catalogue stores books through a store object and exposes them through
# `Enumerable`, so every `Enumerable` method works on it.
#
# @example Build a catalogue and list the titles
# catalog = Shelf::Catalog.new
# catalog.add(book).add(other)
# catalog.map(&:title) # => ["Dune", "Emma"]
#
# @see Shelf::Store::Memory
# @since 0.1.0
class Catalog
include Enumerable
# @param store [#all, #add] any object that responds to `all` and `add`
def initialize(store: Shelf.config.store)
@store = store
@mutex = Thread::Mutex.new
end
# Adds a book to the catalogue.
#
# @param book [Shelf::Book] the book to add
# @return [self] the catalogue, so calls can be chained
# @raise [ArgumentError] if the book is nil
def add(book)
raise ArgumentError, "book must not be nil" if book.nil?
@mutex.synchronize { @store.add(book) }
self
end
# Yields every book in the catalogue.
#
# @yield [book] each book, in insertion order
# @yieldparam book [Shelf::Book] the current book
# @yieldreturn [void]
# @return [Enumerator<Shelf::Book>] when no block is given
# @return [self] when a block is given
def each(&block)
return enum_for(:each) unless block
@mutex.synchronize { @store.all }.each(&block)
self
end
# Looks a book up by title.
#
# @overload find_by_title(title)
# @param title [String] the exact title
# @return [Shelf::Book, nil] the first match, or nil
# @overload find_by_title(title, ignore_case:)
# @param title [String] the title to match
# @param ignore_case [Boolean] compare without regard to case
# @return [Shelf::Book, nil] the first match, or nil
def find_by_title(title, ignore_case: false)
find do |book|
ignore_case ? book.title.casecmp?(title) : book.title == title
end
end
# Counts books by author.
#
# @param options [Hash] reporting options
# @option options [Integer] :limit (10) the maximum number of authors
# @option options [Boolean] :sort (true) sort by count, highest first
# @return [Hash{String => Integer}] author name to number of books
# @deprecated Use {#group_by} with `author.name` instead.
def counts(options = {})
limit = options.fetch(:limit, 10)
map { |book| book.author.name }.tally.first(limit).to_h
end
# @api private
# @todo Replace with a lock-free read.
def snapshot
@mutex.synchronize { @store.all }
end
end
end
The common tags:
| Tag | Meaning |
|---|---|
|
One parameter; keyword parameters too. |
|
One key of an options hash. For keyword parameters use |
|
The return value; repeat the tag for distinct cases (each starting with "if"). |
|
What the method yields to its block and what it expects back. |
|
An exception the method may raise. |
|
Several call forms, each with its own tags. |
|
A code example (the first line is the title). |
|
A related object or URL; it is linked automatically, so do not wrap it in |
|
Notes, to-dos, deprecation with the migration path, version information, authorship. |
|
Mark an object abstract; declare the API surface ( |
|
Deprecated in favour of |
Types
The bracketed list of a tag follows conventions rather than a grammar.
| Written as | Means |
|---|---|
|
A class or module. |
|
Alternatives; |
|
A parameterized collection (also |
|
A hash with key and value types; several keys may share a value type, groups are separated by |
|
An ordered tuple of exactly these types. |
|
A duck type: any object that responds to the named methods. |
|
The return value is not meaningful; YARD prints a note so the reader knows not to use it. |
The examples in the official YARD documentation still use Fixnum; that class was removed in Ruby 3.2, so write Integer.
Source: YARD: Types
Reference tags, linking and directives
-
Reference tags: a docstring or a tag that starts with
(see OTHER)copies the tags fromOTHER.# (see #add)copies the whole docstring;@param (see #add)copies only the@paramtags. -
Links:
{Shelf::Catalog#add}and{#add}link to code objects (the text can be given after a space),{https://example.com Example}links a URL,{file:docs/GUIDE.md Guide}links an extra file,{include:Shelf::Catalog}embeds another docstring, and{render:Shelf::Catalog#add}renders an object in place (useful in README guides). To write a literal hash in a comment, prefix it with!. -
Directives begin with
@!and may create or change objects:@!method,@!attribute [r|w|rw],@!macro,@!parse,@!groupand@!endgroup,@!scope,@!visibility. -
Macros are reusable templates with
$0(the call),$1,$2, … (arguments) and${N-M}(ranges).@!macro [attach] namemakes the macro expand automatically on every later call of that DSL method. -
Separating a header: a trailing
#-line ends a comment block, so a copyright header does not become the docstring of the first class. Only#-works;# -with a space is an ordinary line. This needs RuboCop’sAllowYARDCommentBlockSeparator: true(shown below).
# frozen_string_literal: true
# Copyright (c) Example Author
# Support code shared by the catalogue.
#-
# Looks books up by author.
class AuthorIndex
# @return [Hash{String => Array<String>}] titles by author name
attr_reader :titles
def initialize
@titles = Hash.new { |hash, name| hash[name] = [] }
end
end
Source: YARD documentation: What’s New
Documenting generated methods with YARD
The macro below documents every predicate that the DSL state_predicate defines, once. YARD sees each call of state_predicate :lent and expands the attached macro ($1 is the first argument), creating the documented method lent?. This is the YARD answer to the RDoc :method: directive above, and it needs no repetition per method. The @!group and @!endgroup directives organize the following methods under a heading in the HTML.
# frozen_string_literal: true
module Shelf
# A loan whose state predicates are generated by a small DSL.
class Loan
# @return [Symbol] the current state
attr_reader :state
# @param state [Symbol] the initial state
def initialize(state)
@state = state
end
# Defines a predicate method for one state.
#
# @!macro [attach] state_predicate
# @!method $1?
# Returns true when the loan is in the `$1` state.
# @return [Boolean]
# @param name [Symbol] the state name
# @return [Symbol] the name of the generated method
def self.state_predicate(name)
define_method(:"#{name}?") { state == name }
end
state_predicate :lent
state_predicate :overdue
# @!group Queries
# @return [Boolean] whether the book is out
def out?
lent? || overdue?
end
# @!endgroup
end
end
loan = Shelf::Loan.new(:lent)
loan.lent? # => true
loan.out? # => true
Rules from the guide: YARD takes the method name from the first argument of the DSL call, so @!method is needed only when that is not the name; @!method can carry a signature (@!method foo(name, opts = {})), and @!scope class and @!visibility private change the kind of the generated method. A docstring is required for the generated method to appear, which is what the attached macro supplies. Tests for the generated predicates belong in the normal suite (Testing with Minitest): documentation says what exists, tests prove it works.
Running YARD
| Command | Purpose |
|---|---|
|
Generates HTML into |
|
Serves the project’s docs at |
|
Documentation statistics and a list of undocumented objects (the YARD equivalent of |
|
Lists objects that match a query on tags ( |
|
|
|
A class diagram in Graphviz format (needs |
|
Compare the objects of two gem versions or |
The options are collected in a .yardopts file at the project root (whitespace-separated, one or more per line), which yard doc reads on every run:
--markup markdown
--no-private
--protected
--output-dir doc
--readme README.md
--title "Shelf 0.1.0"
lib/**/*.rb
-
CHANGELOG.md
LICENSE.txt
With --no-private, objects tagged @private are dropped; by default YARD documents only public methods and adds --protected or --private on request. The .yardoc cache is a build artifact and belongs in .gitignore.
YARD ships a built-in renderer that reads both RDoc and Markdown markup without extra gems (the default for both rdoc and markdown markup types since 0.9.40); selecting -M redcarpet, -M kramdown or -M commonmarker uses an installed provider gem when stricter Markdown or extensions are needed.
Extra files (the README, guides) may start with # @markup markdown and # @title Guide lines (in an HTML comment if the file is shown on GitHub).
The Rake integration is YARD::Rake::YardocTask:
# frozen_string_literal: true
require "yard"
YARD::Rake::YardocTask.new(:yard) do |task|
task.files = ["lib/**/*.rb"]
task.options = ["--no-private"]
task.stats_options = ["--list-undoc"]
end
Source: YARD documentation: README
YARD is extended with plugins (any gem named yard-* or yard_*, loaded with --plugin NAME), custom tags (--tag rest_url:"REST URL", --hide-tag), custom handlers for DSLs and its own templates. It also reads RBS signature files (0.9.40), so sig/shelf.rbs can supply the types of methods whose comments have no @param tags.
https://rubydoc.info hosts the generated documentation of every gem pushed to RubyGems.org and of GitHub repositories, built with YARD from the gem sources, so a gem gets a documentation site without publishing one. It is the successor of the older rdoc.info name.
What RDoc makes of YARD tags
RDoc does not know the @tag syntax. Running rdoc over the YARD-documented catalogue and reading the result with ri shows the tags as text, with consecutive tag lines run together into one paragraph:
# Shelf::Catalog#add
(from rid)
### Implementation from Catalog
---
add(book)
---
Adds a book to the catalogue.
@param book [Shelf::Book] the book to add @return [self] the catalogue,
so calls can be chained @raise [ArgumentError] if the book is nil
Source: RDoc documentation: RDoc::Markup
The result is readable but not pretty, so a gem documented with YARD tags is normally also built with YARD (or the tags are kept short). The reverse is not a problem: YARD reads RDoc markup.
YARD compared with RDoc
| Question | RDoc | YARD |
|---|---|---|
Ships with Ruby |
Yes (bundled gem, |
No; add to the development dependencies |
Structured metadata ( |
No; free text, or the |
Yes, with a type syntax |
Reads the other’s comments |
Prints unknown |
Yes, RDoc markup and Markdown |
Markdown |
Supported ( |
Supported by default (built-in renderer) |
Documenting generated methods |
|
|
Terminal viewer |
|
|
Coverage |
|
|
Hosted for gems |
docs.ruby-lang.org (Ruby itself) |
rubydoc.info (all published gems) |
Extension |
Markup extension API; themes |
Plugins, handlers, tags, templates |
Guidance. Use RDoc alone for small libraries and scripts, for anything that should be readable through ri with no setup, and when the project follows the Ruby core style. Add YARD when the API is public, typed information in the documentation is valuable, the gem will be read on rubydoc.info, or the code has a DSL that needs macros. Pick one markup per project and one set of rules for what is public.
RBS inline comments
Types can also live in comments. RBS (the Ruby signature language, see Type Checking with RBS, TypeProf, Steep and Sorbet) normally sits in separate .rbs files (sig/shelf.rbs); RBS 4.0 (2026-03-16) added an experimental inline syntax, @rbs comments and #: annotations, so a method’s type is declared next to its documentation. The rbs-inline gem (a prototype whose features are being merged into the rbs gem) generates .rbs files from comments with a similar syntax. Ruby 4.0.7 bundles RBS 3.10.0, which does not have the inline syntax: add gem "rbs", "~> 4.0" explicitly.
# frozen_string_literal: true
# rbs_inline: enabled
module Shelf
# The catalogue of books.
class Catalog
# @rbs @store: untyped -- the object that holds the books
# @rbs @mutex: Thread::Mutex
include Enumerable #[Shelf::Book]
attr_reader :name #: String
# Creates an empty catalogue.
#
# @rbs name: String -- a label for the catalogue
# @rbs store: untyped -- any object with `all` and `add`
# @rbs return: void
def initialize(name: "main", store: Shelf.config.store)
@name = name
@store = store
@mutex = Thread::Mutex.new
end
# Adds a book and returns the catalogue.
#
#: (Shelf::Book) -> self
def add(book)
@mutex.synchronize { @store.add(book) }
self
end
# Yields each book, or returns an enumerator.
#
# @rbs () { (Shelf::Book) -> void } -> self
# | () -> Enumerator[Shelf::Book, self]
def each(&block)
return enum_for(:each) unless block
@mutex.synchronize { @store.all }.each(&block)
self
end
end
end
Source: RBS: Inline RBS Type Declaration
The forms:
-
# @rbs name: Type -- descriptiontypes one parameter and# @rbs return: Typethe result (with*args: Tand**opts: Tfor splats and&block: () -> voidfor the block);--starts a description that doubles as documentation. -
# @rbs (Type) -> Typeor the short#: (Type) -> Typegives the whole method type (several#:lines or|give overloads); a trailing#: Typeafter anattr_readeror adefline types it. -
# @rbs @ivar: Typedeclares an instance variable, and must come beforeincludeor other statements of the class body (it was reported as unused after anincludeline in the check run for this page). -
include Enumerable #[Shelf::Book]gives the type arguments of a mixin;# @rbs skipignores the next definition;#: class-aliasand#: module-aliastype constant aliases. -
Limits in 4.x: no generic class or module definitions, no method visibility, no
class << self, no top-level methods.
The inline parser of rbs 4.2.0 read the listing above with zero diagnostics:
diagnostics: 0
Source: RBS: Inline RBS Type Declaration
The trailing annotations break one RuboCop default, Layout/LeadingCommentSpace, which wants a space after #; its AllowRBSInlineAnnotation and AllowYARDCommentBlockSeparator options exist for these comments (they are the two options added to the configuration used for this page):
Layout/LeadingCommentSpace:
AllowRBSInlineAnnotation: true
AllowYARDCommentBlockSeparator: true
Relations to the documentation tools. @rbs comments type the code but are not a replacement for descriptions: the text before them is still the RDoc or YARD docstring. RDoc 7.0.4 does not understand them: it prints the @rbs and #: lines as plain text inside the method documentation (shown below), so a project that uses inline RBS and RDoc together should keep the type lines out of the documentation comment or generate the .rbs files and use sig/. RDoc 7.0.4 parses .rbs files and YARD 0.9.40 and later read them, so types written in sig/ can reach the generated documentation. Choose one source of truth for types per project (YARD @param types, RBS files or RBS inline comments) because duplicated types drift apart.
For example, ri shows the @rbs lines of the each method above as ordinary text of its documentation:
# Shelf::Catalog#each
(from rid)
### Implementation from Catalog
---
each(&block)
---
Yields each book, or returns an enumerator.
@rbs () { (Shelf::Book) -> void } -> self
| () -> Enumerator[Shelf::Book, self]
Source: RDoc documentation: RDoc::Markup
Writing documentation that lasts
-
Document the contract, not the code. What the method promises, what it accepts, what it raises, what it yields. The code already says how.
-
One summary sentence, then detail. The index pages show only the first sentence.
-
Name the parameters.
+state+in an RDoc comment or@param statein YARD;rdoc -C1andyard stats --list-undoccheck it. -
Give an example that runs. Indent it in RDoc, use
@examplein YARD, and write the result as a comment (catalog.map(&:title) # => ["Dune", "Emma"]). Keep examples in the repository’s tests too, so they cannot rot. -
Document the errors. The classes raised and the conditions, as the shelf comments do for
ArgumentError. -
Mark deprecations and versions.
@deprecated,@since; in RDoc plain prose such as "Deprecated since 0.3, use …". -
Keep generated and private code out of the way.
:nodoc:(RDoc) or@private/@api private(YARD) on implementation helpers;--visibilityand--no-privatefor the output. -
Gate coverage in CI.
rdoc -C(orrake rdoc:coverage) andyard stats --list-undocfail loudly when a public method has no comment; RuboCop’sStyle/Documentation(classes and modules) is enabled by default, andStyle/DocumentationMethodfor public methods is available but off. -
Treat the README as documentation. Pass it with
--main README.md; reference it from the gemspec’sextra_rdoc_files.
Pitfalls
| Situation | Problem | Do this instead |
|---|---|---|
A copyright header above a class without its own comment |
The header becomes the documentation of the class; blank lines do not detach it. |
Wrap the header in |
|
The locale is |
|
|
RDoc is a bundled gem since Ruby 4.0, not a default gem. |
Add |
Two classes are nested wrongly in the output |
The 7.0.4 Ruby parser mis-reads endless methods. |
Use regular |
A value object built with |
|
Subclass instead ( |
Backticks or |
Supported only by newer RDoc than 7.0.4. |
Use |
|
It was followed directly by a real definition, or (for the singleton-method and attribute forms) it was written with a single |
Put a |
|
Deprecated; prints a warning on every run. |
|
Trailing |
|
Disable the cop on that line, or avoid |
|
The gem was installed without documentation ( |
|
|
Mixing them: YARD warns when a |
|
Types in YARD tags and RBS files that disagree |
Two sources of truth. |
Choose one, and generate or check the other. |
Where older material is outdated
Documentation guidance written for Ruby 1.9 to 2.x, and some blog posts, differ from Ruby 4.0 in these points.
| Older claim | Why it is misleading | Ruby 4.0 |
|---|---|---|
"RDoc comes with Ruby" (as part of the default libraries) |
It was a default gem, loadable without a Gemfile entry. |
Bundled gem since 4.0 ( |
"The default RDoc theme is Darkfish" |
True in the RDoc of Ruby 3.2 to 3.4 (6.5 to 6.14). |
Aliki in RDoc 7; Darkfish is deprecated. |
|
Old guides use them to configure the output. |
Deprecated and warning in 7.0.4; use the options. |
Type examples with |
These classes no longer exist (removed in 3.2); the official YARD tag examples still show |
|
|
They are aliases of |
|
rdoc.info |
The old name of the hosted YARD service. |
|
"Installing a gem generates its HTML documentation" |
Guides written for older RubyGems say so. |
The defaults of RubyGems 4 ( |
|
It was part of RubyGems. |
A separate gem ( |
Version notes
| Version | Change |
|---|---|
Ruby 3.2 |
RDoc 6.5 is the default gem (Darkfish). |
Ruby 3.3 |
RDoc 6.6. |
Ruby 3.4 |
RDoc 6.14 (Darkfish); it contains the experimental Prism-based parser selectable with |
Ruby 4.0 |
RDoc 7.0.4, a bundled gem; the Aliki theme is the default and Darkfish is deprecated; |
RDoc releases after 7.0.4 |
The online reference (8.1.0 at the time of writing) documents backtick monofont, strike-through and a live-reload server task, none of which 7.0.4 has; check |
YARD 0.9.39 / 0.9.40 |
Ruby 4.x syntax support / built-in Markdown renderer, |
RBS 4.0 |
Experimental inline syntax ( |
References
-
RDoc documentation: home (current release), RDoc Markup Reference, Markdown Markup Reference and Configuring RDoc
-
RDoc::Options, RDoc::Task, RDoc::Markup, RDoc::Parser::Ruby and ri
-
Ruby 4.0 documentation, Comments (magic comments) and Ruby 4.0 NEWS
-
RDoc repository (including
lib/rdoc/markup/pre_process.rb, the list of directives) -
RBS: Inline RBS Type Declaration and rbs-inline (community prototype)
-
RuboCop Style cops (community):
Style/Documentation,Style/DocumentationMethod,Style/CommentedKeyword -
YARD (community), Getting Started Guide, Tags Overview, Types, What’s New and rubydoc.info
-
Carleton DiLeo, Clean Ruby: A Guide to Crafting Better Code for Rubyists, 2nd ed., Apress, 2026 — publisher page on SpringerLink (consulted for concepts only)