docscribe 1.2.1 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88fa96ef81689e131461ed06e9f7f4cebff1ac0567416a6417d83418ba2eab3c
4
- data.tar.gz: 827dcd841b63dff86681dbd65adcf1cef54089b3dabd121641eb4560052def03
3
+ metadata.gz: 911fd61018509316b42bf5a799232d24be36cb653d5997c4e170dab5db028884
4
+ data.tar.gz: 4ce0e7f0cc78c6b94ce366cb464726e083a6050a42f835e35995c378072507e6
5
5
  SHA512:
6
- metadata.gz: 8b4f1c36ae73417133f7f3119df2989b6ea6f6273a188d344887035a9e0c68716896e3d6c179fbf8fd314494940c1f7b8179cbd974a67566922241ed8f7c274f
7
- data.tar.gz: 2aa52b164aa86bed76df64af126019f08ad3ef755c044127eca9d022bceed1da1211fc26a772097c4da6d691624a561c850164420026073c1df0e04114a52d99
6
+ metadata.gz: 107a67dd5c484b840ba5ab85918ec25d53b0ce4adda13e4ef612d3bfa99d4201e82b474cd636f79a60e5e40e5cbaeb8e15c899939b5e74df14f397f4188b4168
7
+ data.tar.gz: 9cac2bb4b0c37b29707f0d6beec99987ab8d0f3d9327b3f44e9684816694e74fced394d88fc493ee6f5dc19ce07b82fc295e38f9c0273faad09976d125103a9e
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![License](https://img.shields.io/github/license/unurgunite/docscribe.svg)](https://github.com/unurgunite/docscribe/blob/master/LICENSE.txt)
7
7
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-blue.svg)](#installation)
8
8
 
9
+ ![Docscribe before/after demo](docs/image.png)
10
+
9
11
  Generate inline, YARD-style documentation comments for Ruby methods by analyzing your code's AST.
10
12
 
11
13
  Docscribe inserts doc headers before method definitions, infers parameter and return types (including rescue-aware
@@ -32,6 +34,7 @@ Common workflows:
32
34
  - Inspect what safe doc updates would be applied: `docscribe lib`
33
35
  - Apply safe doc updates: `docscribe -a lib`
34
36
  - Apply aggressive doc updates: `docscribe -A lib`
37
+ - Use RBS gem collection signatures: `docscribe -a --rbs-collection lib`
35
38
  - Use RBS signatures when available: `docscribe -a --rbs --sig-dir sig lib`
36
39
  - Use Sorbet signatures when available: `docscribe -a --sorbet --rbi-dir sorbet/rbi lib`
37
40
 
@@ -51,6 +54,7 @@ Common workflows:
51
54
  * [Parser backend (Parser gem vs Prism)](#parser-backend-parser-gem-vs-prism)
52
55
  * [External type integrations (optional)](#external-type-integrations-optional)
53
56
  * [RBS](#rbs)
57
+ * [RBS collection auto-discovery](#rbs-collection-auto-discovery)
54
58
  * [Sorbet](#sorbet)
55
59
  * [Inline Sorbet example](#inline-sorbet-example)
56
60
  * [Sorbet RBI example](#sorbet-rbi-example)
@@ -61,6 +65,12 @@ Common workflows:
61
65
  * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
62
66
  * [Visibility semantics](#visibility-semantics)
63
67
  * [API (library) usage](#api-library-usage)
68
+ * [Plugin system](#plugin-system)
69
+ * [TagPlugin](#tagplugin)
70
+ * [CollectorPlugin](#collectorplugin)
71
+ * [Registering plugins](#registering-plugins)
72
+ * [Idempotency](#idempotency)
73
+ * [Plugin examples](#plugin-examples)
64
74
  * [Configuration](#configuration)
65
75
  * [Filtering](#filtering)
66
76
  * [`attr_*` example](#attr_-example)
@@ -70,11 +80,13 @@ Common workflows:
70
80
  * [Merge behavior](#merge-behavior)
71
81
  * [Param tag style](#param-tag-style)
72
82
  * [Create a starter config](#create-a-starter-config)
83
+ * [Generate a plugin skeleton](#generate-a-plugin-skeleton)
73
84
  * [CI integration](#ci-integration)
74
85
  * [Comparison to YARD's parser](#comparison-to-yards-parser)
75
86
  * [Limitations](#limitations)
76
87
  * [Roadmap](#roadmap)
77
88
  * [Contributing](#contributing)
89
+ * [Discussion & Community](#discussion--community)
78
90
  * [License](#license)
79
91
 
80
92
  ## Installation
@@ -104,7 +116,6 @@ Requires Ruby 2.7+.
104
116
  Given code:
105
117
 
106
118
  ```ruby
107
-
108
119
  class Demo
109
120
  def foo(a, options: {})
110
121
  42
@@ -213,6 +224,11 @@ If you pass no files and don’t use `--stdin`, Docscribe processes the current
213
224
  - `-A`, `--autocorrect-all`
214
225
  Apply aggressive doc updates in place.
215
226
 
227
+ - `--rbs-collection`
228
+ Auto-discover the RBS collection directory from `rbs_collection.lock.yaml`.
229
+ Reads the `path:` field written by `bundle exec rbs collection install` and adds
230
+ it to the signature search path automatically. Implies `--rbs`.
231
+
216
232
  - `--stdin`
217
233
  Read source from STDIN and print rewritten output.
218
234
 
@@ -276,6 +292,16 @@ If you pass no files and don’t use `--stdin`, Docscribe processes the current
276
292
  docscribe -a --rbs --sig-dir sig lib
277
293
  ```
278
294
 
295
+ - Use RBS signatures with auto-discovered gem collection:
296
+ ```shell
297
+ docscribe -a --rbs-collection lib
298
+ ```
299
+
300
+ - Combine collection auto-discovery with a custom sig directory:
301
+ ```shell
302
+ docscribe -a --rbs-collection --sig-dir sig lib
303
+ ```
304
+
279
305
  - Show detailed reasons for files that would change:
280
306
  ```shell
281
307
  docscribe --verbose --explain lib
@@ -415,7 +441,6 @@ end
415
441
  Generated docs will prefer the RBS signature over inferred Ruby types:
416
442
 
417
443
  ```ruby
418
-
419
444
  class Demo
420
445
  # +Demo#foo+ -> Integer
421
446
  #
@@ -430,6 +455,50 @@ class Demo
430
455
  end
431
456
  ```
432
457
 
458
+ #### RBS collection auto-discovery
459
+
460
+ If your project uses [`rbs collection`](https://github.com/ruby/rbs/blob/master/docs/collection.md),
461
+ Docscribe can discover the installed gem signatures automatically without requiring
462
+ you to pass `--sig-dir` manually.
463
+
464
+ **Setup:**
465
+
466
+ ```shell
467
+ # 1. Initialize the collection config (one-time)
468
+ bundle exec rbs collection init
469
+
470
+ # 2. Install gem signatures
471
+ bundle exec rbs collection install
472
+ ```
473
+
474
+ This produces `rbs_collection.lock.yaml` and `.gem_rbs_collection/` in your project root.
475
+
476
+ **Usage:**
477
+
478
+ ```shell
479
+ docscribe -a --rbs-collection lib
480
+ ```
481
+
482
+ Docscribe reads the `path:` field from `rbs_collection.lock.yaml` and adds the
483
+ resolved directory to the signature search path. If no `path:` is set, it falls
484
+ back to `.gem_rbs_collection`.
485
+
486
+ You can combine `--rbs-collection` with `--sig-dir` to mix gem signatures with your own:
487
+
488
+ ```shell
489
+ docscribe -a --rbs-collection --sig-dir sig lib
490
+ ```
491
+
492
+ > [!NOTE]
493
+ > `--rbs-collection` only improves types for methods defined in gems that ship RBS
494
+ > signatures. For your own classes, provide a `sig/` directory with hand-written or
495
+ > generated `.rbs` files.
496
+
497
+ > [!IMPORTANT]
498
+ > If `rbs_collection.lock.yaml` is missing or the collection directory does not exist
499
+ > on disk, Docscribe will print a warning and skip the collection. Run
500
+ > `bundle exec rbs collection install` first.
501
+
433
502
  ### Sorbet
434
503
 
435
504
  Docscribe can also read Sorbet signatures from:
@@ -635,6 +704,13 @@ Return values:
635
704
  - For simple bodies, Docscribe looks at the last expression or explicit `return`.
636
705
  - Unions with `nil` become optional types (e.g. `String` or `nil` -> `String?`).
637
706
  - For control flow (`if`/`case`), it unifies branches conservatively.
707
+ - **RBS core type inference**: when `--rbs` is enabled, Docscribe resolves return types for method calls on core types
708
+ from their RBS definitions:
709
+ - `arg.positive?` (`arg = 1`) -> `Boolean` (from `Integer#positive?`)
710
+ - `arg.to_i` (`arg = ""`) -> `Integer` (from `String#to_i`)
711
+ - `arg.to_s.length` (`arg = 1`) -> `Integer` (chained: Integer -> String -> Integer)
712
+ - `arg.upcase` (`arg = ""`) -> `String` (from `String#upcase`)
713
+ - Rescue branches are also resolved (e.g. `"default"` -> `String`)
638
714
 
639
715
  ## Rescue-aware returns and @raise
640
716
 
@@ -701,6 +777,174 @@ out2 = Docscribe::InlineRewriter.insert_comments(code, strategy: :safe)
701
777
  out3 = Docscribe::InlineRewriter.insert_comments(code, strategy: :aggressive)
702
778
  ```
703
779
 
780
+ ## Plugin system
781
+
782
+ Docscribe ships a plugin system that lets you extend documentation generation without modifying the gem itself.
783
+
784
+ There are two extension points:
785
+
786
+ | Type | When it runs | What it produces |
787
+ |---------------------|--------------------------------------------------------------|---------------------------------------------------|
788
+ | **TagPlugin** | After a method is collected and its doc block is being built | Extra YARD tags appended to the block |
789
+ | **CollectorPlugin** | Before doc building, alongside the standard AST collector | New insertion targets for non-standard constructs |
790
+
791
+ ### TagPlugin
792
+
793
+ A `TagPlugin` receives a snapshot of everything known about a method at generation time and returns zero or more
794
+ additional YARD tags to append to the doc block.
795
+
796
+ ```ruby
797
+ class SincePlugin < Docscribe::Plugin::Base::TagPlugin
798
+ def initialize(version:)
799
+ @version = version
800
+ end
801
+
802
+ # @param [Docscribe::Plugin::Context] context
803
+ # @return [Array<Docscribe::Plugin::Tag>]
804
+ def call(context)
805
+ [Docscribe::Plugin::Tag.new(name: 'since', text: @version)]
806
+ end
807
+ end
808
+ ```
809
+
810
+ The `Context` struct provides:
811
+
812
+ | Attribute | Type | Description |
813
+ |-------------------|--------------------------|----------------------------------------|
814
+ | `node` | `Parser::AST::Node` | The `:def` or `:defs` AST node |
815
+ | `container` | `String` | e.g. `"MyModule::MyClass"` |
816
+ | `scope` | `Symbol` | `:instance` or `:class` |
817
+ | `visibility` | `Symbol` | `:public`, `:protected`, or `:private` |
818
+ | `method_name` | `Symbol` | Method name |
819
+ | `inferred_params` | `Hash{String => String}` | Name -> inferred type |
820
+ | `inferred_return` | `String` | Inferred return type |
821
+ | `source` | `String` | Raw method source text |
822
+
823
+ The `Tag` struct:
824
+
825
+ ```ruby
826
+ # Simple tag
827
+ Docscribe::Plugin::Tag.new(name: 'since', text: '1.3.0')
828
+ # => # @since 1.3.0
829
+
830
+ # Tag with types
831
+ Docscribe::Plugin::Tag.new(name: 'raise', types: ['ArgumentError'], text: 'if name is nil')
832
+ # => # @raise [ArgumentError] if name is nil
833
+ ```
834
+
835
+ ### CollectorPlugin
836
+
837
+ A `CollectorPlugin` receives the raw AST and source buffer for each file. It walks the tree itself and returns insertion
838
+ targets that Docscribe will document according to the selected strategy.
839
+
840
+ ```ruby
841
+ class DefineMethodPlugin < Docscribe::Plugin::Base::CollectorPlugin
842
+ # @param [Parser::AST::Node] ast
843
+ # @param [Parser::Source::Buffer] buffer
844
+ # @return [Array<Hash>]
845
+ def collect(ast, buffer)
846
+ results = []
847
+
848
+ Docscribe::Infer::ASTWalk.walk(ast) do |node|
849
+ next unless node.type == :send
850
+
851
+ _recv, meth, name_node, *_rest = *node
852
+ next unless meth == :define_method
853
+ next unless name_node&.type == :sym
854
+
855
+ meth_name = name_node.children.first
856
+
857
+ results << {
858
+ anchor_node: node,
859
+ doc: "# Dynamic method: #{meth_name}\n# @return [Object]\n"
860
+ }
861
+ end
862
+
863
+ results
864
+ end
865
+ end
866
+ ```
867
+
868
+ Each result hash must have:
869
+
870
+ | Key | Type | Description |
871
+ |----------------|---------------------|--------------------------------------------|
872
+ | `:anchor_node` | `Parser::AST::Node` | Node above which to insert the doc block |
873
+ | `:doc` | `String` | Complete doc block text including newlines |
874
+
875
+ > [!NOTE]
876
+ > You do not need to handle indentation manually. Docscribe reads the indentation from `anchor_node` and applies it to
877
+ > every line of `:doc` automatically.
878
+
879
+ ### Registering plugins
880
+
881
+ Plugins are registered at load time. The recommended pattern is to put registrations in a dedicated file and reference
882
+ it from `docscribe.yml`.
883
+
884
+ **`docscribe_plugins.rb`** (in your project root or `lib/`):
885
+
886
+ ```ruby
887
+ require 'docscribe/plugin'
888
+
889
+ # Tag plugin
890
+ class SincePlugin < Docscribe::Plugin::Base::TagPlugin
891
+ def call(context)
892
+ [Docscribe::Plugin::Tag.new(name: 'since', text: '1.3.0')]
893
+ end
894
+ end
895
+
896
+ # Collector plugin
897
+ class DefineMethodPlugin < Docscribe::Plugin::Base::CollectorPlugin
898
+ def collect(ast, buffer)
899
+ # ...
900
+ end
901
+ end
902
+
903
+ Docscribe::Plugin::Registry.register(SincePlugin.new)
904
+ Docscribe::Plugin::Registry.register(DefineMethodPlugin.new)
905
+ ```
906
+
907
+ **`docscribe.yml`**:
908
+
909
+ ```yaml
910
+ plugins:
911
+ require:
912
+ - ./docscribe_plugins
913
+ ```
914
+
915
+ Each entry is passed to `require`. The path is expanded relative to the current working directory.
916
+
917
+ Duck typing is also supported — any object responding to `#call` is treated as a `TagPlugin`, any object responding to
918
+ `#collect` is treated as a `CollectorPlugin`:
919
+
920
+ ```ruby
921
+ # Lambda as a TagPlugin
922
+ Docscribe::Plugin::Registry.register(
923
+ ->(context) { [Docscribe::Plugin::Tag.new(name: 'api', text: 'public')] }
924
+ )
925
+ ```
926
+
927
+ ### Idempotency
928
+
929
+ Docscribe handles idempotency for plugins automatically.
930
+
931
+ **TagPlugin**: before appending a tag, Docscribe checks whether a tag with that name already exists in the current doc
932
+ block. If it does, the tag is skipped.
933
+
934
+ **CollectorPlugin**: idempotency depends on the selected strategy.
935
+
936
+ | Strategy | Behaviour |
937
+ |---------------|--------------------------------------------------------------------------------------|
938
+ | `:safe` | Skips insertion if any comment block already exists immediately above `anchor_node` |
939
+ | `:aggressive` | Removes the existing comment block above `anchor_node` and inserts a fresh doc block |
940
+
941
+ This means a `CollectorPlugin`-generated block will not be duplicated on repeated safe runs, and will be fully rebuilt
942
+ on aggressive runs.
943
+
944
+ ### Plugin examples
945
+
946
+ Sample plugin available at [examples](examples/plugins)
947
+
704
948
  ## Configuration
705
949
 
706
950
  Docscribe can be configured via a YAML file (`docscribe.yml` by default, or pass `--config PATH`).
@@ -896,6 +1140,48 @@ Print the template to stdout:
896
1140
  docscribe init --stdout
897
1141
  ```
898
1142
 
1143
+ ### Generate a plugin skeleton
1144
+
1145
+ Docscribe can scaffold a plugin file so you don't have to write boilerplate by hand.
1146
+
1147
+ Generate a **TagPlugin**:
1148
+
1149
+ ```shell
1150
+ docscribe generate tag MyPlugin
1151
+ # Created: my_plugin.rb
1152
+ ```
1153
+
1154
+ Generate a **CollectorPlugin**:
1155
+
1156
+ ```shell
1157
+ docscribe generate collector MyCollector
1158
+ # Created: my_collector.rb
1159
+ ```
1160
+
1161
+ Write to a specific directory:
1162
+
1163
+ ```shell
1164
+ docscribe generate tag SincePlugin --output lib/plugins
1165
+ # Created: lib/plugins/since_plugin.rb
1166
+ ```
1167
+
1168
+ Print to STDOUT instead of writing a file:
1169
+
1170
+ ```shell
1171
+ docscribe generate tag SincePlugin --stdout
1172
+ ```
1173
+
1174
+ The generated file contains:
1175
+ - the correct base class (`Base::TagPlugin` or `Base::CollectorPlugin`)
1176
+ - inline comments describing every available `Context` attribute (TagPlugin)
1177
+ or the expected return shape (CollectorPlugin)
1178
+ - TODO markers showing exactly where to add your logic
1179
+ - registration and next-steps instructions printed to the terminal
1180
+
1181
+ > [!NOTE]
1182
+ > The class name must be a valid Ruby constant (`MyPlugin`, `My::Plugin`).
1183
+ > The output filename is the snake_case equivalent (`my_plugin.rb`, `my/plugin.rb`).
1184
+
899
1185
  ## CI integration
900
1186
 
901
1187
  Fail the build if files would need safe updates:
@@ -944,6 +1230,8 @@ yard doc -o docs
944
1230
 
945
1231
  ## Roadmap
946
1232
 
1233
+ - Method behavior inference from AST;
1234
+ - YAML-based plugin configuration;
947
1235
  - Effective config dump;
948
1236
  - JSON output;
949
1237
  - Overload-aware signature selection;
@@ -958,6 +1246,12 @@ bundle exec rspec
958
1246
  bundle exec rubocop
959
1247
  ```
960
1248
 
1249
+ ## Discussion & Community
1250
+
1251
+ - [Reddit discussion](https://www.reddit.com/r/ruby/comments/1s5uwjj/docscribe_for_ruby_autogenerate_inline_yard_docs/)
1252
+ - [Dev.to article](https://dev.to/unurgunite)
1253
+ - [GitHub Discussions](https://github.com/unurgunite/docscribe/discussions)
1254
+
961
1255
  ## License
962
1256
 
963
1257
  MIT
@@ -22,13 +22,14 @@ module Docscribe
22
22
  # @return [Docscribe::Config] merged effective config
23
23
  def build(base, options)
24
24
  needs_override =
25
- options[:include].any? ||
26
- options[:exclude].any? ||
25
+ options[:include].any? ||
26
+ options[:exclude].any? ||
27
27
  options[:include_file].any? ||
28
28
  options[:exclude_file].any? ||
29
- options[:rbs] ||
30
- options[:sig_dirs].any? ||
31
- options[:sorbet] ||
29
+ options[:rbs] ||
30
+ options[:rbs_collection] ||
31
+ options[:sig_dirs].any? ||
32
+ options[:sorbet] ||
32
33
  options[:rbi_dirs].any?
33
34
 
34
35
  return base unless needs_override
@@ -47,6 +48,17 @@ module Docscribe
47
48
  raw['rbs'] ||= {}
48
49
  raw['rbs']['enabled'] = true
49
50
  raw['rbs']['sig_dirs'] = Array(raw['rbs']['sig_dirs']) + options[:sig_dirs] if options[:sig_dirs].any?
51
+
52
+ if options[:rbs_collection]
53
+ require 'docscribe/types/rbs/collection_loader'
54
+ collection_path = Docscribe::Types::RBS::CollectionLoader.resolve
55
+ if collection_path
56
+ raw['rbs']['sig_dirs'] = Array(raw['rbs']['sig_dirs']) + [collection_path]
57
+ else
58
+ warn 'Docscribe: rbs_collection.lock.yaml not found or collection not installed. ' \
59
+ 'Run `bundle exec rbs collection install` first.'
60
+ end
61
+ end
50
62
  end
51
63
 
52
64
  if options[:sorbet] || options[:rbi_dirs].any?