Project

docscribe

0.0
No release in over 3 years
Autogenerate documentation for Ruby code with YARD syntax.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 3.3
~> 1.8
 Project Readme

Docscribe

Gem Version RubyGems Downloads CI License Ruby

Generate inline, YARD-style documentation comments for Ruby methods by analyzing your code's AST.

Docscribe inserts doc headers before method definitions, infers parameter and return types (including rescue-aware returns), and respects Ruby visibility semantics — without using YARD to parse.

  • No AST reprinting. Your original code, formatting, and constructs (like class << self, heredocs, %i[]) are preserved.
  • Inline-first. Comments are inserted before method headers without reprinting the AST. For methods with a leading Sorbet sig, new docs are inserted above the first sig.
  • Heuristic type inference for params and return values, including conditional returns in rescue branches.
  • Safe and aggressive update modes:
    • safe mode inserts missing docs, merges existing doc-like blocks, and normalizes sortable tags;
    • aggressive mode rebuilds existing doc blocks.
  • Ruby 3.4+ syntax supported using Prism translation (see "Parser backend" below).
  • Optional external type integrations:
    • RBS via --rbs / --sig-dir;
    • Sorbet via inline sig declarations and RBI files with --sorbet / --rbi-dir.
  • Optional @!attribute generation for:
    • attr_reader / attr_writer / attr_accessor;
    • Struct.new declarations in both constant-assigned and class-based styles.

Common workflows:

  • Inspect what safe doc updates would be applied: docscribe lib
  • Apply safe doc updates: docscribe -a lib
  • Apply aggressive doc updates: docscribe -A lib
  • Use RBS signatures when available: docscribe -a --rbs --sig-dir sig lib
  • Use Sorbet signatures when available: docscribe -a --sorbet --rbi-dir sorbet/rbi lib

Contents

  • Docscribe
    • Contents
    • Installation
    • Quick start
    • CLI
      • Options
      • Examples
    • Update strategies
      • Safe strategy
      • Aggressive strategy
      • Output markers
    • Parser backend (Parser gem vs Prism)
    • External type integrations (optional)
      • RBS
      • Sorbet
      • Inline Sorbet example
      • Sorbet RBI example
      • Sorbet comment placement
      • Generic type formatting
      • Notes and fallback behavior
    • Type inference
    • Rescue-aware returns and @raise
    • Visibility semantics
    • API (library) usage
    • Configuration
      • Filtering
      • attr_* example
      • Struct.new examples
        • Constant-assigned struct
        • Class-based struct
      • Merge behavior
      • Param tag style
      • Create a starter config
    • CI integration
    • Comparison to YARD's parser
    • Limitations
    • Roadmap
    • Contributing
    • License

Installation

Add to your Gemfile:

gem "docscribe"

Then:

bundle install

Or install globally:

gem install docscribe

Requires Ruby 2.7+.

Quick start

Given code:

class Demo
  def foo(a, options: {})
    42
  end

  def bar(verbose: true)
    123
  end

  private

  def self.bump
    :ok
  end

  class << self
    private

    def internal; end
  end
end

Run:

echo "...code above..." | docscribe --stdin

Output:

class Demo
  # +Demo#foo+ -> Integer
  #
  # Method documentation.
  #
  # @param [Object] a Param documentation.
  # @param [Hash] options Param documentation.
  # @return [Integer]
  def foo(a, options: {})
    42
  end

  # +Demo#bar+ -> Integer
  #
  # Method documentation.
  #
  # @param [Boolean] verbose Param documentation.
  # @return [Integer]
  def bar(verbose: true)
    123
  end

  private

  # +Demo.bump+ -> Symbol
  #
  # Method documentation.
  #
  # @return [Symbol]
  def self.bump
    :ok
  end

  class << self
    private

    # +Demo.internal+ -> Object
    #
    # Method documentation.
    #
    # @private
    # @return [Object]
    def internal; end
  end
end

Note

  • The tool inserts doc headers before method headers and preserves everything else.
  • For methods with a leading Sorbet sig, docs are inserted above the first sig.
  • Class methods show with a dot (+Demo.bump+, +Demo.internal+).
  • Methods inside class << self under private are marked @private.

CLI

docscribe [options] [files...]

Docscribe has three main ways to run:

  • Inspect mode (default): checks what safe doc updates would be applied and exits non-zero if files need changes.
  • Safe autocorrect (-a, --autocorrect): writes safe, non-destructive updates in place.
  • Aggressive autocorrect (-A, --autocorrect-all): rewrites existing doc blocks more aggressively.
  • STDIN mode (--stdin): reads Ruby source from STDIN and prints rewritten source to STDOUT.

If you pass no files and don’t use --stdin, Docscribe processes the current directory recursively.

Options

  • -a, --autocorrect
    Apply safe doc updates in place.

  • -A, --autocorrect-all
    Apply aggressive doc updates in place.

  • --stdin
    Read source from STDIN and print rewritten output.

  • --verbose
    Print per-file actions.

  • --explain
    Show detailed reasons for each file that would change.

  • --rbs
    Use RBS signatures for @param/@return when available (falls back to inference).

  • --sig-dir DIR
    Add an RBS signature directory (repeatable). Implies --rbs.

  • --include PATTERN
    Include PATTERN (method id or file path; glob or /regex/).

  • --exclude PATTERN
    Exclude PATTERN (method id or file path; glob or /regex/). Exclude wins.

  • --include-file PATTERN
    Only process files matching PATTERN (glob or /regex/).

  • --exclude-file PATTERN
    Skip files matching PATTERN (glob or /regex/). Exclude wins.

  • -C, --config PATH
    Path to config YAML (default: docscribe.yml).

  • -v, --version
    Print version and exit.

  • -h, --help
    Show help.

Examples

  • Inspect a directory:

    docscribe lib
  • Apply safe updates:

    docscribe -a lib
  • Apply aggressive updates:

    docscribe -A lib
  • Preview output for a single file via STDIN:

    cat path/to/file.rb | docscribe --stdin
  • Use RBS signatures:

    docscribe -a --rbs --sig-dir sig lib
  • Show detailed reasons for files that would change:

    docscribe --verbose --explain lib

Update strategies

Docscribe supports two update strategies: safe and aggressive.

Safe strategy

Used by:

  • default inspect mode: docscribe lib
  • safe write mode: docscribe -a lib

Safe strategy:

  • inserts docs for undocumented methods
  • merges missing tags into existing doc-like blocks
  • normalizes configurable tag order inside sortable tag runs
  • preserves existing prose and comments where possible

This is the recommended day-to-day mode.

Aggressive strategy

Used by:

  • aggressive write mode: docscribe -A lib

Aggressive strategy:

  • rebuilds existing doc blocks
  • replaces existing generated documentation more fully
  • is more invasive than safe mode

Use it when you want to rebaseline or regenerate docs wholesale.

Output markers

In inspect mode, Docscribe prints one character per file:

  • . = file is up to date
  • F = file would change
  • E = file had an error

In write modes:

  • . = file already OK
  • C = file was updated
  • E = file had an error

With --verbose, Docscribe prints per-file statuses instead.

With --explain, Docscribe also prints detailed reasons, such as:

  • missing @param
  • missing @return
  • missing module_function note
  • unsorted tags

Parser backend (Parser gem vs Prism)

Docscribe internally works with parser-gem-compatible AST nodes and Parser::Source::* objects (so it can use Parser::Source::TreeRewriter without changing formatting).

  • On Ruby <= 3.3, Docscribe parses using the parser gem.
  • On Ruby >= 3.4, Docscribe parses using Prism and translates the tree into the parser gem's AST.

You can force a backend with an environment variable:

DOCSCRIBE_PARSER_BACKEND=parser bundle exec docscribe lib
DOCSCRIBE_PARSER_BACKEND=prism  bundle exec docscribe lib

External type integrations (optional)

Docscribe can improve generated @param and @return types by reading external signatures instead of relying only on AST inference.

Important

When external type information is available, Docscribe resolves signatures in this order:

  • inline Sorbet sig declarations in the current Ruby source;
  • Sorbet RBI files;
  • RBS files;
  • AST inference fallback.

If an external signature cannot be loaded or parsed, Docscribe falls back to normal inference instead of failing.

RBS

Docscribe can read method signatures from .rbs files and use them to generate more accurate parameter and return types.

CLI:

docscribe -a --rbs --sig-dir sig lib

You can pass --sig-dir multiple times:

docscribe -a --rbs --sig-dir sig --sig-dir vendor/sigs lib

Config:

rbs:
  enabled: true
  sig_dirs:
    - sig
  collapse_generics: false

Example:

# Ruby source
class Demo
  def foo(verbose:, count:)
    "body says String"
  end
end
# sig/demo.rbs
class Demo
    def foo: (verbose: bool, count: Integer) -> Integer
end

Generated docs will prefer the RBS signature over inferred Ruby types:

class Demo
  # +Demo#foo+ -> Integer
  #
  # Method documentation.
  #
  # @param [Boolean] verbose Param documentation.
  # @param [Integer] count Param documentation.
  # @return [Integer]
  def foo(verbose:, count:)
    'body says String'
  end
end

Sorbet

Docscribe can also read Sorbet signatures from:

  • inline sig declarations in Ruby source
  • RBI files

CLI:

docscribe -a --sorbet lib

With RBI directories:

docscribe -a --sorbet --rbi-dir sorbet/rbi lib

You can pass --rbi-dir multiple times:

docscribe -a --sorbet --rbi-dir sorbet/rbi --rbi-dir rbi lib

Config:

sorbet:
  enabled: true
  rbi_dirs:
    - sorbet/rbi
    - rbi
  collapse_generics: false

Inline Sorbet example

class Demo
  extend T::Sig

  sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
  def foo(verbose:, count:)
    'body says String'
  end
end

Docscribe will use the Sorbet signature instead of the inferred body type:

class Demo
  extend T::Sig

  # +Demo#foo+ -> Integer
  #
  # Method documentation.
  #
  # @param [Boolean] verbose Param documentation.
  # @param [Integer] count Param documentation.
  # @return [Integer]
  sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
  def foo(verbose:, count:)
    'body says String'
  end
end

Sorbet RBI example

# Ruby source
class Demo
  def foo(verbose:, count:)
    'body says String'
  end
end
# sorbet/rbi/demo.rbi
class Demo
  extend T::Sig

  sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
  def foo(verbose:, count:); end
end

With:

docscribe -a --sorbet --rbi-dir sorbet/rbi lib

Docscribe will use the RBI signature for generated docs.

Sorbet comment placement

For methods with a leading Sorbet sig, Docscribe treats the signature as part of the method header.

That means:

  • new docs are inserted above the first sig
  • existing docs above the sig are recognized and merged
  • existing legacy docs between sig and def are also recognized

Example input:

# demo.rb
class Demo
  extend T::Sig

  sig { returns(Integer) }
  def foo
    1
  end
end

Example output:

# demo.rb
class Demo
  extend T::Sig

  # +Demo#foo+ -> Integer
  #
  # Method documentation.
  #
  # @return [Integer]
  sig { returns(Integer) }
  def foo
    1
  end
end

Generic type formatting

Both RBS and Sorbet integrations support collapse_generics.

When disabled:

rbs:
  collapse_generics: false

sorbet:
  collapse_generics: false

Docscribe preserves generic container details where possible, for example:

  • Array<String>
  • Hash<Symbol, Integer>

When enabled:

rbs:
  collapse_generics: true

sorbet:
  collapse_generics: true

Docscribe simplifies container types to their outer names, for example:

  • Array
  • Hash

Notes and fallback behavior

  • External signature support is the best effort.
  • If a signature source cannot be loaded or parsed, Docscribe falls back to AST inference.
  • RBS and Sorbet integrations are used only to improve generated types; Docscribe still rewrites Ruby source directly.
  • Sorbet support does not require changing your documentation style — it only improves generated @param and @return tags when signatures are available.

Type inference

Heuristics (best-effort).

Parameters:

  • *args -> Array
  • **kwargs -> Hash
  • &block -> Proc
  • keyword args:
    • verbose: true -> Boolean
    • options: {} -> Hash
    • kw: (no default) -> Object
  • positional defaults:
    • 42 -> Integer, 1.0 -> Float, 'x' -> String, :ok -> Symbol
    • [] -> Array, {} -> Hash, /x/ -> Regexp, true/false -> Boolean, nil -> nil

Return values:

  • For simple bodies, Docscribe looks at the last expression or explicit return.
  • Unions with nil become optional types (e.g. String or nil -> String?).
  • For control flow (if/case), it unifies branches conservatively.

Rescue-aware returns and @raise

Docscribe detects exceptions and rescue branches:

  • Rescue exceptions become @raise tags:

    • rescue Foo, Bar -> @raise [Foo] and @raise [Bar]
    • bare rescue -> @raise [StandardError]
    • explicit raise/fail also adds a tag (raise Foo -> @raise [Foo], raise -> @raise [StandardError])
  • Conditional return types for rescue branches:

    • Docscribe adds @return [Type] if ExceptionA, ExceptionB for each rescue clause

Visibility semantics

We match Ruby's behavior:

  • A bare private/protected/public in a class/module body affects instance methods only.
  • Inside class << self, a bare visibility keyword affects class methods only.
  • def self.x in a class body remains public unless private_class_method is used, or it's inside class << self under private.

Inline tags:

  • @private is added for methods that are private in context.
  • @protected is added similarly for protected methods.

Important

module_function: Docscribe documents methods affected by module_function as module methods (M.foo) rather than instance methods (M#foo), because that is usually the callable/public API. If a method was previously private as an instance method, Docscribe will avoid marking the generated docs as @private after it is promoted to a module method.

module M
  private

  def foo; end

  module_function :foo
end

API (library) usage

require "docscribe/inline_rewriter"

code = <<~RUBY
  class Demo
    def foo(a, options: {}); 42; end
    class << self; private; def internal; end; end
  end
RUBY

# Basic insertion behavior
out = Docscribe::InlineRewriter.insert_comments(code)
puts out

# Safe merge / normalization of existing doc-like blocks
out2 = Docscribe::InlineRewriter.insert_comments(code, strategy: :safe)

# Aggressive rebuild of existing doc blocks (similar to CLI -A)
out3 = Docscribe::InlineRewriter.insert_comments(code, strategy: :aggressive)

Configuration

Docscribe can be configured via a YAML file (docscribe.yml by default, or pass --config PATH).

Filtering

Docscribe can filter both files and methods.

File filtering (recommended for excluding specs, vendor code, etc.):

filter:
  files:
    exclude: [ "spec" ]

Method filtering matches method ids like:

  • MyModule::MyClass#instance_method
  • MyModule::MyClass.class_method

Example:

filter:
  exclude:
    - "*#initialize"

CLI overrides are available too:

# Method filtering (matches method ids like A#foo / A.bar)
docscribe --exclude '*#initialize' lib
docscribe --include '/^MyModule::.*#(foo|bar)$/' lib

# File filtering (matches paths relative to the project root)
docscribe --exclude-file 'spec' lib spec
docscribe --exclude-file '/^spec\//' lib

Note

/regex/ passed to --include/--exclude is treated as a method-id pattern. Use --include-file / --exclude-file for file regex filters.

Enable attribute-style documentation generation with:

emit:
  attributes: true

When enabled, Docscribe can generate YARD @!attribute docs for:

  • attr_reader
  • attr_writer
  • attr_accessor
  • Struct.new declarations

attr_* example

Note

  • Attribute docs are inserted above the attr_* call, not above generated methods (since they don’t exist as def nodes).
  • If RBS is enabled, Docscribe will try to use the RBS return type of the reader method as the attribute type.
class User
  attr_accessor :name
end

Generated docs:

class User
  # @!attribute [rw] name
  #   @return [Object]
  #   @param [Object] value
  attr_accessor :name
end

Struct.new examples

Docscribe supports both common Struct.new declaration styles.

Constant-assigned struct

User = Struct.new(:name, :email, keyword_init: true)

Generated docs:

# @!attribute [rw] name
#   @return [Object]
#   @param [Object] value
#
# @!attribute [rw] email
#   @return [Object]
#   @param [Object] value
User = Struct.new(:name, :email, keyword_init: true)

Class-based struct

class User < Struct.new(:name, :email, keyword_init: true)
end

Generated docs:

# @!attribute [rw] name
#   @return [Object]
#   @param [Object] value
#
# @!attribute [rw] email
#   @return [Object]
#   @param [Object] value
class User < Struct.new(:name, :email, keyword_init: true)
end

Docscribe preserves the original declaration style and does not rewrite one form into the other.

Merge behavior

Struct member docs use the same attribute documentation pipeline as attr_* macros, which means they participate in the normal safe/aggressive rewrite flow.

In safe mode, Docscribe can:

  • insert full @!attribute docs when no doc-like block exists
  • append missing struct member docs into an existing doc-like block

Param tag style

Generated writer-style attribute docs respect doc.param_tag_style.

For example, with:

doc:
  param_tag_style: "type_name"

writer params are emitted as:

#   @param [Object] value

With:

doc:
  param_tag_style: "name_type"

they are emitted as:

#   @param value [Object]

Create a starter config

Create docscribe.yml in the current directory:

docscribe init

Write to a custom path:

docscribe init --config config/docscribe.yml

Overwrite if it already exists:

docscribe init --force

Print the template to stdout:

docscribe init --stdout

CI integration

Fail the build if files would need safe updates:

- name: Check inline docs
  run: docscribe lib

Apply safe fixes before the test stage:

- name: Apply safe inline docs
  run: docscribe -a lib

Aggressively rebuild docs:

- name: Rebuild inline docs
  run: docscribe -A lib

Comparison to YARD's parser

Docscribe and YARD solve different parts of the documentation problem:

  • Docscribe inserts/updates inline comments by rewriting source.
  • YARD can generate HTML docs based on inline comments.

Recommended workflow:

  • Use Docscribe to seed and maintain inline docs with inferred tags/types.
  • Optionally use YARD (dev-only) to render HTML from those comments:
yard doc -o docs

Limitations

  • Safe mode only merges into existing doc-like comment blocks. Ordinary comments that are not recognized as documentation are preserved and treated conservatively.
  • Type inference is heuristic. Complex flows and meta-programming will fall back to Object or best-effort types.
  • Aggressive mode (-A) replaces existing doc blocks and should be reviewed carefully.

Roadmap

  • Effective config dump;
  • JSON output;
  • Overload-aware signature selection;
  • Manual @!attribute merge policy;
  • Richer inference for common APIs;
  • Editor integration.

Contributing

bundle exec rspec
bundle exec rubocop

License

MIT