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 (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), ri data, translation templates (pot) and a coverage report. Bundled gem since Ruby 4.0 (a default gem before).

ri

Reads the ri data that RDoc wrote and prints documentation in the terminal: ri Array#map, ri File, ri ruby:syntax/literals. It works offline.

YARD (yard, yardoc, yri)

Community alternative. Understands RDoc markup and Markdown, adds @tag metadata with types, macros and directives, serves docs locally (yard server) and is what https://rubydoc.info builds for published gems.

RBS inline comments

@rbs and #: comments that carry type signatures. Experimental inline syntax of RBS 4.0 (and the older rbs-inline gem). They document types, not behaviour, and complement the other tools.

RuboCop documentation cops

Style/Documentation (classes and modules), Style/DocumentationMethod (public methods, off by default) and Style/CommentedKeyword check that documentation exists and is placed where the tools can read it.

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.

flowchart LR src["Comments in .rb files, README.md and guide pages"] --> parse["rdoc parses code and comments"] parse --> store["Documentation store"] store --> aliki["aliki: HTML, default in RDoc 7"] store --> darkfish["darkfish: HTML, deprecated"] store --> ri["ri: data for the ri command"] store --> pot["pot: translation templates"] store --> cov["rdoc -C: coverage report"] src --> yard["yard doc, community"] yard --> yardout["YARD HTML and the .yardoc database"] yardout --> rubydoc["rubydoc.info for published gems"]

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 and rdoc -C1 can 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: true magic 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

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.

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, include, extend and prepend targets

The purpose of the type, its invariants, an example of use.

Method name and parameter list, including keyword parameters and defaults (state: :available)

What each parameter means (name it as +state+), valid values, what happens when it is invalid.

The arguments passed to yield (so yield book is shown as { |book| ... })

What the block receives and what it must return; override with :yields:.

Attribute names and access (attr_reader is [R])

What the attribute holds and its type.

Constants and their names

What the constant is for.

Aliases (alias, alias_method)

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 = signs followed by the text: = Title, == Section. Avoid levels deeper than three. Level 1 is the title of the page, so inside a class comment start at ==.

Bullet list

- or * followed by a space. Continuation lines are indented to align with the text.

Numbered list

1., 2. and so on. The numbers are renumbered automatically, so 1. on every item is fine.

Lettered list

a., b. and so on, rendered with lower-case letters.

Labelled list

[label] text (square brackets, label and text may be on one line) or label:: text, a word followed by two colons. The second form renders as a "note 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

>>> on its own line followed by indented text; nested blocks are allowed.

Horizontal rule

A line of three or more hyphens and nothing else.

Directive

A line of the form :name: argument, described in the next section.

Text markup

Written as Result Notes

*bold* or <b>bold</b>

bold

The shorthand works for a single word; use the HTML tag for a phrase.

_italic_, <i>text</i> or <em>text</em>

italic

Single word shorthand, or tags for phrases.

+monofont+, <tt>text</tt> or <code>text</code>

monofont

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 <tt> for anything with spaces or other punctuation. Do not rely on styling inside <tt>: in 7.0.4 <tt>a_b *c*</tt> produced malformed nested HTML.

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 rdoc --version before using them.

<del>text</del>, <s>text</s>

strike-through (newer RDoc only)

In 7.0.4 <del> is escaped and shown as literal text.

\Name

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.

(c), --, ---, ..., straight quotes

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

RDoc turns many strings into links without any markup, and offers explicit forms for the rest.

Written as Links to

Shelf::Catalog, Shelf::Catalog#add, Shelf::Catalog::new, #add

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.

https://example.com, mailto:a@example.com

The URL itself. A fully qualified image URL (.png, .gif, .jpg, .jpeg, .bmp) becomes an inline image.

Example[https://example.com]

One-word link text.

{the guide}[https://example.com/guide]

Multi-word link text.

{text}[rdoc-ref:Shelf::Catalog]

A documented object or file by name (class, module, method, constant, a page such as README.md). If the target does not exist, RDoc 7.0.4 prints rdoc-ref:... can't be resolved and the link is dropped.

{text}[rdoc-label:name], {text}[rdoc-label:name:back]

A label (an anchor on the page), optionally with a "return" label to link back.

link:README_md.html

A file in the generated output, relative to the output root.

{rdoc-image:path}[target]

An image that is also a link.

Class@Heading, +Class@Heading+with+spaces+

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

:nodoc:

Trailing on a definition line: do not document this object. :nodoc: all also hides the documentation of nested classes, modules and methods. Use it for implementation helpers that must be public in Ruby but are not API. RuboCop’s Style/CommentedKeyword allows this trailing form.

:doc:

Trailing on a definition line: document this object even though RDoc would skip it (a private method, say). RuboCop’s Style/CommentedKeyword flags it on a def line.

:stopdoc: and :startdoc:

On a line by themselves: skip everything between them. :enddoc: stops documenting for the rest of the file. For Ruby files the shorthand is #-- to stop and #++ to resume.

:notnew:

Trailing on def initialize: do not show the synthesized new class method; show initialize instead. (:not_new: and :not-new: are accepted spellings.) RuboCop flags the trailing form with Style/CommentedKeyword, which allows :nodoc:, :yields:, RuboCop directives and RBS-inline annotations only.

:call-seq:

Replaces the generated signature with one or more lines you write, including the return type (each { |book| ... } -> catalog). RDoc cannot infer return types or overloads; this is the way to document them.

:args: (:arg:)

Overrides the displayed argument list, trailing on the def line: def go(*args) # :args: thing, tries.

:yields: (:yield:)

Overrides the displayed block parameters, trailing on the def line or in the comment: :yields: book shows { |book| ... }. RuboCop allows it as a trailing comment.

:section: Title

Starts a named section; the methods that follow are grouped under it until the next :section:. A bare :section: returns to the default section. The comment must be separated from the next item by a blank line, and lines of text in it become the section’s description.

:category: Title

Puts the next method only into the named section, then returns to the current one.

:include: path

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 -i/--include (rdoc_include).

:markup: format

Sets the markup for this comment (rdoc, markdown, rd or tomdoc); at the top of a file it applies to the file.

:method: name, :singleton-method: name

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.

:attr: name, :attr_reader:, :attr_writer:, :attr_accessor:

Declares an attribute that the parser cannot see.

:main:, :title:

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 (--main, --title, main_page:, title:, rdoc.main, rdoc.title).

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

: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 constant LIST keeps 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 real attr_accessor creates 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 (or markup: markdown in .rdoc_options, or rdoc.markup = "markdown" in RDoc::Task);

  • per file or per comment: the :markup: markdown directive;

  • implicitly, for files that are Markdown: README.md and other .md files 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

-o, --output DIR

Output directory (default doc). RDoc refuses to overwrite a directory that it did not create unless -O is used.

-m, --main NAME

The file, class or module shown on the first page, usually README.md.

-t, --title TITLE

The title of the documentation set.

-x, --exclude PATTERN

Skip matching files (repeatable). A default exclude list (backup files, gemspecs) applies unless --no-apply-default-exclude; test and spec files are skipped unless --no-skipping-tests.

--visibility LEVEL, -a

Minimum visibility to document: public, protected (the default), private or nodoc. -a/--all is the same as --visibility=private.

-f, --format NAME

aliki, darkfish, ri or pot.

--markup FORMAT

rdoc (default), markdown, rd or tomdoc.

-C[LEVEL], --coverage-report

Print what is undocumented and exit without generating files. -C1 also reports parameters that the comment never mentions.

-e, --encoding ENC

Output encoding. All input is converted to it (default UTF-8).

-i, --include DIRS

Directories searched by :include:.

-H, --show-hash

Keep the leading # of instance method names in links.

-N, --line-numbers

Show line numbers in source listings.

--embed-mixins

Show methods of included modules in the including class.

--copy-files PATH

Copy static files (images used by the pages) to the output.

--root DIR, --page-dir DIR

Root of the source tree, and the directory of guide pages that are not classes.

-U, --force-update / --dry-run

Reparse everything even if nothing is newer than the flag file / write no files.

-p, --pipe

Convert RDoc text on standard input to HTML (used above).

--write-options

Write the current options to .rdoc_options and exit.

-q, -V, -D

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 with error generating index.html: "\xF0" on US-ASCII (Encoding::InvalidByteSequenceError). Setting LANG=en_US.UTF-8 (or another UTF-8 locale) fixes it; passing -e UTF-8 did not in 7.0.4, whereas RUBYOPT=-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

-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_options is 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).

  • .document is 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 .document in a subdirectory narrows what is documented there.

  • RDOCOPT is 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::Task is 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

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

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 as label(= "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.new and Data.define. Author = Data.define(:name) do ... end is 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 for Struct.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

ri Array, ri File::Stat

A class or module. A prefix that is unique is enough (ri Arr shows Array); an ambiguous one such as ri Fil prints "Nothing known about Fil" and a "Did you mean?" suggestion.

ri Array#map, ri File.read, ri IO::readlines

An instance method (#), a class method (::), or both (.). A bare ri readlines looks the method up in every class.

ri ruby:

The list of pages of the Ruby core documentation; ri ruby:syntax/literals shows one.

ri GEMNAME: and ri GEMNAME:README

The pages of an installed gem, when it was installed with ri documentation.

ri -i

Interactive mode with completion: type names until a blank line.

ri --list, -a/--all

All classes and modules; all the documentation for a class including its methods.

ri -f markdown, ansi, bs, rdoc

The output format (the default is bs when paged, ansi otherwise); -T disables the pager.

ri --server[=PORT]

Serves the documentation over HTTP (default port 8214).

ri -d DIR, --no-standard-docs, --list-doc-dirs

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.

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

spec.files

Must list the sources that carry the comments and the README; documentation generated on the user’s machine reads these installed files.

spec.extra_rdoc_files

Pages added to the documentation: README.md, CHANGELOG.md, guides.

spec.rdoc_options

Arguments passed to rdoc when gem rdoc or gem install --document builds the docs: --main README.md, --title, --markup markdown, --exclude.

spec.metadata["documentation_uri"]

The link RubyGems.org shows as "Documentation". Related keys: source_code_uri, changelog_uri, bug_tracker_uri.

# 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 ri data by default (the defaults printed by gem help install include --document=ri); HTML is generated only on request. gem install shelf --document=rdoc,ri builds both, -N/--no-document builds none, and the setting can be made permanent in ~/.gemrc (gem: --no-document is common on CI images, which is why ri shows 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 --rdoc adds 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 commands marks it "requires rubygems-server". YARD’s yard server --gems fills 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

@param name [Types] description

One parameter; keyword parameters too. @param (see #other) copies the tags of another method.

@option name [Types] :key (default) description

One key of an options hash. For keyword parameters use @param, not @option.

@return [Types] description

The return value; repeat the tag for distinct cases (each starting with "if").

@yield [params] description, @yieldparam name [Types] description, @yieldreturn [Types] description

What the method yields to its block and what it expects back.

@raise [ExceptionClass] description

An exception the method may raise.

@overload signature with an indented docstring

Several call forms, each with its own tags.

@example Optional title with indented code

A code example (the first line is the title).

@see name description

A related object or URL; it is linked automatically, so do not wrap it in {}.

@note, @todo, @deprecated description, @since version, @version, @author

Notes, to-dos, deprecation with the migration path, version information, authorship.

@abstract, @api private, @private

Mark an object abstract; declare the API surface (@api private prints a notice, and yardoc --query can select by API); declare logical privacy that Ruby visibility cannot express (used with --no-private).

@attr, @attr_reader, @attr_writer

Deprecated in favour of @!attribute.

Types

The bracketed list of a tag follows conventions rather than a grammar.

Written as Means

String, Shelf::Book, Boolean

A class or module. Boolean stands for true or false and does not exist in Ruby.

String, nil

Alternatives; nil, true, false, :symbol, "literal" and self are accepted.

Array<String>

A parameterized collection (also Enumerator<Shelf::Book>).

Hash{String => Integer}

A hash with key and value types; several keys may share a value type, groups are separated by ;.

Array(String, Integer)

An ordered tuple of exactly these types.

#read, #all, #add

A duck type: any object that responds to the named methods.

void

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 from OTHER. # (see #add) copies the whole docstring; @param (see #add) copies only the @param tags.

  • 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, @!group and @!endgroup, @!scope, @!visibility.

  • Macros are reusable templates with $0 (the call), $1, $2, …​ (arguments) and ${N-M} (ranges). @!macro [attach] name makes 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’s AllowYARDCommentBlockSeparator: 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

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

yard doc (alias yardoc)

Generates HTML into doc/ and a .yardoc cache. Without arguments it documents lib/**/*.rb and any README* file. Extra files are listed after a - separator: yard doc 'lib/**/*.rb' - README.md CHANGELOG.md LICENSE.txt.

yard server and yard server --reload

Serves the project’s docs at http://localhost:8808, regenerating on each request with --reload; --gems serves all installed gems.

yard stats --list-undoc

Documentation statistics and a list of undocumented objects (the YARD equivalent of rdoc -C).

yard list --query '@todo'

Lists objects that match a query on tags (--query '@api.text == "public"').

yard ri (alias yri)

ri-style lookup of the project’s documentation in the terminal, from the .yardoc cache.

yard graph

A class diagram in Graphviz format (needs dot).

yard diff, yard gems, yard config

Compare the objects of two gem versions or .yardoc files, build the index of installed gems, view or edit ~/.yard/config.

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

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

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, ri included)

No; add to the development dependencies

Structured metadata (@param, @return, types)

No; free text, or the :call-seq: directive

Yes, with a type syntax

Reads the other’s comments

Prints unknown @tags as text

Yes, RDoc markup and Markdown

Markdown

Supported (--markup markdown)

Supported by default (built-in renderer)

Documenting generated methods

:method:, :attr:, :singleton-method:

@!method, @!attribute, @!macro [attach]

Terminal viewer

ri

yri

Coverage

rdoc -C, -C1

yard stats --list-undoc

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

The forms:

  • # @rbs name: Type -- description types one parameter and # @rbs return: Type the result (with *args: T and **opts: T for splats and &block: () -> void for the block); -- starts a description that doubles as documentation.

  • # @rbs (Type) -> Type or the short #: (Type) -> Type gives the whole method type (several #: lines or | give overloads); a trailing #: Type after an attr_reader or a def line types it.

  • # @rbs @ivar: Type declares an instance variable, and must come before include or other statements of the class body (it was reported as unused after an include line in the check run for this page).

  • include Enumerable #[Shelf::Book] gives the type arguments of a mixin; # @rbs skip ignores the next definition; #: class-alias and #: module-alias type 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

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]

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 state in YARD; rdoc -C1 and yard stats --list-undoc check it.

  • Give an example that runs. Indent it in RDoc, use @example in 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; --visibility and --no-private for the output.

  • Gate coverage in CI. rdoc -C (or rake rdoc:coverage) and yard stats --list-undoc fail loudly when a public method has no comment; RuboCop’s Style/Documentation (classes and modules) is enabled by default, and Style/DocumentationMethod for public methods is available but off.

  • Treat the README as documentation. Pass it with --main README.md; reference it from the gemspec’s extra_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 #-- and #++ (RDoc), end it with #- (YARD), or give the class a comment of its own.

rdoc fails with Encoding::InvalidByteSequenceError in CI

The locale is C, and Aliki’s pages contain non-ASCII characters.

LANG=en_US.UTF-8, or RUBYOPT=-Eutf-8.

require "rdoc/task" fails under bundle exec

RDoc is a bundled gem since Ruby 4.0, not a default gem.

Add gem "rdoc" to the Gemfile (development group).

Two classes are nested wrongly in the output

The 7.0.4 Ruby parser mis-reads endless methods.

Use regular def ... end in documented files, or run with RDOC_USE_PRISM_PARSER=1 and check.

A value object built with Data.define has no methods in the docs

Name = Data.define ... do ... end is documented as a constant; the block is not parsed.

Subclass instead (class Name < Data.define(...)) or document the constant.

Backticks or <del> in RDoc markup render as text

Supported only by newer RDoc than 7.0.4.

Use +code+ and <tt>, and avoid strike-through.

:method: does nothing

It was followed directly by a real definition, or (for the singleton-method and attribute forms) it was written with a single #.

Put a ## comment with a blank line after it; one directive per method.

# :main: or # :title: in a source file

Deprecated; prints a warning on every run.

--main/--title, main_page:/title: in .rdoc_options, or RDoc::Task attributes.

Trailing # :notnew: gets a RuboCop offense

Style/CommentedKeyword allows :nodoc:, :yields:, RuboCop directives and RBS-inline annotations.

Disable the cop on that line, or avoid :notnew:.

ri Foo finds nothing for an installed gem

The gem was installed without documentation (gem: --no-document).

gem rdoc GEMNAME --ri, or install with --document=ri.

@param for a keyword argument, @option for a hash

Mixing them: YARD warns when a @param name is not in the signature.

@param for positional and keyword parameters; @option for keys of an options hash only.

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 (rdoc 7.0.4): it is installed, but Bundler needs gem "rdoc" to load it.

"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.

:main: and :title: in source files

Old guides use them to configure the output.

Deprecated and warning in 7.0.4; use the options.

Type examples with Fixnum and Bignum

These classes no longer exist (removed in 3.2); the official YARD tag examples still show Fixnum.

Integer.

yardoc and yri as separate programs

They are aliases of yard doc and yard ri.

yard doc, yard ri, yard server, yard stats.

rdoc.info

The old name of the hosted YARD service.

https://rubydoc.info

"Installing a gem generates its HTML documentation"

Guides written for older RubyGems say so.

The defaults of RubyGems 4 (gem help install) build ri data only; HTML on request (--document=rdoc,ri).

gem server for local gem documentation

It was part of RubyGems.

A separate gem (rubygems-server) in RubyGems 4.

Version notes

Version Change

Ruby 3.2

RDoc 6.5 is the default gem (Darkfish). Fixnum/Bignum are gone, so type lists in tags must name Integer.

Ruby 3.3

RDoc 6.6.

Ruby 3.4

RDoc 6.14 (Darkfish); it contains the experimental Prism-based parser selectable with RDOC_USE_PRISM_PARSER.

Ruby 4.0

RDoc 7.0.4, a bundled gem; the Aliki theme is the default and Darkfish is deprecated; :main: and :title: print deprecation warnings. IRB, RBS (3.10.0 bundled) and Rake are bundled gems too.

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 rdoc --version before using what the reference describes.

YARD 0.9.39 / 0.9.40

Ruby 4.x syntax support / built-in Markdown renderer, .rbs support and the #- separator.

RBS 4.0

Experimental inline syntax (@rbs, #:).

References