Docscribe
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.
-
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.newexamples- 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 installOr install globally:
gem install docscribeRequires 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
endRun:
echo "...code above..." | docscribe --stdinOutput:
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
endNote
- The tool inserts doc headers before method headers and preserves everything else.
- For methods with a leading Sorbet
sig, docs are inserted above the firstsig. - Class methods show with a dot (
+Demo.bump+,+Demo.internal+). - Methods inside
class << selfunderprivateare 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/@returnwhen 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
parsergem. - On Ruby >= 3.4, Docscribe parses using Prism and translates the tree into the
parsergem'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 libExternal 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
sigdeclarations 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 libYou can pass --sig-dir multiple times:
docscribe -a --rbs --sig-dir sig --sig-dir vendor/sigs libConfig:
rbs:
enabled: true
sig_dirs:
- sig
collapse_generics: falseExample:
# 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
endSorbet
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: falseInline Sorbet example
class 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
endSorbet 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
endWith:
docscribe -a --sorbet --rbi-dir sorbet/rbi libDocscribe 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
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
endGeneric type formatting
Both RBS and Sorbet integrations support collapse_generics.
When disabled:
rbs:
collapse_generics: false
sorbet:
collapse_generics: falseDocscribe preserves generic container details where possible, for example:
Array<String>Hash<Symbol, Integer>
When enabled:
rbs:
collapse_generics: true
sorbet:
collapse_generics: trueDocscribe simplifies container types to their outer names, for example:
ArrayHash
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
@paramand@returntags 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
nilbecome optional types (e.g.Stringornil->String?). - For control flow (
if/case), it unifies branches conservatively.
Rescue-aware returns and @raise
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
Visibility semantics
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
endAPI (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_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_accessor-
Struct.newdeclarations
attr_* example
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
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)
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.
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
@!attributedocs 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] valueWith:
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 initWrite to a custom path:
docscribe init --config config/docscribe.ymlOverwrite if it already exists:
docscribe init --forcePrint the template to stdout:
docscribe init --stdoutCI integration
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 libAggressively rebuild docs:
- name: Rebuild inline docs
run: docscribe -A libComparison 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 docsLimitations
- 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.
Roadmap
- Effective config dump;
- JSON output;
- Overload-aware signature selection;
- Manual
@!attributemerge policy; - Richer inference for common APIs;
- Editor integration.
Contributing
bundle exec rspec
bundle exec rubocopLicense
MIT