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 at the start of each
def/defsline. - Heuristic type inference for params and return values, including conditional returns in rescue branches.
- Optional refresh mode (
--refresh) for regenerating existing method docs. - Ruby 3.4+ syntax supported using Prism translation (see "Parser backend" below).
- Optional RBS integration (
--rbs,--sig-dir) for more accurate@param/@returntypes. - Optional
attr_reader/attr_writer/attr_accessordocumentation via YARD@!attribute(see Configuration).
Common workflows:
-
Generate docs (write changes):
docscribe --write lib -
Check in CI (no changes, fails if docs would change):
docscribe --dry lib -
Refresh/rebaseline docs (regenerate existing doc blocks):
docscribe --write --refresh lib -
Use RBS signatures when available:
docscribe --rbs --sig-dir sig --write lib
Contents
- Docscribe
- Contents
- Installation
- Quick start
- CLI
- Inline behavior
- Refresh mode
- Output markers in CI
- Parser backend (Parser gem vs Prism)
- RBS integration (optional)
- Type inference
- Rescue-aware returns and @raise
- Visibility semantics
- API (library) usage
- Configuration
- Filtering
- 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 at the start of def/defs lines and preserves everything else.
- Class methods show with a dot (
+Demo.bump+,+Demo.internal+). - Methods inside
class << selfunder private are marked@private.
CLI
docscribe [options] [files...]Options:
-
--stdinRead source from STDIN and print with docs inserted. -
--writeRewrite files in place. -
--check,--dryDry-run: exit 1 if any file would change (useful in CI). -
--refreshRegenerate docs: replace existing doc blocks above methods. -
--rbsUse RBS signatures for@param/@returnwhen available (falls back to inference). -
--sig-dir DIRAdd an RBS signature directory (repeatable). Implies--rbs. -
--include PATTERNInclude PATTERN (method id or file path; glob or /regex/). -
--exclude PATTERNExclude PATTERN (method id or file path; glob or /regex/). Exclude wins. -
--include-file PATTERNOnly process files matching PATTERN (glob or /regex/). -
--exclude-file PATTERNSkip files matching PATTERN (glob or /regex/). Exclude wins. -
--config PATHPath to config YAML (default:docscribe.yml). -
--versionPrint version and exit. -
-h,--helpShow help.
Examples:
-
Print to stdout for one file:
docscribe path/to/file.rb
-
Rewrite files in place (ensure a clean working tree):
docscribe --write lib/**/*.rb
-
CI check (fail if docs are missing/stale):
docscribe --dry lib/**/*.rb
-
Refresh docs (regenerate headers/tags):
docscribe --write --refresh lib/**/*.rb
-
Check a directory (Docscribe expands directories to
**/*.rb):docscribe --dry lib
Tip: --dry --refresh is a "refresh dry-run" — it tells you whether regenerating docs would change anything.
Inline behavior
- Inserts comment blocks immediately above def/defs nodes.
- Skips methods that already have a comment directly above them (does not merge into existing comments) unless you pass
--refresh. - Maintains original formatting and constructs; only adds comments.
Refresh mode
With --refresh, Docscribe removes the contiguous comment block immediately above a method (plus intervening blank
lines)
and replaces it with a fresh generated block.
Use with caution (prefer a clean working tree and review diffs).
Output markers in CI
When using --dry, Docscribe prints one character per file:
-
.= file is up-to-date -
F= file would change (missing/stale docs)
When using --write:
-
.= file already OK -
C= file was corrected and rewritten
Docscribe prints a summary at the end and exits non-zero in --dry mode if any file would change.
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 --dry lib
DOCSCRIBE_PARSER_BACKEND=prism bundle exec docscribe --dry libRBS integration (optional)
Docscribe can use RBS signatures to improve @param and @return types.
CLI:
docscribe --rbs --sig-dir sig --write libConfig:
rbs:
enabled: true
sig_dirs: [ "sig" ]
collapse_generics: falseNote
If collapse_generics is set to true, Docscribe will simplify generic types from RBS:
-
Hash<Symbol, Object>->Hash -
Array<String>->Array
Important
If you run Docscribe via Bundler (bundle exec docscribe), you may need to add gem "rbs" to your project's
Gemfile (or use a Gemfile that includes it) so require "rbs" works reliably. If RBS can't be loaded, Docscribe falls
back to inference.
Type inference
Heuristics (best-effort).
Parameters:
-
*args->Array -
**kwargs->Hash -
&block->Proc - keyword args:
- verbose:
true->Boolean - options:
{}->Hash - kw: (no default) ->
Object
- verbose:
- 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.,
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/fail also 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
# Insert docs (skip methods that already have a comment above)
out = Docscribe::InlineRewriter.insert_comments(code)
puts out
# Replace existing comment blocks above methods (equivalent to CLI --refresh)
out2 = Docscribe::InlineRewriter.insert_comments(code, rewrite: true)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:
docscribe --dry --exclude '*#initialize' lib
docscribe --dry --exclude-file 'spec' lib specAttribute macros (attr_*)
Docscribe can generate YARD @!attribute directives above attr_reader, attr_writer, and attr_accessor.
Enable it:
emit:
attributes: trueExample:
class User
attr_reader :name
private
attr_accessor :token
endBecomes:
class User
# @!attribute [r] name
# @return [Object]
attr_reader :name
private
# @!attribute [rw] token
# @private
# @return [Object]
# @param value [Object]
attr_accessor :token
endNote
- 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.
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 change:
- name: Check inline docs
run: docscribe --dry lib/**/*.rbAuto-fix before test stage:
- name: Insert inline docs
run: docscribe --write lib/**/*.rbRefresh mode (regenerate existing method docs):
- name: Refresh inline docs
run: docscribe --write --refresh lib/**/*.rbComparison 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
-
Does not merge into existing comments; in normal mode, a method with a comment directly above it is skipped. Use
--refreshto regenerate. - Type inference is heuristic. Complex flows and meta-programming will fall back to
Objector best-effort types. - Inline rewrite is textual; ensure a clean working tree before using
--writeor--refresh.
Roadmap
- Merge tags into existing docstrings (opt-in).
- Recognize common APIs for return inference (
Time.now,File.read,JSON.parse). - Configurable rules and per-project exclusions.
- Editor integration for on-save inline docs.
Contributing
bundle exec rspec
bundle exec rubocopLicense
MIT