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 firstsig. - 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
sigdeclarations and RBI files with--sorbet/--rbi-dir.
- RBS via
- Optional
@!attributegeneration for:attr_reader/attr_writer/attr_accessor;Struct.newdeclarations in both constant-assigned and class-based styles.
Note
Docscribe is under active development. If you run into any edge cases or have ideas for improvement, feel free to open an issue or submit a pull request.
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 gem collection signatures:
docscribe -a --rbs-collection 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
# Check what safe doc updates would be applied
docscribe lib
# Apply safe updates (insert missing docs, merge existing)
docscribe -a lib
# Rebuild all doc blocks aggressively
docscribe -A libTip
See CLI for all options and Update strategies for the difference between safe and aggressive modes.
Want IDE integration? Check out the VS Code and RubyMine plugins.
- Docscribe
- Quick start
- Contents
- Installation
- Architecture
- CLI
- Update strategies
- Tips & tricks
- Parser backend (Parser gem vs Prism)
- External type integrations (optional)
- Type inference
- Rescue-aware returns and @raise
- Visibility semantics
- API (library) usage
- Plugin system
- Configuration
- CI integration
- Comparison to YARD's parser
- Limitations
- Roadmap
- Editor Integration
- Contributing
- Discussion & Community
- Logo Attribution
- License
Add to your Gemfile:
gem "docscribe"Then:
bundle installOr install globally:
gem install docscribeRequires Ruby 2.7+.
Docscribe is organized into several subsystems. The CLI layer receives user input and orchestrates configuration loading, then delegates to the core engine which parses source code, collects methods (using an AST walker), builds YARD doc lines — combining heuristic type inference, external RBS/Sorbet signatures, and plugin output — and finally rewrites the source via a strategy (safe merge or aggressive replace).
flowchart TB
subgraph CLI["CLI Layer"]
Exe["exe/docscribe\nEntry point"]
Run["CLI::Run\nMain execution\n· expand paths\n· iterate files\n· report results"]
Options["CLI::Options\nARGV parsing\n(mode, strategy,\nfilters, flags)"]
InitCmd["CLI::Init\ndocscribe init\nGenerate config"]
GenCmd["CLI::Generate\ndocscribe generate\nScaffold plugins"]
SigsCmd["CLI::Sigs\ndocscribe sigs\nCheck RBS coverage"]
RbsGenCmd["CLI::RbsGen\ndocscribe rbs\nGenerate RBS from YARD"]
SarifFormatter["CLI::Formatters::Sarif\nSARIF 2.1 JSON\nCode Scanning"]
ConfigBuilder["CLI::ConfigBuilder\nApply CLI overrides\nto config"]
end
subgraph Config["Configuration"]
ConfigClass["Config\nCentral config object\n· raw hash\n· query methods"]
Defaults["config/defaults.rb\nDEFAULT hash"]
Loader["config/loader.rb\nYAML loading\n+ deep merge"]
Emit["config/emit.rb\nEmission toggles\n(header, tags, etc.)"]
Filtering["config/filtering.rb\nFile/method\ninclude/exclude"]
RBSConfig["config/rbs.rb\nRBS provider\nfactory"]
SorbetConfig["config/sorbet.rb\nSorbet provider\nchain factory"]
PluginConfig["config/plugin.rb\nPlugin loading\nfrom YAML"]
end
subgraph Parsing["Parsing"]
ParsingModule["Parsing\nBackend selection\n(:parser / :prism)"]
ParserGem["Parser gem\n(whitequark/parser)"]
Prism["Prism translator\n(Ruby 3.4+)"]
end
subgraph Core["Core Engine"]
InlineRewriter["InlineRewriter\n· parse -> collect\n· deduplicate -> dispatch\n· rewrite"]
Collector["Collector\n< Parser::AST::Processor\nAST walker\n· find methods/attrs\n· track visibility\n· track containers"]
DocBuilder["DocBuilder\nGenerate YARD doc lines\n· combine inference\n· external signatures\n· plugin tags"]
DocBlock["DocBlock\nSafe strategy:\nparse -> merge -> sort\nexisting doc blocks"]
SourceHelpers["SourceHelpers\nPosition/range\nutilities"]
end
subgraph Infer["Inference Engine"]
InferModule["Infer\nEntry point"]
Params["Infer::Params\nParameter type\nfrom name + default"]
Returns["Infer::Returns\nReturn type\nfrom method body"]
Raises["Infer::Raises\n@raise tags\nfrom raise/rescue"]
Literals["Infer::Literals\nAST literal ->\ntype string"]
Names["Infer::Names\n:const node ->\nFQN string"]
ASTWalk["Infer::ASTWalk\nRecursive DFS\nAST traversal"]
end
subgraph Plugins["Plugin System"]
PluginModule["Plugin\nTag/Collector\ndispatch"]
Registry["Plugin::Registry\nGlobal registry\n· register -> route\n· tag_entries\n· collector_entries"]
TagPlugin["Base::TagPlugin\nOverride #call(context)\n-> Array<Tag>"]
CollectorPlugin["Base::CollectorPlugin\nOverride #collect(ast, buffer)\n-> Array<Hash>"]
TagValue["Plugin::Tag\nStruct (name, text, types)"]
Context["Plugin::Context\nMethod snapshot struct"]
end
subgraph Types["External Type System"]
ProviderChain["ProviderChain\nComposite:\nquery in order\nfirst match wins"]
RBSProvider["RBS::Provider\n.rbs files\n-> RBS lib"]
RBSFormatter["RBS::TypeFormatter\nRBS type ->\nYARD type string"]
RBSCollection["RBS::CollectionLoader\nrbs_collection\n.lock.yaml"]
SorbetBase["Sorbet::BaseProvider\nRBS::Prototype::RBI\nbridge"]
SorbetSource["Sorbet::SourceProvider\nInline sig{}\ndeclarations"]
SorbetRBI["Sorbet::RBIProvider\n.rbi files\ndirectories"]
end
subgraph YardTypes["YARD Type Parser"]
YParser["Yard::Parser\nParse YARD type\nstrings -> AST"]
YFormatter["Yard::Formatter\nYARD AST ->\nRBS string"]
YTypes["Yard::Types\n9 AST node types\n(Named, Generic, etc.)"]
end
Exe --> Run
Run --> Options
Run --> InitCmd
Run --> GenCmd
Run --> SigsCmd
Run --> RbsGenCmd
Run --> SarifFormatter
Run --> ConfigBuilder
ConfigBuilder --> ConfigClass
ConfigClass --> Defaults
ConfigClass --> Loader
ConfigClass --> Emit
ConfigClass --> Filtering
ConfigClass --> RBSConfig
ConfigClass --> SorbetConfig
ConfigClass --> PluginConfig
Run --> InlineRewriter
InlineRewriter --> ParsingModule
ParsingModule --> ParserGem
ParsingModule --> Prism
InlineRewriter --> Collector
Collector --> PluginModule
PluginModule --> Registry
Registry --> CollectorPlugin
InlineRewriter --> DocBuilder
DocBuilder --> InferModule
InferModule --> Params
InferModule --> Returns
InferModule --> Raises
Params --> Literals
Returns --> Literals
Raises --> ASTWalk
Raises --> Names
DocBuilder --> ProviderChain
ProviderChain --> SorbetSource
ProviderChain --> SorbetRBI
ProviderChain --> RBSProvider
SorbetSource --> SorbetBase
SorbetRBI --> SorbetBase
RBSProvider --> RBSFormatter
RBSProvider --> RBSCollection
DocBuilder --> PluginModule
PluginModule --> Registry
Registry --> TagPlugin
TagPlugin --> TagValue
TagPlugin --> Context
InlineRewriter --> DocBlock
InlineRewriter --> SourceHelpers
RbsGenCmd --> ParsingModule
RbsGenCmd --> YParser
YParser --> YTypes
YParser --> YFormatter
flowchart LR
Input["Source files\n(.rb)"] --> Parse["Parsing.parse_buffer\nParser gem / Prism"]
Parse --> AST["AST + Comments"]
AST --> Collect["Collector.process\n· Find methods\n· Track visibility\n· Find attr_*"]
AST --> CollectPlugins["CollectorPlugin#collect\n· Custom AST walks\n· Non-standard constructs"]
Collect --> Insertions["Insertion list\n(sorted by position)"]
CollectPlugins --> Insertions
Insertions --> Dedup["Deduplicate\n(override by position)"]
Dedup --> Build["DocBuilder.build_doc_lines\nper insertion"]
Build --> Infer["Infer params / returns / raises\n(heuristic fallback)"]
Build --> SigQuery["ProviderChain\nquery external types"]
Build --> TagPlugins["TagPlugin#call\n(add extra @tags)"]
Infer --> ResultDoc["Generated YARD doc block"]
SigQuery --> ResultDoc
TagPlugins --> ResultDoc
ResultDoc --> Strategy{"Strategy?"}
Strategy -->|Safe| Merge["DocBlock.merge\npreserve + append + sort"]
Strategy -->|Aggressive| Replace["Replace entirely"]
Merge --> Rewritten["Rewriter#process\n-> rewritten source"]
Replace --> Rewritten
Rewritten --> Output["Modified .rb file\n/ STDOUT"]
docscribe [options] [files...]
docscribe init [options]
docscribe generate [type] [name] [options]
docscribe sigs [options] [files...]
docscribe rbs [options] [files...]
docscribe update_types [directory]
docscribe check_for_comments [paths...]Docscribe has three main ways to run:
- Inspect mode (default): checks what safe doc updates would be applied and exits 1 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.
- 0 — all files are up to date (no changes needed)
- 1 — some files need documentation updates
- 2 — execution error (parse error, missing files, etc.)
-
-a,--autocorrect
Apply safe doc updates in place. -
-A,--autocorrect-all
Apply aggressive doc updates in place. -
--rbs-collection
Auto-discover the RBS collection directory fromrbs_collection.lock.yaml.
Reads thepath:field written bybundle exec rbs collection installand adds
it to the signature search path automatically. Implies--rbs. -
--stdin
Read source from STDIN and print rewritten output. -
--verbose
Print per-file actions. Also enables--progress. -
--progress
Show progress ([N/total] path/to/file.rb) on stderr as each file is processed. -
--quiet(-q)
Only show status, no details (suppresses change reasons). Overrides the default detailed output. -
--explain
Show detailed reasons for each file (default; no-op for compatibility). -
-k,--keep-descriptions
Preserve existing documentation text when rebuilding doc blocks in aggressive mode. -
-B,--no-boilerplate
Suppress boilerplate text (Method documentation.,Param documentation.) in output.
Equivalent toemit.include_default_message: falseandemit.include_param_documentation: falsein config. -
--format FORMAT
Output format:text(default, human-readable),json(machine-readable, RuboCop-compatible), orsarif(SARIF 2.1 JSON, compatible with GitHub Code Scanning). -
--rbs
Use RBS signatures for@param/@returnwhen available (falls back to inference). -
--sig-dir DIR
Add an RBS signature directory (repeatable). Implies--rbs. -
--sorbet
Use Sorbet signatures for@param/@returnwhen available (falls back to inference). -
--rbi-dir DIR
Add an Sorbet RBI directory (repeatable). Implies--sorbet. -
--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.
-
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
-
Use RBS signatures with auto-discovered gem collection:
docscribe -a --rbs-collection lib
-
Combine collection auto-discovery with a custom sig directory:
docscribe -a --rbs-collection --sig-dir sig lib
-
Show detailed reasons for files that would change:
docscribe --verbose lib
Warning
docscribe sigs requires Ruby 3.0+ and the rbs gem. On Ruby 2.7 it will print an error and exit with code 2.
docscribe sigs parses Ruby files, extracts method definitions, and checks each
method against the configured RBS signature directories. Reports methods that lack RBS type signatures.
# Check lib/ against sig/ signatures
docscribe sigs lib
# Use RBS collection
docscribe sigs --rbs-collection lib
# Multiple signature directories
docscribe sigs -s sig -s vendor/sigs lib
# Verbose output (also prints methods that have signatures)
docscribe sigs --verbose libFlags:
-s,--sig-dir DIR— add an RBS signature directory (repeatable). Default:sig.--rbs-collection— use RBS collection (readsrbs_collection.lock.yaml).--verbose— also print methods that have signatures.-h,--help— show help.
Exit codes:
0— all methods have RBS signatures.1— some methods lack RBS signatures.2— error occurred.
Warning
docscribe rbs requires Ruby 3.0+ and the rbs gem. On Ruby 2.7 it will print an error and exit with code 2.
docscribe rbs parses YARD comments (@param, @return, @option) from Ruby files
and generates corresponding .rbs signature files.
# Generate RBS files in sig/
docscribe rbs lib
# Print to stdout (no files written)
docscribe rbs -n lib
# Specify output directory
docscribe rbs -o sig/gen lib
# Overwrite existing files
docscribe rbs -f libFlags:
-o,--output DIR— output directory (default:sig).-n,--dry-run— print generated RBS to stdout, do not write files.-f,--force— overwrite existing files.-h,--help— show help.
Exit codes:
0— success.1— errors occurred during processing.2— execution error (no files found).
# Example: lib/user.rb
class User
# @param [String] name
# @param [Integer] age
# @return [User]
def initialize(name, age)
@name = name
@age = age
end
enddocscribe rbs lib
# Generated sig/user.rbs# sig/user.rbs
class User
def initialize: (String name, Integer age) -> User
endNote
docscribe update_types is a convenience alias for the two-pass workflow above. It requires Ruby 3.0+ and the rbs
gem (because of --rbs-collection). The RBS collection must be set up first with
bundle exec rbs collection install. Type accuracy depends on your RBS signatures — if signatures are incomplete or
missing, types will fall back to AST inference.
docscribe update_types runs two passes to bring both docs and RBS signatures up to date:
- Pass 1 —
docscribe -AkB --rbs-collection <dir>: aggressively rebuilds doc blocks, preserves existing descriptions, suppresses boilerplate, uses RBS collection types. - Pass 2 —
docscribe -aB --rbs-collection <dir>: safe merge cleanup with no boilerplate.
# Update docs in lib/ using RBS collection
docscribe update_types lib
# Defaults to current directory
docscribe update_typesdocscribe check_for_comments scans Ruby source files for YARD comments that still contain default placeholder
text. Reads the configured placeholder messages from docscribe.yml (or built-in defaults). Useful in CI to catch
files where auto-generated text was never replaced with real documentation.
# Scan entire project
docscribe check_for_comments
# Scan specific directories
docscribe check_for_comments lib appExit code 0 if no placeholders found, 1 if any are detected.
Flags:
-h,--help— show help.
Docscribe supports two update strategies: safe and aggressive.
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.
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.
Caution
Aggressive mode rewrites existing doc blocks entirely. Review changes with git diff before committing.
In inspect mode, Docscribe prints one character per file:
.= file is up to dateF= file would changeE= file had an errorM= type mismatch detected (external RBS/Sorbet signature differs from inferred type)
In write modes:
.= file already OKC= file was updatedE= file had an errorM= file updated but type mismatch remains
Important
The M marker only appears when --rbs or --sorbet is enabled, since type mismatches are detected by
comparing external signatures against inferred types.
With --verbose, Docscribe prints per-file statuses instead; type mismatches show as MT with the specific difference.
With --explain, Docscribe also prints detailed reasons, such as:
- missing
@param - missing
@return - missing module_function note
- unsorted tags
Use --quiet to suppress these details and show only file names and the summary line.
Useful flag combinations for common workflows:
docscribe -aB lib— safe autocorrect without boilerplate. Uses safe mode but suppresses default text, leaving only type tags (@param,@return). Clean minimal docs.docscribe -AkB lib— aggressive autocorrect with preserved descriptions and no boilerplate. Rebuilds doc blocks, keeps your hand-written descriptions, suppresses default text. Best for rebaselining.docscribe -a --rbs-collection lib— safe autocorrect using RBS gem collection signatures. Recommended for Rails projects.docscribe -a --sorbet --rbi-dir sorbet/rbi lib— safe autocorrect using Sorbet RBI signatures.docscribe update_types lib— two-pass type-aware update: aggressively rebuilds docs with kept descriptions and RBS collection, then safe-merges to clean up. See docscribe update_types.
Note
docscribe update_types is a convenient shortcut, but be aware it uses --rbs-collection under the hood.
If your RBS signatures are incomplete, types may fall back to AST inference.
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
parsergem. - On Ruby >= 3.4, Docscribe parses using Prism and translates the tree into the
parsergem's AST.
Note
Prism support is automatic on Ruby 3.4+. On earlier Rubies, the parser gem is always used.
You can override with DOCSCRIBE_PARSER_BACKEND=prism on Ruby 3.1+ if you have the prism gem installed,
but this is not recommended for production use.
You can force a backend with an environment variable:
DOCSCRIBE_PARSER_BACKEND=parser bundle exec docscribe lib
DOCSCRIBE_PARSER_BACKEND=prism bundle exec docscribe libDocscribe can improve generated @param and @return types by reading external signatures instead of relying only on
AST inference.
Important
Docscribe resolves types in a two-level chain. For documentation tags (@param, @return), the priority is:
| Priority | Source | When active |
|---|---|---|
| 1 | Inline Sorbet sig { ... } in current file |
--sorbet or sorbet.enabled: true |
| 2 | Sorbet RBI files (.rbi) |
--sorbet --rbi-dir or sorbet.rbi_dirs |
| 3 | RBS files (sig_dirs + collection, loaded into one env) | --rbs --sig-dir / --rbs-collection or rbs.* config |
| 3a | └─ Fallback: sig_dirs only (collection dropped on conflict) | Automatic if priority 3 env load fails |
| 4 | AST inference (fallback) | Always active |
For intra-method body inference (e.g. resolving Integer#positive? -> Boolean), a separate core RBS provider
loads only stdlib/built-in signatures and is active automatically when the rbs gem is available — even without
--rbs.
If an external signature cannot be loaded or parsed, Docscribe falls back to the next source in the chain instead of failing.
Important
All RBS features require the rbs gem. Add gem "rbs" to your Gemfile and run bundle install,
or install it globally with gem install rbs.
On Ruby 2.7 the rbs gem cannot load at all — --rbs, --sig-dir, --rbs-collection,
docscribe sigs, and docscribe rbs will print a warning and fall back to AST-only inference.
On Ruby 3.0+, if the gem is missing, Docscribe silently falls back to inference when you pass RBS flags.
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 libYou can pass --sig-dir multiple times:
docscribe -a --rbs --sig-dir sig --sig-dir vendor/sigs libConfig:
rbs:
enabled: false
sig_dirs:
- sig
collection: false
collapse_generics: false
collapse_object_generics: false
warn_missing_collection: truecollection— enable auto-discovery of RBS collection fromrbs_collection.lock.yaml. Pass--rbs-collectionon the CLI.collapse_generics— strip all generic type arguments (e.g.Array<String>->Array).collapse_object_generics— only strip generic arguments when all areObject(e.g.Hash<Object, Object>->Hash, butHash<Symbol, Object>stays).warn_missing_collection— warn on stderr whenrbs_collection.lock.yamlis found but collection is not enabled. Set tofalseto suppress.
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
endGenerated 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
endIf your project uses rbs collection,
Docscribe can discover the installed gem signatures automatically without requiring
you to pass --sig-dir manually.
Setup:
# 1. Initialize the collection config (one-time)
bundle exec rbs collection init
# 2. Install gem signatures
bundle exec rbs collection installThis produces rbs_collection.lock.yaml and .gem_rbs_collection/ in your project root.
Usage:
docscribe -a --rbs-collection libDocscribe reads the path: field from rbs_collection.lock.yaml and adds the
resolved directory to the signature search path. If no path: is set, it falls
back to .gem_rbs_collection.
You can combine --rbs-collection with --sig-dir to mix gem signatures with your own:
docscribe -a --rbs-collection --sig-dir sig libNote
--rbs-collection only improves types for methods defined in gems that ship RBS
signatures. For your own classes, provide a sig/ directory with hand-written or
generated .rbs files.
Important
If rbs_collection.lock.yaml is missing or the collection directory does not exist
on disk, Docscribe will print a warning and skip the collection. Run
bundle exec rbs collection install first.
Docscribe can also read Sorbet signatures from:
- inline
sigdeclarations in Ruby source - RBI files
CLI:
docscribe -a --sorbet libWith RBI directories:
docscribe -a --sorbet --rbi-dir sorbet/rbi libYou can pass --rbi-dir multiple times:
docscribe -a --sorbet --rbi-dir sorbet/rbi --rbi-dir rbi libConfig:
sorbet:
enabled: true
rbi_dirs:
- sorbet/rbi
- rbi
collapse_generics: falseclass Demo
extend T::Sig
sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
def foo(verbose:, count:)
'body says String'
end
endDocscribe 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# 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
endWith:
docscribe -a --sorbet --rbi-dir sorbet/rbi libDocscribe will use the RBI signature for generated docs.
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
sigare recognized and merged - existing legacy docs between
siganddefare also recognized
Example input:
# demo.rb
class Demo
extend T::Sig
sig { returns(Integer) }
def foo
1
end
endExample output:
# demo.rb
class Demo
extend T::Sig
# +Demo#foo+ -> Integer
#
# Method documentation.
#
# @return [Integer]
sig { returns(Integer) }
def foo
1
end
endBoth RBS and Sorbet integrations support generic type collapsing.
collapse_generics — strips all generic type arguments.
collapse_object_generics — only strips arguments when all are Object (e.g. Hash<Object, Object> -> Hash, but
Hash<Symbol, Object> stays).
When both disabled:
rbs:
collapse_generics: false
collapse_object_generics: false
sorbet:
collapse_generics: falseDocscribe preserves generic container details, for example:
Array<String>Hash<Symbol, Integer>
When collapse_generics is enabled:
rbs:
collapse_generics: true
sorbet:
collapse_generics: trueDocscribe simplifies all container types to their outer names, for example:
ArrayHash
- 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
@paramand@returntags when signatures are available.
Heuristics (best-effort).
Parameters:
*args->Array**kwargs->Hash&block->Proc- keyword args:
verbose: true->Booleanoptions: {}->Hashkw:(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
nilbecome optional types (e.g.Stringornil->String?). - For control flow (
if/case), it unifies branches conservatively.
Tip
Docscribe resolves return types for core Ruby methods (Integer#positive?, String#upcase, etc.)
even without --rbs — the core RBS provider is always active when the rbs gem is available.
- RBS core type inference: when
--rbsis enabled, Docscribe resolves return types for method calls on core types from their RBS definitions:arg.positive?(arg = 1) ->Boolean(fromInteger#positive?)arg.to_i(arg = "") ->Integer(fromString#to_i)arg.to_s.length(arg = 1) ->Integer(chained: Integer -> String -> Integer)arg.upcase(arg = "") ->String(fromString#upcase)- Rescue branches are also resolved (e.g.
"default"->String)
Docscribe detects exceptions and rescue branches:
-
Rescue exceptions become
@raisetags:rescue Foo, Bar->@raise [Foo]and@raise [Bar]- bare rescue ->
@raise [StandardError] - explicit
raise/failalso adds a tag (raise Foo->@raise [Foo],raise->@raise [StandardError])
-
Conditional return types for rescue branches:
- Docscribe adds
@return [Type] if ExceptionA, ExceptionBfor each rescue clause
- Docscribe adds
We match Ruby's behavior:
- A bare
private/protected/publicin a class/module body affects instance methods only. - Inside
class << self, a bare visibility keyword affects class methods only. def self.xin a class body remainspublicunlessprivate_class_methodis used, or it's insideclass << selfunderprivate.
Inline tags:
@privateis added for methods that are private in context.@protectedis 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
endrequire "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)Docscribe ships a plugin system that lets you extend documentation generation without modifying the gem itself.
There are two extension points:
| Type | When it runs | What it produces |
|---|---|---|
| TagPlugin | After a method is collected and its doc block is being built | Extra YARD tags appended to the block |
| CollectorPlugin | Before doc building, alongside the standard AST collector | New insertion targets for non-standard constructs |
A TagPlugin receives a snapshot of everything known about a method at generation time and returns zero or more
additional YARD tags to append to the doc block.
class SincePlugin < Docscribe::Plugin::Base::TagPlugin
def initialize(version:)
@version = version
end
# @param [Docscribe::Plugin::Context] context
# @return [Array<Docscribe::Plugin::Tag>]
def call(context)
[Docscribe::Plugin::Tag.new(name: 'since', text: @version)]
end
endThe Context struct provides:
| Attribute | Type | Description |
|---|---|---|
node |
Parser::AST::Node |
The :def or :defs AST node |
container |
String |
e.g. "MyModule::MyClass" |
scope |
Symbol |
:instance or :class |
visibility |
Symbol |
:public, :protected, or :private |
method_name |
Symbol |
Method name |
inferred_params |
Hash{String => String} |
Name -> inferred type |
inferred_return |
String |
Inferred return type |
source |
String |
Raw method source text |
The Tag struct:
# Simple tag
Docscribe::Plugin::Tag.new(name: 'since', text: '1.3.0')
# => # @since 1.3.0
# Tag with types
Docscribe::Plugin::Tag.new(name: 'raise', types: ['ArgumentError'], text: 'if name is nil')
# => # @raise [ArgumentError] if name is nilA CollectorPlugin receives the raw AST and source buffer for each file. It walks the tree itself and returns insertion
targets that Docscribe will document according to the selected strategy.
class DefineMethodPlugin < Docscribe::Plugin::Base::CollectorPlugin
# @param [Parser::AST::Node] ast
# @param [Parser::Source::Buffer] buffer
# @return [Array<Hash>]
def collect(ast, buffer)
results = []
Docscribe::Infer::ASTWalk.walk(ast) do |node|
next unless node.type == :send
_recv, meth, name_node, *_rest = *node
next unless meth == :define_method
next unless name_node&.type == :sym
meth_name = name_node.children.first
results << {
anchor_node: node,
doc: "# Dynamic method: #{meth_name}\n# @return [Object]\n"
}
end
results
end
endEach result hash must have :anchor_node plus either :doc or :method_override:
| Key | Type | Description |
|---|---|---|
:anchor_node |
Parser::AST::Node |
Node above which to insert the doc block |
:doc |
String |
Complete doc block text including newlines (Docscribe may normalize indentation and default message for method anchors) |
:method_override |
Hash |
Structured overrides that patch the standard DocBuilder output instead of replacing it (see below) |
Note
You do not need to handle indentation manually. Docscribe reads the indentation from anchor_node and applies it to
every line of :doc automatically.
When returning doc:, CollectorPlugins provide raw strings that Docscribe inserts without running the standard
method DocBuilder pipeline. Docscribe does normalize indentation and may prepend the default method message for
def/defs anchors, but headers (emit.header) and param/return tags must be included explicitly.
When a CollectorPlugin targets a def method, it can return method_override: instead of doc: to patch the
standard DocBuilder output rather than replace it entirely:
{
anchor_node: node,
method_override: {
return_type: 'ActiveRecord::Relation', # overrides @return
param_types: { 'period' => 'Integer' }, # merges into @param types
tags: [Docscribe::Plugin::Tag.new(name: 'since', text: '2.0')] # additional tags
}
}| Key | Type | Description |
|---|---|---|
:return_type |
String |
Overrides the @return type name |
:param_types |
Hash{String=>String} |
Merges into inferred param types (external sig wins) |
:tags |
Array<Tag/Hash> |
Tags appended to the doc block (Hash auto-converted to Tag) |
method_override merges at the data level before rendering, so the standard pipeline still generates headers,
@param tags, default messages, and tag sorting. Only the specified fields are overridden.
Plugins are registered at load time. The recommended pattern is to put registrations in a dedicated file and reference
it from docscribe.yml.
docscribe_plugins.rb (in your project root or lib/):
require 'docscribe/plugin'
# Tag plugin
class SincePlugin < Docscribe::Plugin::Base::TagPlugin
def call(context)
[Docscribe::Plugin::Tag.new(name: 'since', text: '1.3.0')]
end
end
# Collector plugin
class DefineMethodPlugin < Docscribe::Plugin::Base::CollectorPlugin
def collect(ast, buffer)
# ...
end
end
# You can optionally set priority (default: 0). Higher number => higher priority.
Docscribe::Plugin::Registry.register(SincePlugin.new, priority: 10)
Docscribe::Plugin::Registry.register(DefineMethodPlugin.new, priority: 0)docscribe.yml:
plugins:
require:
- ./docscribe_pluginsEach entry is passed to require. The path is expanded relative to the current working directory.
Duck typing is also supported — any object responding to #call is treated as a TagPlugin, any object responding to
#collect is treated as a CollectorPlugin:
# Lambda as a TagPlugin
Docscribe::Plugin::Registry.register(
->(context) { [Docscribe::Plugin::Tag.new(name: 'api', text: 'public')] }
)Registry.register(plugin, priority: N) accepts an optional integer priority (default: 0).
Higher number means higher priority.
CollectorPlugin priority (conflicts at the same source position):
- For
doc:insertions: if a plugin insertion and a standard method insertion share the same source position (anchor_node.loc.expression.begin_pos), the standard insertion is dropped and the plugin insertion is kept. - For
method_override:insertions: the method insertion is kept and patched with the override data. The standard DocBuilder pipeline still runs (generating@param, headers, etc.), and only the specified fields are overridden. - If multiple CollectorPlugins target the same source position, only insertions from the highest-priority plugin(s) are kept (ties are kept).
- Multiple insertions from the winning plugin(s) at the same position are preserved (e.g. one
@!attributeper column).
TagPlugin priority:
- TagPlugins run in descending priority order (higher priority runs earlier).
- Multiple TagPlugins may emit the same tag name (e.g. two
@sincetags) — duplicates in the same run are allowed.
This allows plugins like ModelAttributes to supply more accurate @return
types for ActiveRecord model methods, replacing the generic docs the standard
collector would have produced for the same def.
Docscribe handles idempotency for plugins automatically.
TagPlugin: in safe merge mode, Docscribe will not add a plugin tag if the existing doc block already contains a tag with that name. (Multiple TagPlugins can still emit the same tag name in a single run; duplicates are allowed.)
CollectorPlugin: idempotency depends on the selected strategy.
| Strategy | Behaviour |
|---|---|
:safe |
Skips insertion if any comment block already exists immediately above anchor_node |
:aggressive |
Removes the existing comment block above anchor_node and inserts a fresh doc block |
This means a CollectorPlugin-generated block will not be duplicated on repeated safe runs, and will be fully rebuilt
on aggressive runs.
Sample plugins available at examples:
ApiTagPlugin(tag_plugin/): TagPlugin that appends@api public/@api privatebased on method visibility.RailsAssociations(collector_plugin/rails_associations/): CollectorPlugin that documents ActiveRecordbelongs_to,has_many, etc.SchemaAttributes(collector_plugin/schema_attributes/): CollectorPlugin that generates@!attributeblocks by readingdb/schema.rb.ModelAttributes(collector_plugin/model_attributes/): CollectorPlugin that generates accurate@returntypes for model methods usingdb/schema.rbordb/structure.sql.
Docscribe can be configured via a YAML file (docscribe.yml by default, or pass --config PATH).
Ruby 3.2+ allows anonymous block arguments (def foo(&)). By default, Docscribe generates @param [Proc] block for
these — but since the parameter has no name, the tag is misleading.
To suppress the @param tag for anonymous block arguments:
skip_anonymous_block_params: trueWhen false (default), anonymous block params generate @param [Proc] block.
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_methodMyModule::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\//' libNote
/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: trueWhen enabled, Docscribe can generate YARD @!attribute docs for:
attr_readerattr_writerattr_accessorStruct.newdeclarations
Note
- Attribute docs are inserted above the
attr_*call, not above generated methods (since they don’t exist asdefnodes). - 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
endGenerated docs:
class User
# @!attribute [rw] name
# @return [Object]
# @param [Object] value
attr_accessor :name
endDocscribe supports both common Struct.new declaration styles.
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 User < Struct.new(:name, :email, keyword_init: true)
endGenerated 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)
endDocscribe preserves the original declaration style and does not rewrite one form into the other.
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
@!attributedocs when no doc-like block exists - append missing struct member docs into an existing doc-like block
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] valueWith:
doc:
param_tag_style: "name_type"they are emitted as:
# @param value [Object]Create docscribe.yml in the current directory:
docscribe initWrite to a custom path:
docscribe init --config config/docscribe.ymlOverwrite if it already exists:
docscribe init --forcePrint the template to stdout:
docscribe init --stdoutDocscribe can scaffold a plugin file so you don't have to write boilerplate by hand.
Generate a TagPlugin:
docscribe generate tag MyPlugin
# Created: my_plugin.rbGenerate a CollectorPlugin:
docscribe generate collector MyCollector
# Created: my_collector.rbWrite to a specific directory:
docscribe generate tag SincePlugin --output lib/plugins
# Created: lib/plugins/since_plugin.rbPrint to STDOUT instead of writing a file:
docscribe generate tag SincePlugin --stdoutThe generated file contains:
- the correct base class (
Base::TagPluginorBase::CollectorPlugin) - inline comments describing every available
Contextattribute (TagPlugin) or the expected return shape (CollectorPlugin) - TODO markers showing exactly where to add your logic
- registration and next-steps instructions printed to the terminal
Note
The class name must be a valid Ruby constant (MyPlugin, My::Plugin).
The output filename is the snake_case equivalent (my_plugin.rb, my/plugin.rb).
Click to expand — 43 config keys
| Key | Type | Default | Description |
|---|---|---|---|
keep_descriptions |
bool |
false |
Preserve existing doc text in aggressive mode |
skip_anonymous_block_params |
bool |
false |
Skip @param [Proc] block for anonymous & params |
emit.header |
bool |
false |
Generate method header line (+#foo+ -> ...) |
emit.include_default_message |
bool |
true |
Insert default message (Method documentation.) |
emit.include_param_documentation |
bool |
true |
Insert param description text (Param documentation.) |
emit.param_tags |
bool |
true |
Generate @param tags |
emit.return_tag |
bool |
true |
Generate @return tag |
emit.visibility_tags |
bool |
true |
Generate @private/@protected tags |
emit.raise_tags |
bool |
true |
Generate @raise tags (for raise in method body) |
emit.rescue_conditional_returns |
bool |
true |
Consider rescue branches in return type inference |
emit.attributes |
bool |
false |
Generate @!attribute for attr_* and Struct.new |
doc.default_message |
string |
"Method documentation." |
Default text for method description |
doc.param_documentation |
string |
"Param documentation." |
Default text for param description |
doc.sort_tags |
bool |
true |
Sort tags according to tag_order |
doc.tag_order |
string[] |
["todo","note","api","private","protected","param","option","yieldparam","raise","return"] |
Tag sort order |
doc.param_tag_style |
string |
"type_name" |
@param style: type_name ([Type] name) or name_type (name [Type]) |
inference.fallback_type |
string |
"Object" |
Fallback type when inference yields no result |
inference.nil_as_optional |
bool |
true |
Treat nil as an optional type |
inference.treat_options_keyword_as_hash |
bool |
true |
Treat **options as Hash in @option tags |
filter.include |
string[] |
[] |
Only include methods matching pattern |
filter.exclude |
string[] |
[] |
Exclude methods matching pattern |
filter.visibilities |
string[] |
["public","protected","private"] |
Visibilities to process |
filter.scopes |
string[] |
["instance","class"] |
Scopes to process |
filter.files.include |
string[] |
[] |
Only process files matching pattern |
filter.files.exclude |
string[] |
["spec"] |
Skip files matching pattern |
methods.instance.public |
object |
{} |
Overrides for instance public methods |
methods.instance.protected |
object |
{} |
Overrides for instance protected methods |
methods.instance.private |
object |
{} |
Overrides for instance private methods |
methods.class.public |
object |
{} |
Overrides for class public methods |
methods.class.protected |
object |
{} |
Overrides for class protected methods |
methods.class.private |
object |
{} |
Overrides for class private methods |
rbs.enabled |
bool |
false |
Enable RBS integration |
rbs.collection |
bool |
false |
Auto-discover RBS collection from rbs_collection.lock.yaml |
rbs.sig_dirs |
string[] |
["sig"] |
RBS signature directories |
rbs.collection_dirs |
string[] |
[] |
RBS collection directories (set automatically) |
rbs.collapse_generics |
bool |
false |
Strip generic arguments (Array<String> -> Array) |
rbs.collapse_object_generics |
bool |
false |
Strip generics only when all are Object |
rbs.warn_missing_collection |
bool |
true |
Warn when rbs_collection.lock.yaml found but collection not enabled |
sorbet.enabled |
bool |
false |
Enable Sorbet integration |
sorbet.rbi_dirs |
string[] |
["sorbet/rbi","rbi"] |
RBI file directories |
sorbet.collapse_generics |
bool |
false |
Strip generic arguments from Sorbet signatures |
plugins.require |
string[] |
[] |
Plugin paths for require |
Fail the build if files would need safe updates:
- name: Check inline docs
run: docscribe libApply safe fixes before the test stage:
- name: Apply safe inline docs
run: docscribe -a libRebuild docs aggressively, preserve descriptions, suppress boilerplate, with verbose output:
- name: Rebuild inline docs
run: docscribe -AkB --verboseRun static type checking with Steep (requires Ruby ≥ 3.2):
- name: Steep (type check)
if: ${{ matrix.ruby >= 3.2 }}
run: bundle exec steep checkFail the build if any generated docs still contain default placeholder text:
- name: Check for placeholder docs
run: docscribe check_for_comments lib/For stricter validation, check for empty doc blocks:
- name: Check for empty doc blocks
run: "! grep -rn '^ # $' lib/"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- 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
Objector best-effort types. - Aggressive mode (
-A) replaces existing doc blocks and should be reviewed carefully.
- Method behavior inference from AST;
- Deeper YARD integration — parse
.csource comments and generate docs for C-extensions; - Custom parser plugins — support non-Ruby languages (Crystal, etc.) via the plugin system;
- Effective config dump;
- Overload-aware signature selection;
- Manual
@!attributemerge policy; - Richer inference for common APIs;
- Editor integration (LSP, VS Code extension);
- Documentation coverage report — percentage of documented methods, params, returns;
- Pre-commit hook auto-install (
docscribe init --pre-commit); - Parallel processing for large codebases.
Docscribe provides IDE plugins for a better development experience:
- Repository: github.com/FlorexLabs/docscribe-vscode
- Marketplace: VS Code Marketplace
- Features: inline diagnostics, quick-fix, auto-check on save, status bar
- Repository: github.com/FlorexLabs/docscribe-rubymine
- Marketplace: JetBrains Marketplace
- Features: ExternalAnnotator, AnAction, IntentionAction, Settings UI
Note
Both plugins require docscribe >= 1.5.0.
bundle exec rspec
bundle exec rubocopThe Docscribe logo uses the Ruby gem icon by FlorexLabs.
MIT
