docscribe 1.1.0 → 1.2.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +662 -187
  3. data/exe/docscribe +2 -126
  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 +142 -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 +184 -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 +104 -258
  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 +607 -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 +599 -428
  33. data/lib/docscribe/parsing.rb +55 -44
  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. metadata +37 -3
data/README.md CHANGED
@@ -13,26 +13,63 @@ returns), and respects Ruby visibility semantics — without using YARD to parse
13
13
 
14
14
  - No AST reprinting. Your original code, formatting, and constructs (like `class << self`, `heredocs`, `%i[]`) are
15
15
  preserved.
16
- - Inline-first. Comments are inserted at the start of each `def`/`defs` line.
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`.
17
18
  - Heuristic type inference for params and return values, including conditional returns in rescue branches.
18
- - Optional rewrite mode for regenerating existing method docs.
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.
19
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.
20
29
 
21
- Why not YARD? We started with YARD's parser, but switched to an AST-based in-place rewriter for maximum preservation of
22
- source structure and exact control over Ruby semantics.
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
23
39
 
24
40
  * [Docscribe](#docscribe)
41
+ * [Contents](#contents)
25
42
  * [Installation](#installation)
26
43
  * [Quick start](#quick-start)
27
44
  * [CLI](#cli)
28
- * [Inline behavior](#inline-behavior)
29
- * [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)
30
60
  * [Type inference](#type-inference)
31
61
  * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
32
62
  * [Visibility semantics](#visibility-semantics)
33
63
  * [API (library) usage](#api-library-usage)
34
64
  * [Configuration](#configuration)
35
- * [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)
36
73
  * [CI integration](#ci-integration)
37
74
  * [Comparison to YARD's parser](#comparison-to-yards-parser)
38
75
  * [Limitations](#limitations)
@@ -45,7 +82,7 @@ source structure and exact control over Ruby semantics.
45
82
  Add to your Gemfile:
46
83
 
47
84
  ```ruby
48
- gem 'docscribe'
85
+ gem "docscribe"
49
86
  ```
50
87
 
51
88
  Then:
@@ -100,7 +137,6 @@ echo "...code above..." | docscribe --stdin
100
137
  Output:
101
138
 
102
139
  ```ruby
103
-
104
140
  class Demo
105
141
  # +Demo#foo+ -> Integer
106
142
  #
@@ -148,11 +184,11 @@ class Demo
148
184
  end
149
185
  ```
150
186
 
151
- Notes:
152
-
153
- - The tool inserts doc headers at the start of def/defs lines and preserves everything else.
154
- - Class methods show with a dot (`+Demo.bump+`, `+Demo.internal+`).
155
- - 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`.
156
192
 
157
193
  ## CLI
158
194
 
@@ -160,174 +196,458 @@ Notes:
160
196
  docscribe [options] [files...]
161
197
  ```
162
198
 
163
- 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`).
164
245
 
165
- - `--stdin` Read source from STDIN and print with docs inserted.
166
- - `--write` Rewrite files in place (inline mode).
167
- - `--check` Dry-run: exit 1 if any file would change (useful in CI).
168
- - `--rewrite` Replace any existing comment block above methods (see “Rewrite mode” below).
169
- - `--version` Print version and exit.
170
- - `-h`, `--help` Show help.
246
+ - `-v`, `--version`
247
+ Print version and exit.
171
248
 
172
- Examples:
249
+ - `-h`, `--help`
250
+ Show help.
173
251
 
174
- - Print to stdout for one file:
252
+ ### Examples
253
+
254
+ - Inspect a directory:
175
255
  ```shell
176
- docscribe path/to/file.rb
256
+ docscribe lib
177
257
  ```
178
- - Rewrite files in place (ensure a clean working tree):
258
+
259
+ - Apply safe updates:
260
+ ```shell
261
+ docscribe -a lib
262
+ ```
263
+
264
+ - Apply aggressive updates:
179
265
  ```shell
180
- docscribe --write lib/**/*.rb
266
+ docscribe -A lib
181
267
  ```
182
- - CI check (fail if docs are missing/stale):
268
+
269
+ - Preview output for a single file via STDIN:
183
270
  ```shell
184
- docscribe --check lib/**/*.rb
271
+ cat path/to/file.rb | docscribe --stdin
185
272
  ```
186
- - Rewrite existing doc blocks above methods (regenerate headers/tags):
273
+
274
+ - Use RBS signatures:
187
275
  ```shell
188
- docscribe --rewrite --write lib/**/*.rb
276
+ docscribe -a --rbs --sig-dir sig lib
189
277
  ```
190
- - Check a directory (Docscribe expands directories to `**/*.rb`):
191
- ```bash
192
- docscribe --check lib
278
+
279
+ - Show detailed reasons for files that would change:
280
+ ```shell
281
+ docscribe --verbose --explain lib
193
282
  ```
194
283
 
195
- ## Inline behavior
284
+ ## Update strategies
285
+
286
+ Docscribe supports two update strategies: **safe** and **aggressive**.
287
+
288
+ ### Safe strategy
289
+
290
+ Used by:
291
+
292
+ - default inspect mode: `docscribe lib`
293
+ - safe write mode: `docscribe -a lib`
294
+
295
+ Safe strategy:
296
+
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
301
+
302
+ This is the recommended day-to-day mode.
303
+
304
+ ### Aggressive strategy
305
+
306
+ Used by:
307
+
308
+ - aggressive write mode: `docscribe -A lib`
196
309
 
197
- - Inserts comment blocks immediately above def/defs nodes.
198
- - Skips methods that already have a comment directly above them (does not merge into existing comments) unless you pass
199
- `--rewrite`.
200
- - Maintains original formatting and constructs; only adds comments.
310
+ Aggressive strategy:
201
311
 
202
- ### Rewrite mode
312
+ - rebuilds existing doc blocks
313
+ - replaces existing generated documentation more fully
314
+ - is more invasive than safe mode
203
315
 
204
- - With `--rewrite`, Docscribe will remove the contiguous comment block immediately above a method (plus intervening
205
- blank
206
- lines) and replace it with a fresh generated block.
207
- - This is useful to refresh docs across a codebase after improving inference or rules.
208
- - Use with caution (prefer a clean working tree and review diffs).
316
+ Use it when you want to rebaseline or regenerate docs wholesale.
209
317
 
210
- ### Output markers in CI
318
+ ### Output markers
211
319
 
212
- When using `--check`, Docscribe prints one character per file:
320
+ In inspect mode, Docscribe prints one character per file:
213
321
 
214
- - `.` = file is up-to-date
215
- - `F` = file would change (missing/stale docs)
322
+ - `.` = file is up to date
323
+ - `F` = file would change
324
+ - `E` = file had an error
216
325
 
217
- When using `--write`:
326
+ In write modes:
218
327
 
219
328
  - `.` = file already OK
220
- - `C` = file was corrected and rewritten
329
+ - `C` = file was updated
330
+ - `E` = file had an error
221
331
 
222
- Docscribe prints a summary at the end and exits non-zero in `--check` mode if any file would change.
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
223
340
 
224
341
  ## Parser backend (Parser gem vs Prism)
225
342
 
226
343
  Docscribe internally works with `parser`-gem-compatible AST nodes and `Parser::Source::*` objects (so it can use
227
- `Parser::Source::TreeRewriter` without changing your formatting).
344
+ `Parser::Source::TreeRewriter` without changing formatting).
228
345
 
229
346
  - On Ruby **<= 3.3**, Docscribe parses using the `parser` gem.
230
- - On Ruby **>= 3.4**, Docscribe parses using **Prism** and translates the tree into the `parser` gems AST (so tooling
231
- stays compatible).
347
+ - On Ruby **>= 3.4**, Docscribe parses using **Prism** and translates the tree into the `parser` gem's AST.
232
348
 
233
349
  You can force a backend with an environment variable:
234
350
 
235
- ```bash
236
- DOCSCRIBE_PARSER_BACKEND=parser bundle exec docscribe --check lib
237
- DOCSCRIBE_PARSER_BACKEND=prism bundle exec docscribe --check lib
351
+ ```shell
352
+ DOCSCRIBE_PARSER_BACKEND=parser bundle exec docscribe lib
353
+ DOCSCRIBE_PARSER_BACKEND=prism bundle exec docscribe lib
238
354
  ```
239
355
 
240
- ## Type inference
356
+ ## External type integrations (optional)
241
357
 
242
- Heuristics (best-effort):
358
+ Docscribe can improve generated `@param` and `@return` types by reading external signatures instead of relying only on
359
+ AST inference.
243
360
 
244
- Parameters:
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.
245
369
 
246
- - `*args` -> `Array`
247
- - `**kwargs` -> `Hash`
248
- - `&block` -> `Proc`
249
- - keyword args:
250
- - verbose: `true` -> `Boolean`
251
- - options: `{}` -> `Hash`
252
- - kw: (no default) -> `Object`
253
- - positional defaults:
254
- - `42` -> `Integer`, `1.0` -> `Float`, `'x'` -> `String`, `:ok` -> `Symbol`
255
- - `[]` -> `Array`, `{}` -> `Hash`, `/x/` -> `Regexp`, `true`/`false` -> `Boolean`, `nil` -> `nil`
370
+ ### RBS
256
371
 
257
- Return values:
372
+ Docscribe can read method signatures from `.rbs` files and use them to generate more accurate parameter and return
373
+ types.
258
374
 
259
- - For simple bodies, Docscribe looks at the last expression or explicit return:
260
- - `42` -> `Integer`
261
- - `:ok` -> `Symbol`
262
- - Unions with nil become optional types (e.g., `String` or `nil` -> `String?`).
263
- - For control flow (`if`/`case`), it unifies branches conservatively.
375
+ CLI:
264
376
 
265
- ## Rescue-aware returns and @raise
377
+ ```shell
378
+ docscribe -a --rbs --sig-dir sig lib
379
+ ```
266
380
 
267
- Docscribe detects exceptions and rescue branches:
381
+ You can pass `--sig-dir` multiple times:
268
382
 
269
- - Rescue exceptions become `@raise` tags:
270
- - `rescue Foo, Bar` -> `@raise [Foo]` and `@raise [Bar]`
271
- - bare rescue -> `@raise [StandardError]`
272
- - (optional) explicit raise/fail also adds a tag (`raise Foo` -> `@raise [Foo]`, `raise` ->
273
- `@raise [StandardError]`).
383
+ ```shell
384
+ docscribe -a --rbs --sig-dir sig --sig-dir vendor/sigs lib
385
+ ```
274
386
 
275
- - Conditional return types for rescue branches:
276
- - Docscribe adds `@return [Type]` if `ExceptionA`, `ExceptionB` for each rescue clause.
387
+ Config:
388
+
389
+ ```yaml
390
+ rbs:
391
+ enabled: true
392
+ sig_dirs:
393
+ - sig
394
+ collapse_generics: false
395
+ ```
277
396
 
278
397
  Example:
279
398
 
280
399
  ```ruby
400
+ # Ruby source
401
+ class Demo
402
+ def foo(verbose:, count:)
403
+ "body says String"
404
+ end
405
+ end
406
+ ```
281
407
 
282
- class X
283
- def a
284
- 42
285
- rescue Foo, Bar
286
- "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'
287
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:
441
+
442
+ ```shell
443
+ docscribe -a --sorbet lib
444
+ ```
445
+
446
+ With RBI directories:
288
447
 
289
- def b
290
- risky
291
- rescue
292
- "n"
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'
293
478
  end
294
479
  end
295
480
  ```
296
481
 
297
- Becomes:
482
+ Docscribe will use the Sorbet signature instead of the inferred body type:
298
483
 
299
484
  ```ruby
485
+ class Demo
486
+ extend T::Sig
300
487
 
301
- class X
302
- # +X#a+ -> Integer
488
+ # +Demo#foo+ -> Integer
303
489
  #
304
490
  # Method documentation.
305
491
  #
306
- # @raise [Foo]
307
- # @raise [Bar]
492
+ # @param [Boolean] verbose Param documentation.
493
+ # @param [Integer] count Param documentation.
308
494
  # @return [Integer]
309
- # @return [String] if Foo, Bar
310
- def a
311
- 42
312
- rescue Foo, Bar
313
- "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'
314
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
551
+ end
552
+ end
553
+ ```
554
+
555
+ Example output:
556
+
557
+ ```ruby
558
+ # demo.rb
559
+ class Demo
560
+ extend T::Sig
315
561
 
316
- # +X#b+ -> Object
562
+ # +Demo#foo+ -> Integer
317
563
  #
318
564
  # Method documentation.
319
565
  #
320
- # @raise [StandardError]
321
- # @return [Object]
322
- # @return [String] if StandardError
323
- def b
324
- risky
325
- rescue
326
- "n"
566
+ # @return [Integer]
567
+ sig { returns(Integer) }
568
+ def foo
569
+ 1
327
570
  end
328
571
  end
329
572
  ```
330
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
+
331
651
  ## Visibility semantics
332
652
 
333
653
  We match Ruby's behavior:
@@ -342,10 +662,26 @@ Inline tags:
342
662
  - `@private` is added for methods that are private in context.
343
663
  - `@protected` is added similarly for protected methods.
344
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
+
345
681
  ## API (library) usage
346
682
 
347
683
  ```ruby
348
- require 'docscribe/inline_rewriter'
684
+ require "docscribe/inline_rewriter"
349
685
 
350
686
  code = <<~RUBY
351
687
  class Demo
@@ -354,135 +690,274 @@ code = <<~RUBY
354
690
  end
355
691
  RUBY
356
692
 
357
- # Insert docs (skip methods that already have a comment above)
693
+ # Basic insertion behavior
358
694
  out = Docscribe::InlineRewriter.insert_comments(code)
359
695
  puts out
360
696
 
361
- # Replace existing comment blocks above methods
362
- 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)
363
702
  ```
364
703
 
365
704
  ## Configuration
366
705
 
367
- 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`
368
724
 
369
725
  Example:
370
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
+
371
751
  ```yaml
372
752
  emit:
373
- header: true # controls "# +Class#method+ -> Type"
374
- param_tags: true # include @param lines
375
- return_tag: true # include normal @return
376
- visibility_tags: true # include @private/@protected
377
- raise_tags: true # include @raise [Error]
378
- 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
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.
379
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
380
850
  doc:
381
- default_message: "Method documentation."
851
+ param_tag_style: "type_name"
852
+ ```
382
853
 
383
- methods:
384
- instance:
385
- public:
386
- return_tag: true
387
- default_message: "Public API. Please document purpose and params."
388
- class:
389
- private:
390
- return_tag: false
854
+ writer params are emitted as:
391
855
 
392
- inference:
393
- fallback_type: "Object"
394
- nil_as_optional: true
395
- treat_options_keyword_as_hash: true
856
+ ```ruby
857
+ # @param [Object] value
396
858
  ```
397
859
 
398
- - emit.* toggles control which tags are emitted globally.
399
- - methods.<scope>.<visibility> allows per-method overrides:
400
- - return_tag: true/false
401
- - default_message: override the message for that bucket
402
- - inference.* tunes type inference defaults.
860
+ With:
403
861
 
404
- ### CLI
862
+ ```yaml
863
+ doc:
864
+ param_tag_style: "name_type"
865
+ ```
866
+
867
+ they are emitted as:
405
868
 
406
- ```bash
407
- docscribe --config docscribe.yml --write lib/**/*.rb
869
+ ```ruby
870
+ # @param value [Object]
871
+ ```
872
+
873
+ ### Create a starter config
874
+
875
+ Create `docscribe.yml` in the current directory:
876
+
877
+ ```shell
878
+ docscribe init
879
+ ```
880
+
881
+ Write to a custom path:
882
+
883
+ ```shell
884
+ docscribe init --config config/docscribe.yml
885
+ ```
886
+
887
+ Overwrite if it already exists:
888
+
889
+ ```shell
890
+ docscribe init --force
891
+ ```
892
+
893
+ Print the template to stdout:
894
+
895
+ ```shell
896
+ docscribe init --stdout
408
897
  ```
409
898
 
410
899
  ## CI integration
411
900
 
412
- Fail the build if files would change:
901
+ Fail the build if files would need safe updates:
413
902
 
414
903
  ```yaml
415
904
  - name: Check inline docs
416
- run: docscribe --check lib/**/*.rb
905
+ run: docscribe lib
417
906
  ```
418
907
 
419
- Auto-fix before test stage:
908
+ Apply safe fixes before the test stage:
420
909
 
421
910
  ```yaml
422
- - name: Insert inline docs
423
- run: docscribe --write lib/**/*.rb
911
+ - name: Apply safe inline docs
912
+ run: docscribe -a lib
424
913
  ```
425
914
 
426
- Rewrite mode (regenerate existing method docs):
915
+ Aggressively rebuild docs:
427
916
 
428
917
  ```yaml
429
- - name: Refresh inline docs
430
- run: docscribe --rewrite --write lib/**/*.rb
918
+ - name: Rebuild inline docs
919
+ run: docscribe -A lib
431
920
  ```
432
921
 
433
922
  ## Comparison to YARD's parser
434
923
 
435
924
  Docscribe and YARD solve different parts of the documentation problem:
436
925
 
437
- - Parsing and insertion:
438
- - Docscribe parses with Ruby's AST (parser gem) and inserts/updates doc comments inline. It does not reformat code
439
- or produce HTML by itself.
440
- - YARD parses Ruby into a registry and can generate documentation sites and perform advanced analysis (tags,
441
- transitive docs, macros).
442
-
443
- - Preservation vs generation:
444
- - Docscribe preserves your original source exactly, only inserting comment blocks above methods.
445
- - YARD generates documentation output (HTML, JSON) based on its registry; it's not designed to write back to your
446
- source.
447
-
448
- - Semantics:
449
- - Docscribe models Ruby visibility semantics precisely for inline usage (including `class << self`).
450
- - YARD has rich semantics around tags and directives; it can leverage your inline comments (including those inserted
451
- by Docscribe).
452
-
453
- - Recommended workflow:
454
- - Use Docscribe to seed and maintain inline docs with inferred tags/types.
455
- - Optionally use YARD (dev-only) to render HTML from those comments:
456
- ```shell
457
- yard doc -o docs
458
- ```
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
+ ```
459
937
 
460
938
  ## Limitations
461
939
 
462
- - Does not merge into existing comments; in normal mode, a method with a comment directly above it is skipped. Use
463
- `--rewrite` to regenerate.
464
- - Type inference is heuristic. Complex flows and meta-programming will fall back to Object or best-effort types.
465
- - Ruby 2.7+ supported.
466
- - 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.
467
944
 
468
945
  ## Roadmap
469
946
 
470
- - Merge tags into existing docstrings (opt-in).
471
- - Recognize common APIs for return inference (`Time.now`, `File.read`, `JSON.parse`).
472
- - Configurable rules and per-project exclusions.
473
- - 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.
474
953
 
475
954
  ## Contributing
476
955
 
477
- Issues and PRs welcome. Please run:
478
-
479
956
  ```shell
480
957
  bundle exec rspec
481
958
  bundle exec rubocop
482
959
  ```
483
960
 
484
- See CODE_OF_CONDUCT.md.
485
-
486
961
  ## License
487
962
 
488
963
  MIT