docscribe 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +692 -180
  3. data/exe/docscribe +2 -74
  4. data/lib/docscribe/cli/config_builder.rb +62 -0
  5. data/lib/docscribe/cli/init.rb +58 -0
  6. data/lib/docscribe/cli/options.rb +204 -0
  7. data/lib/docscribe/cli/run.rb +415 -0
  8. data/lib/docscribe/cli.rb +31 -0
  9. data/lib/docscribe/config/defaults.rb +71 -0
  10. data/lib/docscribe/config/emit.rb +126 -0
  11. data/lib/docscribe/config/filtering.rb +160 -0
  12. data/lib/docscribe/config/loader.rb +59 -0
  13. data/lib/docscribe/config/rbs.rb +51 -0
  14. data/lib/docscribe/config/sorbet.rb +87 -0
  15. data/lib/docscribe/config/sorting.rb +23 -0
  16. data/lib/docscribe/config/template.rb +176 -0
  17. data/lib/docscribe/config/utils.rb +102 -0
  18. data/lib/docscribe/config.rb +20 -230
  19. data/lib/docscribe/infer/ast_walk.rb +28 -0
  20. data/lib/docscribe/infer/constants.rb +11 -0
  21. data/lib/docscribe/infer/literals.rb +55 -0
  22. data/lib/docscribe/infer/names.rb +43 -0
  23. data/lib/docscribe/infer/params.rb +62 -0
  24. data/lib/docscribe/infer/raises.rb +68 -0
  25. data/lib/docscribe/infer/returns.rb +171 -0
  26. data/lib/docscribe/infer.rb +110 -259
  27. data/lib/docscribe/inline_rewriter/collector.rb +845 -0
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +605 -0
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
  32. data/lib/docscribe/inline_rewriter.rb +604 -425
  33. data/lib/docscribe/parsing.rb +120 -0
  34. data/lib/docscribe/types/provider_chain.rb +37 -0
  35. data/lib/docscribe/types/rbs/provider.rb +213 -0
  36. data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
  37. data/lib/docscribe/types/signature.rb +65 -0
  38. data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
  39. data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
  40. data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
  41. data/lib/docscribe/version.rb +1 -1
  42. data/lib/docscribe.rb +1 -0
  43. metadata +85 -17
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -11
  46. data/.rubocop_todo.yml +0 -73
  47. data/CODE_OF_CONDUCT.md +0 -84
  48. data/Gemfile +0 -6
  49. data/Gemfile.lock +0 -73
  50. data/Rakefile +0 -12
  51. data/rakelib/docs.rake +0 -73
  52. data/stingray_docs_internal.gemspec +0 -41
data/README.md CHANGED
@@ -4,32 +4,72 @@
4
4
  [![RubyGems Downloads](https://img.shields.io/gem/dt/docscribe.svg)](https://rubygems.org/gems/docscribe)
5
5
  [![CI](https://github.com/unurgunite/docscribe/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/unurgunite/docscribe/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/github/license/unurgunite/docscribe.svg)](https://github.com/unurgunite/docscribe/blob/master/LICENSE.txt)
7
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-blue.svg)](#installation)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-blue.svg)](#installation)
8
8
 
9
- Generate inline, YARD-style documentation comments for Ruby methods by analyzing your code's AST. Docscribe inserts doc
10
- headers before method definitions, infers parameter and return types (including rescue-aware returns), and respects Ruby
11
- visibility semantics — without using YARD to parse.
9
+ Generate inline, YARD-style documentation comments for Ruby methods by analyzing your code's AST.
12
10
 
13
- - No AST reprinting. Your original code, formatting, and constructs (like `class << self`, `heredocs`, `%i[]`) are preserved.
14
- - Inline-first. Comments are inserted surgically at the start of each `def`/`defs` line.
15
- - Heuristic type inference for params and return values, including conditional returns in rescue branches.
16
- - Optional rewrite mode for regenerating existing method docs.
11
+ Docscribe inserts doc headers before method definitions, infers parameter and return types (including rescue-aware
12
+ returns), and respects Ruby visibility semantics without using YARD to parse.
17
13
 
18
- Why not YARD? We started with YARD's parser, but switched to an AST-based in-place rewriter for maximum preservation of
19
- source structure and exact control over Ruby semantics.
14
+ - No AST reprinting. Your original code, formatting, and constructs (like `class << self`, `heredocs`, `%i[]`) are
15
+ preserved.
16
+ - Inline-first. Comments are inserted before method headers without reprinting the AST. For methods with a leading
17
+ Sorbet `sig`, new docs are inserted above the first `sig`.
18
+ - Heuristic type inference for params and return values, including conditional returns in rescue branches.
19
+ - Safe and aggressive update modes:
20
+ - safe mode inserts missing docs, merges existing doc-like blocks, and normalizes sortable tags;
21
+ - aggressive mode rebuilds existing doc blocks.
22
+ - Ruby 3.4+ syntax supported using Prism translation (see "Parser backend" below).
23
+ - Optional external type integrations:
24
+ - RBS via `--rbs` / `--sig-dir`;
25
+ - Sorbet via inline `sig` declarations and RBI files with `--sorbet` / `--rbi-dir`.
26
+ - Optional `@!attribute` generation for:
27
+ - `attr_reader` / `attr_writer` / `attr_accessor`;
28
+ - `Struct.new` declarations in both constant-assigned and class-based styles.
29
+
30
+ Common workflows:
31
+
32
+ - Inspect what safe doc updates would be applied: `docscribe lib`
33
+ - Apply safe doc updates: `docscribe -a lib`
34
+ - Apply aggressive doc updates: `docscribe -A lib`
35
+ - Use RBS signatures when available: `docscribe -a --rbs --sig-dir sig lib`
36
+ - Use Sorbet signatures when available: `docscribe -a --sorbet --rbi-dir sorbet/rbi lib`
37
+
38
+ ## Contents
20
39
 
21
40
  * [Docscribe](#docscribe)
41
+ * [Contents](#contents)
22
42
  * [Installation](#installation)
23
43
  * [Quick start](#quick-start)
24
44
  * [CLI](#cli)
25
- * [Inline behavior](#inline-behavior)
26
- * [Rewrite mode](#rewrite-mode)
45
+ * [Options](#options)
46
+ * [Examples](#examples)
47
+ * [Update strategies](#update-strategies)
48
+ * [Safe strategy](#safe-strategy)
49
+ * [Aggressive strategy](#aggressive-strategy)
50
+ * [Output markers](#output-markers)
51
+ * [Parser backend (Parser gem vs Prism)](#parser-backend-parser-gem-vs-prism)
52
+ * [External type integrations (optional)](#external-type-integrations-optional)
53
+ * [RBS](#rbs)
54
+ * [Sorbet](#sorbet)
55
+ * [Inline Sorbet example](#inline-sorbet-example)
56
+ * [Sorbet RBI example](#sorbet-rbi-example)
57
+ * [Sorbet comment placement](#sorbet-comment-placement)
58
+ * [Generic type formatting](#generic-type-formatting)
59
+ * [Notes and fallback behavior](#notes-and-fallback-behavior)
27
60
  * [Type inference](#type-inference)
28
61
  * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
29
62
  * [Visibility semantics](#visibility-semantics)
30
63
  * [API (library) usage](#api-library-usage)
31
64
  * [Configuration](#configuration)
32
- * [CLI](#cli-1)
65
+ * [Filtering](#filtering)
66
+ * [`attr_*` example](#attr_-example)
67
+ * [`Struct.new` examples](#structnew-examples)
68
+ * [Constant-assigned struct](#constant-assigned-struct)
69
+ * [Class-based struct](#class-based-struct)
70
+ * [Merge behavior](#merge-behavior)
71
+ * [Param tag style](#param-tag-style)
72
+ * [Create a starter config](#create-a-starter-config)
33
73
  * [CI integration](#ci-integration)
34
74
  * [Comparison to YARD's parser](#comparison-to-yards-parser)
35
75
  * [Limitations](#limitations)
@@ -42,7 +82,7 @@ source structure and exact control over Ruby semantics.
42
82
  Add to your Gemfile:
43
83
 
44
84
  ```ruby
45
- gem 'docscribe'
85
+ gem "docscribe"
46
86
  ```
47
87
 
48
88
  Then:
@@ -57,7 +97,7 @@ Or install globally:
57
97
  gem install docscribe
58
98
  ```
59
99
 
60
- Requires Ruby 3.0+.
100
+ Requires Ruby 2.7+.
61
101
 
62
102
  ## Quick start
63
103
 
@@ -97,7 +137,6 @@ echo "...code above..." | docscribe --stdin
97
137
  Output:
98
138
 
99
139
  ```ruby
100
-
101
140
  class Demo
102
141
  # +Demo#foo+ -> Integer
103
142
  #
@@ -145,11 +184,11 @@ class Demo
145
184
  end
146
185
  ```
147
186
 
148
- Notes:
149
-
150
- - The tool inserts doc headers at the start of def/defs lines and preserves everything else.
151
- - Class methods show with a dot (`+Demo.bump+`, `+Demo.internal+`).
152
- - Methods inside `class << self` under private are marked `@private.`
187
+ > [!NOTE]
188
+ > - The tool inserts doc headers before method headers and preserves everything else.
189
+ > - For methods with a leading Sorbet `sig`, docs are inserted above the first `sig`.
190
+ > - Class methods show with a dot (`+Demo.bump+`, `+Demo.internal+`).
191
+ > - Methods inside `class << self` under `private` are marked `@private`.
153
192
 
154
193
  ## CLI
155
194
 
@@ -157,158 +196,492 @@ Notes:
157
196
  docscribe [options] [files...]
158
197
  ```
159
198
 
160
- Options:
199
+ Docscribe has three main ways to run:
200
+
201
+ - **Inspect mode** (default): checks what safe doc updates would be applied and exits non-zero if files need changes.
202
+ - **Safe autocorrect** (`-a`, `--autocorrect`): writes safe, non-destructive updates in place.
203
+ - **Aggressive autocorrect** (`-A`, `--autocorrect-all`): rewrites existing doc blocks more aggressively.
204
+ - **STDIN mode** (`--stdin`): reads Ruby source from STDIN and prints rewritten source to STDOUT.
205
+
206
+ If you pass no files and don’t use `--stdin`, Docscribe processes the current directory recursively.
207
+
208
+ ### Options
209
+
210
+ - `-a`, `--autocorrect`
211
+ Apply safe doc updates in place.
212
+
213
+ - `-A`, `--autocorrect-all`
214
+ Apply aggressive doc updates in place.
215
+
216
+ - `--stdin`
217
+ Read source from STDIN and print rewritten output.
218
+
219
+ - `--verbose`
220
+ Print per-file actions.
221
+
222
+ - `--explain`
223
+ Show detailed reasons for each file that would change.
224
+
225
+ - `--rbs`
226
+ Use RBS signatures for `@param`/`@return` when available (falls back to inference).
227
+
228
+ - `--sig-dir DIR`
229
+ Add an RBS signature directory (repeatable). Implies `--rbs`.
230
+
231
+ - `--include PATTERN`
232
+ Include PATTERN (method id or file path; glob or `/regex/`).
233
+
234
+ - `--exclude PATTERN`
235
+ Exclude PATTERN (method id or file path; glob or `/regex/`). Exclude wins.
236
+
237
+ - `--include-file PATTERN`
238
+ Only process files matching PATTERN (glob or `/regex/`).
239
+
240
+ - `--exclude-file PATTERN`
241
+ Skip files matching PATTERN (glob or `/regex/`). Exclude wins.
242
+
243
+ - `-C`, `--config PATH`
244
+ Path to config YAML (default: `docscribe.yml`).
245
+
246
+ - `-v`, `--version`
247
+ Print version and exit.
161
248
 
162
- - `--stdin` Read source from STDIN and print with docs inserted.
163
- - `--write` Rewrite files in place (inline mode).
164
- - `--check` Dry-run: exit 1 if any file would change (useful in CI).
165
- - `--rewrite` Replace any existing comment block above methods (see “Rewrite mode” below).
166
- - `--version` Print version and exit.
167
- - `-h`, `--help` Show help.
249
+ - `-h`, `--help`
250
+ Show help.
168
251
 
169
- Examples:
252
+ ### Examples
170
253
 
171
- - Print to stdout for one file:
254
+ - Inspect a directory:
172
255
  ```shell
173
- docscribe path/to/file.rb
256
+ docscribe lib
174
257
  ```
175
- - Rewrite files in place (ensure a clean working tree):
258
+
259
+ - Apply safe updates:
176
260
  ```shell
177
- docscribe --write lib/**/*.rb
261
+ docscribe -a lib
178
262
  ```
179
- - CI check (fail if docs are missing/stale):
263
+
264
+ - Apply aggressive updates:
180
265
  ```shell
181
- docscribe --check lib/**/*.rb
266
+ docscribe -A lib
182
267
  ```
183
- - Rewrite existing doc blocks above methods (regenerate headers/tags):
268
+
269
+ - Preview output for a single file via STDIN:
184
270
  ```shell
185
- docscribe --rewrite --write lib/**/*.rb
271
+ cat path/to/file.rb | docscribe --stdin
186
272
  ```
187
273
 
188
- ## Inline behavior
274
+ - Use RBS signatures:
275
+ ```shell
276
+ docscribe -a --rbs --sig-dir sig lib
277
+ ```
189
278
 
190
- - Inserts comment blocks immediately above def/defs nodes.
191
- - Skips methods that already have a comment directly above them (does not merge into existing comments) unless you pass
192
- `--rewrite`.
193
- - Maintains original formatting and constructs; only adds comments.
279
+ - Show detailed reasons for files that would change:
280
+ ```shell
281
+ docscribe --verbose --explain lib
282
+ ```
194
283
 
195
- ### Rewrite mode
284
+ ## Update strategies
196
285
 
197
- - With `--rewrite`, Docscribe will remove the contiguous comment block immediately above a method (plus intervening
198
- blank
199
- lines) and replace it with a fresh generated block.
200
- - This is useful to refresh docs across a codebase after improving inference or rules.
201
- - Use with caution (prefer a clean working tree and review diffs).
286
+ Docscribe supports two update strategies: **safe** and **aggressive**.
202
287
 
203
- ## Type inference
288
+ ### Safe strategy
204
289
 
205
- Heuristics (best-effort):
290
+ Used by:
206
291
 
207
- Parameters:
292
+ - default inspect mode: `docscribe lib`
293
+ - safe write mode: `docscribe -a lib`
208
294
 
209
- - `*args` -> `Array`
210
- - `**kwargs` -> `Hash`
211
- - `&block` -> `Proc`
212
- - keyword args:
213
- - verbose: `true` -> `Boolean`
214
- - options: `{}` -> `Hash`
215
- - kw: (no default) -> `Object`
216
- - positional defaults:
217
- - `42` -> `Integer`, `1.0` -> `Float`, `'x'` -> `String`, `:ok` -> `Symbol`
218
- - `[]` -> `Array`, `{}` -> `Hash`, `/x/` -> `Regexp`, `true`/`false` -> `Boolean`, `nil` -> `nil`
295
+ Safe strategy:
219
296
 
220
- Return values:
297
+ - inserts docs for undocumented methods
298
+ - merges missing tags into existing **doc-like** blocks
299
+ - normalizes configurable tag order inside sortable tag runs
300
+ - preserves existing prose and comments where possible
221
301
 
222
- - For simple bodies, Docscribe looks at the last expression or explicit return:
223
- - `42` -> `Integer`
224
- - `:ok` -> `Symbol`
225
- - Unions with nil become optional types (e.g., `String` or `nil` -> `String?`).
226
- - For control flow (`if`/`case`), it unifies branches conservatively.
302
+ This is the recommended day-to-day mode.
227
303
 
228
- ## Rescue-aware returns and @raise
304
+ ### Aggressive strategy
229
305
 
230
- Docscribe detects exceptions and rescue branches:
306
+ Used by:
231
307
 
232
- - Rescue exceptions become `@raise` tags:
233
- - `rescue Foo, Bar` -> `@raise [Foo]` and `@raise [Bar]`
234
- - bare rescue -> `@raise [StandardError]`
235
- - (optional) explicit raise/fail also adds a tag (`raise Foo` -> `@raise [Foo]`, `raise` ->
236
- `@raise [StandardError]`).
308
+ - aggressive write mode: `docscribe -A lib`
237
309
 
238
- - Conditional return types for rescue branches:
239
- - Docscribe adds `@return [Type]` if `ExceptionA`, `ExceptionB` for each rescue clause.
310
+ Aggressive strategy:
311
+
312
+ - rebuilds existing doc blocks
313
+ - replaces existing generated documentation more fully
314
+ - is more invasive than safe mode
315
+
316
+ Use it when you want to rebaseline or regenerate docs wholesale.
317
+
318
+ ### Output markers
319
+
320
+ In inspect mode, Docscribe prints one character per file:
321
+
322
+ - `.` = file is up to date
323
+ - `F` = file would change
324
+ - `E` = file had an error
325
+
326
+ In write modes:
327
+
328
+ - `.` = file already OK
329
+ - `C` = file was updated
330
+ - `E` = file had an error
331
+
332
+ With `--verbose`, Docscribe prints per-file statuses instead.
333
+
334
+ With `--explain`, Docscribe also prints detailed reasons, such as:
335
+
336
+ - missing `@param`
337
+ - missing `@return`
338
+ - missing module_function note
339
+ - unsorted tags
340
+
341
+ ## Parser backend (Parser gem vs Prism)
342
+
343
+ Docscribe internally works with `parser`-gem-compatible AST nodes and `Parser::Source::*` objects (so it can use
344
+ `Parser::Source::TreeRewriter` without changing formatting).
345
+
346
+ - On Ruby **<= 3.3**, Docscribe parses using the `parser` gem.
347
+ - On Ruby **>= 3.4**, Docscribe parses using **Prism** and translates the tree into the `parser` gem's AST.
348
+
349
+ You can force a backend with an environment variable:
350
+
351
+ ```shell
352
+ DOCSCRIBE_PARSER_BACKEND=parser bundle exec docscribe lib
353
+ DOCSCRIBE_PARSER_BACKEND=prism bundle exec docscribe lib
354
+ ```
355
+
356
+ ## External type integrations (optional)
357
+
358
+ Docscribe can improve generated `@param` and `@return` types by reading external signatures instead of relying only on
359
+ AST inference.
360
+
361
+ > [!IMPORTANT]
362
+ > When external type information is available, Docscribe resolves signatures in this order:
363
+ > - inline Sorbet `sig` declarations in the current Ruby source;
364
+ > - Sorbet RBI files;
365
+ > - RBS files;
366
+ > - AST inference fallback.
367
+ >
368
+ > If an external signature cannot be loaded or parsed, Docscribe falls back to normal inference instead of failing.
369
+
370
+ ### RBS
371
+
372
+ Docscribe can read method signatures from `.rbs` files and use them to generate more accurate parameter and return
373
+ types.
374
+
375
+ CLI:
376
+
377
+ ```shell
378
+ docscribe -a --rbs --sig-dir sig lib
379
+ ```
380
+
381
+ You can pass `--sig-dir` multiple times:
382
+
383
+ ```shell
384
+ docscribe -a --rbs --sig-dir sig --sig-dir vendor/sigs lib
385
+ ```
386
+
387
+ Config:
388
+
389
+ ```yaml
390
+ rbs:
391
+ enabled: true
392
+ sig_dirs:
393
+ - sig
394
+ collapse_generics: false
395
+ ```
240
396
 
241
397
  Example:
242
398
 
243
399
  ```ruby
400
+ # Ruby source
401
+ class Demo
402
+ def foo(verbose:, count:)
403
+ "body says String"
404
+ end
405
+ end
406
+ ```
244
407
 
245
- class X
246
- def a
247
- 42
248
- rescue Foo, Bar
249
- "fallback"
408
+ ```ruby.rbs
409
+ # sig/demo.rbs
410
+ class Demo
411
+ def foo: (verbose: bool, count: Integer) -> Integer
412
+ end
413
+ ```
414
+
415
+ Generated docs will prefer the RBS signature over inferred Ruby types:
416
+
417
+ ```ruby
418
+
419
+ class Demo
420
+ # +Demo#foo+ -> Integer
421
+ #
422
+ # Method documentation.
423
+ #
424
+ # @param [Boolean] verbose Param documentation.
425
+ # @param [Integer] count Param documentation.
426
+ # @return [Integer]
427
+ def foo(verbose:, count:)
428
+ 'body says String'
250
429
  end
430
+ end
431
+ ```
432
+
433
+ ### Sorbet
434
+
435
+ Docscribe can also read Sorbet signatures from:
436
+
437
+ - inline `sig` declarations in Ruby source
438
+ - RBI files
439
+
440
+ CLI:
251
441
 
252
- def b
253
- risky
254
- rescue
255
- "n"
442
+ ```shell
443
+ docscribe -a --sorbet lib
444
+ ```
445
+
446
+ With RBI directories:
447
+
448
+ ```shell
449
+ docscribe -a --sorbet --rbi-dir sorbet/rbi lib
450
+ ```
451
+
452
+ You can pass `--rbi-dir` multiple times:
453
+
454
+ ```shell
455
+ docscribe -a --sorbet --rbi-dir sorbet/rbi --rbi-dir rbi lib
456
+ ```
457
+
458
+ Config:
459
+
460
+ ```yaml
461
+ sorbet:
462
+ enabled: true
463
+ rbi_dirs:
464
+ - sorbet/rbi
465
+ - rbi
466
+ collapse_generics: false
467
+ ```
468
+
469
+ ### Inline Sorbet example
470
+
471
+ ```ruby
472
+ class Demo
473
+ extend T::Sig
474
+
475
+ sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
476
+ def foo(verbose:, count:)
477
+ 'body says String'
256
478
  end
257
479
  end
258
480
  ```
259
481
 
260
- Becomes:
482
+ Docscribe will use the Sorbet signature instead of the inferred body type:
261
483
 
262
484
  ```ruby
485
+ class Demo
486
+ extend T::Sig
263
487
 
264
- class X
265
- # +X#a+ -> Integer
488
+ # +Demo#foo+ -> Integer
266
489
  #
267
490
  # Method documentation.
268
491
  #
269
- # @raise [Foo]
270
- # @raise [Bar]
492
+ # @param [Boolean] verbose Param documentation.
493
+ # @param [Integer] count Param documentation.
271
494
  # @return [Integer]
272
- # @return [String] if Foo, Bar
273
- def a
274
- 42
275
- rescue Foo, Bar
276
- "fallback"
495
+ sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
496
+ def foo(verbose:, count:)
497
+ 'body says String'
498
+ end
499
+ end
500
+ ```
501
+
502
+ ### Sorbet RBI example
503
+
504
+ ```ruby
505
+ # Ruby source
506
+ class Demo
507
+ def foo(verbose:, count:)
508
+ 'body says String'
509
+ end
510
+ end
511
+ ```
512
+
513
+ ```ruby
514
+ # sorbet/rbi/demo.rbi
515
+ class Demo
516
+ extend T::Sig
517
+
518
+ sig { params(verbose: T::Boolean, count: Integer).returns(Integer) }
519
+ def foo(verbose:, count:); end
520
+ end
521
+ ```
522
+
523
+ With:
524
+
525
+ ```shell
526
+ docscribe -a --sorbet --rbi-dir sorbet/rbi lib
527
+ ```
528
+
529
+ Docscribe will use the RBI signature for generated docs.
530
+
531
+ ### Sorbet comment placement
532
+
533
+ For methods with a leading Sorbet `sig`, Docscribe treats the signature as part of the method header.
534
+
535
+ That means:
536
+
537
+ - new docs are inserted **above the first `sig`**
538
+ - existing docs **above the `sig`** are recognized and merged
539
+ - existing legacy docs **between `sig` and `def`** are also recognized
540
+
541
+ Example input:
542
+
543
+ ```ruby
544
+ # demo.rb
545
+ class Demo
546
+ extend T::Sig
547
+
548
+ sig { returns(Integer) }
549
+ def foo
550
+ 1
277
551
  end
552
+ end
553
+ ```
554
+
555
+ Example output:
556
+
557
+ ```ruby
558
+ # demo.rb
559
+ class Demo
560
+ extend T::Sig
278
561
 
279
- # +X#b+ -> Object
562
+ # +Demo#foo+ -> Integer
280
563
  #
281
564
  # Method documentation.
282
565
  #
283
- # @raise [StandardError]
284
- # @return [Object]
285
- # @return [String] if StandardError
286
- def b
287
- risky
288
- rescue
289
- "n"
566
+ # @return [Integer]
567
+ sig { returns(Integer) }
568
+ def foo
569
+ 1
290
570
  end
291
571
  end
292
572
  ```
293
573
 
574
+ ### Generic type formatting
575
+
576
+ Both RBS and Sorbet integrations support `collapse_generics`.
577
+
578
+ When disabled:
579
+
580
+ ```yaml
581
+ rbs:
582
+ collapse_generics: false
583
+
584
+ sorbet:
585
+ collapse_generics: false
586
+ ```
587
+
588
+ Docscribe preserves generic container details where possible, for example:
589
+
590
+ - `Array<String>`
591
+ - `Hash<Symbol, Integer>`
592
+
593
+ When enabled:
594
+
595
+ ```yaml
596
+ rbs:
597
+ collapse_generics: true
598
+
599
+ sorbet:
600
+ collapse_generics: true
601
+ ```
602
+
603
+ Docscribe simplifies container types to their outer names, for example:
604
+
605
+ - `Array`
606
+ - `Hash`
607
+
608
+ ### Notes and fallback behavior
609
+
610
+ - External signature support is the **best effort**.
611
+ - If a signature source cannot be loaded or parsed, Docscribe falls back to AST inference.
612
+ - RBS and Sorbet integrations are used only to improve generated types; Docscribe still rewrites Ruby source directly.
613
+ - Sorbet support does not require changing your documentation style — it only improves generated `@param` and `@return`
614
+ tags when signatures are available.
615
+
616
+ ## Type inference
617
+
618
+ Heuristics (best-effort).
619
+
620
+ Parameters:
621
+
622
+ - `*args` -> `Array`
623
+ - `**kwargs` -> `Hash`
624
+ - `&block` -> `Proc`
625
+ - keyword args:
626
+ - `verbose: true` -> `Boolean`
627
+ - `options: {}` -> `Hash`
628
+ - `kw:` (no default) -> `Object`
629
+ - positional defaults:
630
+ - `42` -> `Integer`, `1.0` -> `Float`, `'x'` -> `String`, `:ok` -> `Symbol`
631
+ - `[]` -> `Array`, `{}` -> `Hash`, `/x/` -> `Regexp`, `true`/`false` -> `Boolean`, `nil` -> `nil`
632
+
633
+ Return values:
634
+
635
+ - For simple bodies, Docscribe looks at the last expression or explicit `return`.
636
+ - Unions with `nil` become optional types (e.g. `String` or `nil` -> `String?`).
637
+ - For control flow (`if`/`case`), it unifies branches conservatively.
638
+
639
+ ## Rescue-aware returns and @raise
640
+
641
+ Docscribe detects exceptions and rescue branches:
642
+
643
+ - Rescue exceptions become `@raise` tags:
644
+ - `rescue Foo, Bar` -> `@raise [Foo]` and `@raise [Bar]`
645
+ - bare rescue -> `@raise [StandardError]`
646
+ - explicit `raise`/`fail` also adds a tag (`raise Foo` -> `@raise [Foo]`, `raise` -> `@raise [StandardError]`)
647
+
648
+ - Conditional return types for rescue branches:
649
+ - Docscribe adds `@return [Type] if ExceptionA, ExceptionB` for each rescue clause
650
+
294
651
  ## Visibility semantics
295
652
 
296
653
  We match Ruby's behavior:
297
654
 
298
655
  - A bare `private`/`protected`/`public` in a class/module body affects instance methods only.
299
656
  - Inside `class << self`, a bare visibility keyword affects class methods only.
300
- - `def self.x` in a class body remains `public` unless `private_class_method` is used or it's inside `class << self` under
301
- `private`.
657
+ - `def self.x` in a class body remains `public` unless `private_class_method` is used, or it's inside `class << self`
658
+ under `private`.
302
659
 
303
660
  Inline tags:
304
661
 
305
662
  - `@private` is added for methods that are private in context.
306
663
  - `@protected` is added similarly for protected methods.
307
664
 
665
+ > [!IMPORTANT]
666
+ > `module_function`: Docscribe documents methods affected by `module_function` as module methods (`M.foo`) rather than
667
+ > instance methods (`M#foo`), because that is usually the callable/public API. If a method was previously private as an
668
+ > instance method, Docscribe will avoid marking the generated docs as `@private` after it is promoted to a module
669
+ > method.
670
+
671
+ ```ruby
672
+ module M
673
+ private
674
+
675
+ def foo; end
676
+
677
+ module_function :foo
678
+ end
679
+ ```
680
+
308
681
  ## API (library) usage
309
682
 
310
683
  ```ruby
311
- require 'docscribe/inline_rewriter'
684
+ require "docscribe/inline_rewriter"
312
685
 
313
686
  code = <<~RUBY
314
687
  class Demo
@@ -317,135 +690,274 @@ code = <<~RUBY
317
690
  end
318
691
  RUBY
319
692
 
320
- # Insert docs (skip methods that already have a comment above)
693
+ # Basic insertion behavior
321
694
  out = Docscribe::InlineRewriter.insert_comments(code)
322
695
  puts out
323
696
 
324
- # Replace existing comment blocks above methods
325
- out2 = Docscribe::InlineRewriter.insert_comments(code, rewrite: true)
697
+ # Safe merge / normalization of existing doc-like blocks
698
+ out2 = Docscribe::InlineRewriter.insert_comments(code, strategy: :safe)
699
+
700
+ # Aggressive rebuild of existing doc blocks (similar to CLI -A)
701
+ out3 = Docscribe::InlineRewriter.insert_comments(code, strategy: :aggressive)
326
702
  ```
327
703
 
328
704
  ## Configuration
329
705
 
330
- Docscribe can be configured via a YAML file (docscribe.yml by default, or pass --config PATH).
706
+ Docscribe can be configured via a YAML file (`docscribe.yml` by default, or pass `--config PATH`).
707
+
708
+ ### Filtering
709
+
710
+ Docscribe can filter both *files* and *methods*.
711
+
712
+ File filtering (recommended for excluding specs, vendor code, etc.):
713
+
714
+ ```yaml
715
+ filter:
716
+ files:
717
+ exclude: [ "spec" ]
718
+ ```
719
+
720
+ Method filtering matches method ids like:
721
+
722
+ - `MyModule::MyClass#instance_method`
723
+ - `MyModule::MyClass.class_method`
331
724
 
332
725
  Example:
333
726
 
727
+ ```yaml
728
+ filter:
729
+ exclude:
730
+ - "*#initialize"
731
+ ```
732
+
733
+ CLI overrides are available too:
734
+
735
+ ```shell
736
+ # Method filtering (matches method ids like A#foo / A.bar)
737
+ docscribe --exclude '*#initialize' lib
738
+ docscribe --include '/^MyModule::.*#(foo|bar)$/' lib
739
+
740
+ # File filtering (matches paths relative to the project root)
741
+ docscribe --exclude-file 'spec' lib spec
742
+ docscribe --exclude-file '/^spec\//' lib
743
+ ```
744
+
745
+ > [!NOTE]
746
+ > `/regex/` passed to `--include`/`--exclude` is treated as a **method-id** pattern. Use `--include-file` /
747
+ `--exclude-file` for file regex filters.
748
+
749
+ Enable attribute-style documentation generation with:
750
+
334
751
  ```yaml
335
752
  emit:
336
- header: true # controls "# +Class#method+ -> Type"
337
- param_tags: true # include @param lines
338
- return_tag: true # include normal @return
339
- visibility_tags: true # include @private/@protected
340
- raise_tags: true # include @raise [Error]
341
- rescue_conditional_returns: true # include "@return [...] if Exception"
753
+ attributes: true
754
+ ```
755
+
756
+ When enabled, Docscribe can generate YARD `@!attribute` docs for:
757
+
758
+ - `attr_reader`
759
+ - `attr_writer`
760
+ - `attr_accessor`
761
+ - `Struct.new` declarations
762
+
763
+ ### `attr_*` example
764
+
765
+ > [!NOTE]
766
+ > - Attribute docs are inserted above the `attr_*` call, not above generated methods (since they don’t exist as `def`
767
+ nodes).
768
+ > - If RBS is enabled, Docscribe will try to use the RBS return type of the reader method as the attribute type.
769
+
770
+ ```ruby
771
+ class User
772
+ attr_accessor :name
773
+ end
774
+ ```
775
+
776
+ Generated docs:
777
+
778
+ ```ruby
779
+ class User
780
+ # @!attribute [rw] name
781
+ # @return [Object]
782
+ # @param [Object] value
783
+ attr_accessor :name
784
+ end
785
+ ```
786
+
787
+ ### `Struct.new` examples
788
+
789
+ Docscribe supports both common `Struct.new` declaration styles.
790
+
791
+ #### Constant-assigned struct
792
+
793
+ ```ruby
794
+ User = Struct.new(:name, :email, keyword_init: true)
795
+ ```
796
+
797
+ Generated docs:
798
+
799
+ ```ruby
800
+ # @!attribute [rw] name
801
+ # @return [Object]
802
+ # @param [Object] value
803
+ #
804
+ # @!attribute [rw] email
805
+ # @return [Object]
806
+ # @param [Object] value
807
+ User = Struct.new(:name, :email, keyword_init: true)
808
+ ```
809
+
810
+ #### Class-based struct
811
+
812
+ ```ruby
813
+ class User < Struct.new(:name, :email, keyword_init: true)
814
+ end
815
+ ```
816
+
817
+ Generated docs:
818
+
819
+ ```ruby
820
+ # @!attribute [rw] name
821
+ # @return [Object]
822
+ # @param [Object] value
823
+ #
824
+ # @!attribute [rw] email
825
+ # @return [Object]
826
+ # @param [Object] value
827
+ class User < Struct.new(:name, :email, keyword_init: true)
828
+ end
829
+ ```
830
+
831
+ Docscribe preserves the original declaration style and does not rewrite one form into the other.
832
+
833
+ ### Merge behavior
342
834
 
835
+ Struct member docs use the same attribute documentation pipeline as `attr_*` macros, which means they participate in the
836
+ normal safe/aggressive rewrite flow.
837
+
838
+ In safe mode, Docscribe can:
839
+
840
+ - insert full `@!attribute` docs when no doc-like block exists
841
+ - append missing struct member docs into an existing doc-like block
842
+
843
+ ### Param tag style
844
+
845
+ Generated writer-style attribute docs respect `doc.param_tag_style`.
846
+
847
+ For example, with:
848
+
849
+ ```yaml
343
850
  doc:
344
- default_message: "Method documentation."
851
+ param_tag_style: "type_name"
852
+ ```
853
+
854
+ writer params are emitted as:
855
+
856
+ ```ruby
857
+ # @param [Object] value
858
+ ```
859
+
860
+ With:
861
+
862
+ ```yaml
863
+ doc:
864
+ param_tag_style: "name_type"
865
+ ```
866
+
867
+ they are emitted as:
868
+
869
+ ```ruby
870
+ # @param value [Object]
871
+ ```
872
+
873
+ ### Create a starter config
345
874
 
346
- methods:
347
- instance:
348
- public:
349
- return_tag: true
350
- default_message: "Public API. Please document purpose and params."
351
- class:
352
- private:
353
- return_tag: false
875
+ Create `docscribe.yml` in the current directory:
354
876
 
355
- inference:
356
- fallback_type: "Object"
357
- nil_as_optional: true
358
- treat_options_keyword_as_hash: true
877
+ ```shell
878
+ docscribe init
359
879
  ```
360
880
 
361
- - emit.* toggles control which tags are emitted globally.
362
- - methods.<scope>.<visibility> allows per-method overrides:
363
- - return_tag: true/false
364
- - default_message: override the message for that bucket
365
- - inference.* tunes type inference defaults.
881
+ Write to a custom path:
882
+
883
+ ```shell
884
+ docscribe init --config config/docscribe.yml
885
+ ```
366
886
 
367
- ### CLI
887
+ Overwrite if it already exists:
368
888
 
369
- ```bash
370
- docscribe --config docscribe.yml --write lib/**/*.rb
889
+ ```shell
890
+ docscribe init --force
891
+ ```
892
+
893
+ Print the template to stdout:
894
+
895
+ ```shell
896
+ docscribe init --stdout
371
897
  ```
372
898
 
373
899
  ## CI integration
374
900
 
375
- Fail the build if files would change:
901
+ Fail the build if files would need safe updates:
376
902
 
377
903
  ```yaml
378
904
  - name: Check inline docs
379
- run: docscribe --check lib/**/*.rb
905
+ run: docscribe lib
380
906
  ```
381
907
 
382
- Auto-fix before test stage:
908
+ Apply safe fixes before the test stage:
383
909
 
384
910
  ```yaml
385
- - name: Insert inline docs
386
- run: docscribe --write lib/**/*.rb
911
+ - name: Apply safe inline docs
912
+ run: docscribe -a lib
387
913
  ```
388
914
 
389
- Rewrite mode (regenerate existing method docs):
915
+ Aggressively rebuild docs:
390
916
 
391
917
  ```yaml
392
- - name: Refresh inline docs
393
- run: docscribe --rewrite --write lib/**/*.rb
918
+ - name: Rebuild inline docs
919
+ run: docscribe -A lib
394
920
  ```
395
921
 
396
922
  ## Comparison to YARD's parser
397
923
 
398
924
  Docscribe and YARD solve different parts of the documentation problem:
399
925
 
400
- - Parsing and insertion:
401
- - Docscribe parses with Ruby's AST (parser gem) and inserts/updates doc comments inline. It does not reformat code
402
- or produce HTML by itself.
403
- - YARD parses Ruby into a registry and can generate documentation sites and perform advanced analysis (tags,
404
- transitive docs, macros).
405
-
406
- - Preservation vs generation:
407
- - Docscribe preserves your original source exactly, only inserting comment blocks above methods.
408
- - YARD generates documentation output (HTML, JSON) based on its registry; it's not designed to write back to your
409
- source.
410
-
411
- - Semantics:
412
- - Docscribe models Ruby visibility semantics precisely for inline usage (including `class << self`).
413
- - YARD has rich semantics around tags and directives; it can leverage your inline comments (including those inserted
414
- by Docscribe).
415
-
416
- - Recommended workflow:
417
- - Use Docscribe to seed and maintain inline docs with inferred tags/types.
418
- - Optionally use YARD (dev-only) to render HTML from those comments:
419
- ```shell
420
- yard doc -o docs
421
- ```
926
+ - Docscribe inserts/updates inline comments by rewriting source.
927
+ - YARD can generate HTML docs based on inline comments.
928
+
929
+ Recommended workflow:
930
+
931
+ - Use Docscribe to seed and maintain inline docs with inferred tags/types.
932
+ - Optionally use YARD (dev-only) to render HTML from those comments:
933
+
934
+ ```shell
935
+ yard doc -o docs
936
+ ```
422
937
 
423
938
  ## Limitations
424
939
 
425
- - Does not merge into existing comments; in normal mode, a method with a comment directly above it is skipped. Use
426
- `--rewrite` to regenerate.
427
- - Type inference is heuristic. Complex flows and meta-programming will fall back to Object or best-effort types.
428
- - Only Ruby 3.0+ is officially supported.
429
- - Inline rewrite is textual; ensure a clean working tree before using `--write` or `--rewrite`.
940
+ - Safe mode only merges into existing **doc-like** comment blocks. Ordinary comments that are not recognized as
941
+ documentation are preserved and treated conservatively.
942
+ - Type inference is heuristic. Complex flows and meta-programming will fall back to `Object` or best-effort types.
943
+ - Aggressive mode (`-A`) replaces existing doc blocks and should be reviewed carefully.
430
944
 
431
945
  ## Roadmap
432
946
 
433
- - Merge tags into existing docstrings (opt-in).
434
- - Recognize common APIs for return inference (`Time.now`, `File.read`, `JSON.parse`).
435
- - Configurable rules and per-project exclusions.
436
- - Editor integration for on-save inline docs.
947
+ - Effective config dump;
948
+ - JSON output;
949
+ - Overload-aware signature selection;
950
+ - Manual `@!attribute` merge policy;
951
+ - Richer inference for common APIs;
952
+ - Editor integration.
437
953
 
438
954
  ## Contributing
439
955
 
440
- Issues and PRs welcome. Please run:
441
-
442
956
  ```shell
443
957
  bundle exec rspec
444
958
  bundle exec rubocop
445
959
  ```
446
960
 
447
- See CODE_OF_CONDUCT.md.
448
-
449
961
  ## License
450
962
 
451
963
  MIT