docscribe 1.0.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.
data/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # Docscribe
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/docscribe.svg)](https://rubygems.org/gems/docscribe)
4
+ [![RubyGems Downloads](https://img.shields.io/gem/dt/docscribe.svg)](https://rubygems.org/gems/docscribe)
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
+ [![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)
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.
12
+
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.
17
+
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.
20
+
21
+ * [Docscribe](#docscribe)
22
+ * [Installation](#installation)
23
+ * [Quick start](#quick-start)
24
+ * [CLI](#cli)
25
+ * [Inline behavior](#inline-behavior)
26
+ * [Rewrite mode](#rewrite-mode)
27
+ * [Type inference](#type-inference)
28
+ * [Rescue-aware returns and @raise](#rescue-aware-returns-and-raise)
29
+ * [Visibility semantics](#visibility-semantics)
30
+ * [API (library) usage](#api-library-usage)
31
+ * [Configuration](#configuration)
32
+ * [CLI](#cli-1)
33
+ * [CI integration](#ci-integration)
34
+ * [Comparison to YARD's parser](#comparison-to-yards-parser)
35
+ * [Limitations](#limitations)
36
+ * [Roadmap](#roadmap)
37
+ * [Contributing](#contributing)
38
+ * [License](#license)
39
+
40
+ ## Installation
41
+
42
+ Add to your Gemfile:
43
+
44
+ ```ruby
45
+ gem 'docscribe'
46
+ ```
47
+
48
+ Then:
49
+
50
+ ```shell
51
+ bundle install
52
+ ```
53
+
54
+ Or install globally:
55
+
56
+ ```shell
57
+ gem install docscribe
58
+ ```
59
+
60
+ Requires Ruby 3.0+.
61
+
62
+ ## Quick start
63
+
64
+ Given code:
65
+
66
+ ```ruby
67
+
68
+ class Demo
69
+ def foo(a, options: {})
70
+ 42
71
+ end
72
+
73
+ def bar(verbose: true)
74
+ 123
75
+ end
76
+
77
+ private
78
+
79
+ def self.bump
80
+ :ok
81
+ end
82
+
83
+ class << self
84
+ private
85
+
86
+ def internal; end
87
+ end
88
+ end
89
+ ```
90
+
91
+ Run:
92
+
93
+ ```shell
94
+ echo "...code above..." | docscribe --stdin
95
+ ```
96
+
97
+ Output:
98
+
99
+ ```ruby
100
+
101
+ class Demo
102
+ # +Demo#foo+ -> Integer
103
+ #
104
+ # Method documentation.
105
+ #
106
+ # @param [Object] a Param documentation.
107
+ # @param [Hash] options Param documentation.
108
+ # @return [Integer]
109
+ def foo(a, options: {})
110
+ 42
111
+ end
112
+
113
+ # +Demo#bar+ -> Integer
114
+ #
115
+ # Method documentation.
116
+ #
117
+ # @param [Boolean] verbose Param documentation.
118
+ # @return [Integer]
119
+ def bar(verbose: true)
120
+ 123
121
+ end
122
+
123
+ private
124
+
125
+ # +Demo.bump+ -> Symbol
126
+ #
127
+ # Method documentation.
128
+ #
129
+ # @return [Symbol]
130
+ def self.bump
131
+ :ok
132
+ end
133
+
134
+ class << self
135
+ private
136
+
137
+ # +Demo.internal+ -> Object
138
+ #
139
+ # Method documentation.
140
+ #
141
+ # @private
142
+ # @return [Object]
143
+ def internal; end
144
+ end
145
+ end
146
+ ```
147
+
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.`
153
+
154
+ ## CLI
155
+
156
+ ```shell
157
+ docscribe [options] [files...]
158
+ ```
159
+
160
+ Options:
161
+
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.
168
+
169
+ Examples:
170
+
171
+ - Print to stdout for one file:
172
+ ```shell
173
+ docscribe path/to/file.rb
174
+ ```
175
+ - Rewrite files in place (ensure a clean working tree):
176
+ ```shell
177
+ docscribe --write lib/**/*.rb
178
+ ```
179
+ - CI check (fail if docs are missing/stale):
180
+ ```shell
181
+ docscribe --check lib/**/*.rb
182
+ ```
183
+ - Rewrite existing doc blocks above methods (regenerate headers/tags):
184
+ ```shell
185
+ docscribe --rewrite --write lib/**/*.rb
186
+ ```
187
+
188
+ ## Inline behavior
189
+
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.
194
+
195
+ ### Rewrite mode
196
+
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).
202
+
203
+ ## Type inference
204
+
205
+ Heuristics (best-effort):
206
+
207
+ Parameters:
208
+
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`
219
+
220
+ Return values:
221
+
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.
227
+
228
+ ## Rescue-aware returns and @raise
229
+
230
+ Docscribe detects exceptions and rescue branches:
231
+
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]`).
237
+
238
+ - Conditional return types for rescue branches:
239
+ - Docscribe adds `@return [Type]` if `ExceptionA`, `ExceptionB` for each rescue clause.
240
+
241
+ Example:
242
+
243
+ ```ruby
244
+
245
+ class X
246
+ def a
247
+ 42
248
+ rescue Foo, Bar
249
+ "fallback"
250
+ end
251
+
252
+ def b
253
+ risky
254
+ rescue
255
+ "n"
256
+ end
257
+ end
258
+ ```
259
+
260
+ Becomes:
261
+
262
+ ```ruby
263
+
264
+ class X
265
+ # +X#a+ -> Integer
266
+ #
267
+ # Method documentation.
268
+ #
269
+ # @raise [Foo]
270
+ # @raise [Bar]
271
+ # @return [Integer]
272
+ # @return [String] if Foo, Bar
273
+ def a
274
+ 42
275
+ rescue Foo, Bar
276
+ "fallback"
277
+ end
278
+
279
+ # +X#b+ -> Object
280
+ #
281
+ # Method documentation.
282
+ #
283
+ # @raise [StandardError]
284
+ # @return [Object]
285
+ # @return [String] if StandardError
286
+ def b
287
+ risky
288
+ rescue
289
+ "n"
290
+ end
291
+ end
292
+ ```
293
+
294
+ ## Visibility semantics
295
+
296
+ We match Ruby's behavior:
297
+
298
+ - A bare `private`/`protected`/`public` in a class/module body affects instance methods only.
299
+ - 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`.
302
+
303
+ Inline tags:
304
+
305
+ - `@private` is added for methods that are private in context.
306
+ - `@protected` is added similarly for protected methods.
307
+
308
+ ## API (library) usage
309
+
310
+ ```ruby
311
+ require 'docscribe/inline_rewriter'
312
+
313
+ code = <<~RUBY
314
+ class Demo
315
+ def foo(a, options: {}); 42; end
316
+ class << self; private; def internal; end; end
317
+ end
318
+ RUBY
319
+
320
+ # Insert docs (skip methods that already have a comment above)
321
+ out = Docscribe::InlineRewriter.insert_comments(code)
322
+ puts out
323
+
324
+ # Replace existing comment blocks above methods
325
+ out2 = Docscribe::InlineRewriter.insert_comments(code, rewrite: true)
326
+ ```
327
+
328
+ ## Configuration
329
+
330
+ Docscribe can be configured via a YAML file (docscribe.yml by default, or pass --config PATH).
331
+
332
+ Example:
333
+
334
+ ```yaml
335
+ 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"
342
+
343
+ doc:
344
+ default_message: "Method documentation."
345
+
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
354
+
355
+ inference:
356
+ fallback_type: "Object"
357
+ nil_as_optional: true
358
+ treat_options_keyword_as_hash: true
359
+ ```
360
+
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.
366
+
367
+ ### CLI
368
+
369
+ ```bash
370
+ docscribe --config docscribe.yml --write lib/**/*.rb
371
+ ```
372
+
373
+ ## CI integration
374
+
375
+ Fail the build if files would change:
376
+
377
+ ```yaml
378
+ - name: Check inline docs
379
+ run: docscribe --check lib/**/*.rb
380
+ ```
381
+
382
+ Auto-fix before test stage:
383
+
384
+ ```yaml
385
+ - name: Insert inline docs
386
+ run: docscribe --write lib/**/*.rb
387
+ ```
388
+
389
+ Rewrite mode (regenerate existing method docs):
390
+
391
+ ```yaml
392
+ - name: Refresh inline docs
393
+ run: docscribe --rewrite --write lib/**/*.rb
394
+ ```
395
+
396
+ ## Comparison to YARD's parser
397
+
398
+ Docscribe and YARD solve different parts of the documentation problem:
399
+
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
+ ```
422
+
423
+ ## Limitations
424
+
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`.
430
+
431
+ ## Roadmap
432
+
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.
437
+
438
+ ## Contributing
439
+
440
+ Issues and PRs welcome. Please run:
441
+
442
+ ```shell
443
+ bundle exec rspec
444
+ bundle exec rubocop
445
+ ```
446
+
447
+ See CODE_OF_CONDUCT.md.
448
+
449
+ ## License
450
+
451
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/exe/docscribe ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'docscribe/config'
6
+ require 'docscribe/inline_rewriter'
7
+
8
+ options = {
9
+ stdin: false,
10
+ write: false, # rewrite files in place
11
+ check: false, # dry-run (exit 1 if any file would change)
12
+ rewrite: false, # replace existing comment blocks when inserting
13
+ config: nil
14
+ }
15
+
16
+ parser = OptionParser.new do |opts|
17
+ opts.banner = 'Usage: docscribe [options] [files...]'
18
+ opts.on('--stdin', 'Read code from STDIN and print with docs inserted') { options[:stdin] = true }
19
+ opts.on('--write', 'Rewrite files in place') { options[:write] = true }
20
+ opts.on('--check', 'Dry-run: exit 1 if any file would change') { options[:check] = true }
21
+ opts.on('--rewrite', 'Replace existing comment blocks above methods') { options[:rewrite] = true }
22
+ opts.on('--config PATH', 'Path to config YAML (default: docscribe.yml)') { |v| options[:config] = v }
23
+ opts.on('--version', 'Print version and exit') do
24
+ require 'docscribe/version'
25
+ puts Docscribe::VERSION
26
+ exit
27
+ end
28
+ opts.on('-h', '--help', 'Show this help') do
29
+ puts opts
30
+ exit
31
+ end
32
+ end
33
+
34
+ parser.parse!(ARGV)
35
+
36
+ conf = Docscribe::Config.load(options[:config])
37
+
38
+ def transform(code, replace:, config:)
39
+ Docscribe::InlineRewriter.insert_comments(code, rewrite: replace, config: config)
40
+ end
41
+
42
+ def rewrite(code, replace:)
43
+ Docscribe::InlineRewriter.insert_comments(code, rewrite: replace)
44
+ end
45
+
46
+ if options[:stdin]
47
+ code = $stdin.read
48
+ puts rewrite(code, replace: options[:rewrite])
49
+ exit 0
50
+ end
51
+
52
+ if ARGV.empty?
53
+ warn 'No input. Use --stdin or pass file paths. See --help.'
54
+ exit 1
55
+ end
56
+
57
+ changed = false
58
+
59
+ ARGV.each do |path|
60
+ src = File.read(path)
61
+ out = transform(src, replace: options[:rewrite], config: conf)
62
+ if options[:check]
63
+ if out != src
64
+ warn "Would change: #{path}"
65
+ changed = true
66
+ end
67
+ elsif options[:write]
68
+ if out != src
69
+ File.write(path, out)
70
+ puts "Updated: #{path}"
71
+ end
72
+ else
73
+ puts out
74
+ end
75
+ end
76
+
77
+ exit(options[:check] && changed ? 1 : 0)