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/exe/docscribe CHANGED
@@ -1,129 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
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
- def expand_paths(args)
47
- files = []
48
-
49
- args.each do |path|
50
- if File.directory?(path)
51
- files.concat(Dir.glob(File.join(path, '**', '*.rb')))
52
- elsif File.file?(path)
53
- files << path
54
- else
55
- warn "Skipping missing path: #{path}"
56
- end
57
- end
58
-
59
- files.uniq.sort
60
- end
61
-
62
- if options[:stdin]
63
- code = $stdin.read
64
- puts rewrite(code, replace: options[:rewrite])
65
- exit 0
66
- end
67
-
68
- if ARGV.empty?
69
- warn 'No input. Use --stdin or pass file paths. See --help.'
70
- exit 1
71
- end
72
-
73
- $stdout.sync = true
74
-
75
- paths = expand_paths(ARGV)
76
- if paths.empty?
77
- warn 'No files found. Pass files or directories (e.g. `docscribe --check lib`).'
78
- exit 1
79
- end
80
-
81
- changed = false
82
- checked_ok = 0
83
- checked_fail = 0
84
- corrected = 0
85
- fail_paths = []
86
-
87
- paths.each do |path|
88
- src = File.read(path)
89
- out = transform(src, replace: options[:rewrite], config: conf)
90
-
91
- if options[:check]
92
- if out == src
93
- print '.'
94
- checked_ok += 1
95
- else
96
- print 'F'
97
- checked_fail += 1
98
- changed = true
99
- fail_paths << path
100
- end
101
-
102
- elsif options[:write]
103
- if out == src
104
- print '.'
105
- else
106
- File.write(path, out)
107
- print 'C'
108
- corrected += 1
109
- end
110
-
111
- else
112
- puts out
113
- end
114
- end
115
-
116
- if options[:check]
117
- puts
118
- if checked_fail.zero?
119
- puts "Docscribe: OK (#{checked_ok} files checked)"
120
- else
121
- puts "Docscribe: FAILED (#{checked_fail} failing, #{checked_ok} ok)"
122
- fail_paths.each { |p| warn "Missing docs: #{p}" }
123
- end
124
- elsif options[:write]
125
- puts
126
- puts "Docscribe: updated #{corrected} file(s)" if corrected.positive?
127
- end
128
-
129
- exit(options[:check] && changed ? 1 : 0)
4
+ require 'docscribe/cli'
5
+ exit Docscribe::CLI.run(ARGV)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'docscribe/config'
4
+
5
+ module Docscribe
6
+ module CLI
7
+ module ConfigBuilder
8
+ module_function
9
+
10
+ # Build an effective config by applying CLI overrides on top of a base config.
11
+ #
12
+ # CLI overrides currently affect:
13
+ # - method/file include and exclude filters
14
+ # - RBS enablement and additional signature directories
15
+ # - Sorbet enablement and RBI directories
16
+ #
17
+ # If no relevant CLI override is present, the original config is returned unchanged.
18
+ #
19
+ # @note module_function: when included, also defines #build (instance visibility: private)
20
+ # @param [Docscribe::Config] base base config loaded from YAML/defaults
21
+ # @param [Hash] options parsed CLI options
22
+ # @return [Docscribe::Config] merged effective config
23
+ def build(base, options)
24
+ needs_override =
25
+ options[:include].any? ||
26
+ options[:exclude].any? ||
27
+ options[:include_file].any? ||
28
+ options[:exclude_file].any? ||
29
+ options[:rbs] ||
30
+ options[:sig_dirs].any? ||
31
+ options[:sorbet] ||
32
+ options[:rbi_dirs].any?
33
+
34
+ return base unless needs_override
35
+
36
+ raw = Marshal.load(Marshal.dump(base.raw))
37
+
38
+ raw['filter'] ||= {}
39
+ raw['filter']['include'] = Array(raw['filter']['include']) + options[:include]
40
+ raw['filter']['exclude'] = Array(raw['filter']['exclude']) + options[:exclude]
41
+
42
+ raw['filter']['files'] ||= {}
43
+ raw['filter']['files']['include'] = Array(raw['filter']['files']['include']) + options[:include_file]
44
+ raw['filter']['files']['exclude'] = Array(raw['filter']['files']['exclude']) + options[:exclude_file]
45
+
46
+ if options[:rbs] || options[:sig_dirs].any?
47
+ raw['rbs'] ||= {}
48
+ raw['rbs']['enabled'] = true
49
+ raw['rbs']['sig_dirs'] = Array(raw['rbs']['sig_dirs']) + options[:sig_dirs] if options[:sig_dirs].any?
50
+ end
51
+
52
+ if options[:sorbet] || options[:rbi_dirs].any?
53
+ raw['sorbet'] ||= {}
54
+ raw['sorbet']['enabled'] = true
55
+ raw['sorbet']['rbi_dirs'] = Array(raw['sorbet']['rbi_dirs']) + options[:rbi_dirs] if options[:rbi_dirs].any?
56
+ end
57
+
58
+ Docscribe::Config.new(raw)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'docscribe/config'
5
+
6
+ module Docscribe
7
+ module CLI
8
+ module Init
9
+ class << self
10
+ # Create or print a starter Docscribe configuration file.
11
+ #
12
+ # Supported behaviors:
13
+ # - write `docscribe.yml` (default)
14
+ # - write to a custom path via `--config`
15
+ # - overwrite an existing file via `--force`
16
+ # - print the template to STDOUT via `--stdout`
17
+ #
18
+ # @param [Array<String>] argv command-line arguments for `docscribe init`
19
+ # @return [Integer] process exit code
20
+ def run(argv)
21
+ opts = {
22
+ config: 'docscribe.yml',
23
+ force: false,
24
+ stdout: false
25
+ }
26
+
27
+ OptionParser.new do |o|
28
+ o.banner = 'Usage: docscribe init [options]'
29
+ o.on('--config PATH', 'Where to write the config (default: docscribe.yml)') { |v| opts[:config] = v }
30
+ o.on('-f', '--force', 'Overwrite if the file already exists') { opts[:force] = true }
31
+ o.on('--stdout', 'Print config template to STDOUT instead of writing a file') { opts[:stdout] = true }
32
+ o.on('-h', '--help', 'Show this help') do
33
+ puts o
34
+ return 0
35
+ end
36
+ end.parse!(argv)
37
+
38
+ yaml = Docscribe::Config.default_yaml
39
+
40
+ if opts[:stdout]
41
+ puts yaml
42
+ return 0
43
+ end
44
+
45
+ path = opts[:config]
46
+ if File.exist?(path) && !opts[:force]
47
+ warn "Config already exists: #{path} (use --force to overwrite)"
48
+ return 1
49
+ end
50
+
51
+ File.write(path, yaml)
52
+ puts "Created: #{path}"
53
+ 0
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Docscribe
6
+ module CLI
7
+ module Options
8
+ DEFAULT = {
9
+ stdin: false,
10
+ mode: :check, # :check, :write, :stdin
11
+ strategy: :safe, # :safe, :aggressive
12
+ verbose: false,
13
+ explain: false,
14
+ config: nil,
15
+ include: [],
16
+ exclude: [],
17
+ include_file: [],
18
+ exclude_file: [],
19
+ rbs: false,
20
+ sig_dirs: [],
21
+ sorbet: false,
22
+ rbi_dirs: []
23
+ }.freeze
24
+
25
+ module_function
26
+
27
+ # Parse CLI arguments into normalized Docscribe runtime options.
28
+ #
29
+ # CLI behavior model:
30
+ # - default: inspect mode using the safe strategy
31
+ # - `-a` / `--autocorrect`: write mode using the safe strategy
32
+ # - `-A` / `--autocorrect-all`: write mode using the aggressive strategy
33
+ # - `--stdin`: stdin mode using the selected strategy (safe by default)
34
+ #
35
+ # Filtering, config, verbosity, and external type options are applied
36
+ # orthogonally.
37
+ #
38
+ # @note module_function: when included, also defines #parse! (instance visibility: private)
39
+ # @param [Array<String>] argv raw CLI arguments
40
+ # @return [Hash] normalized runtime options
41
+ def parse!(argv)
42
+ options = Marshal.load(Marshal.dump(DEFAULT))
43
+ autocorrect_mode = nil
44
+
45
+ parser = OptionParser.new do |opts|
46
+ opts.banner = <<~TEXT
47
+ Usage: docscribe [options] [files...]
48
+
49
+ Default behavior:
50
+ Inspect files and report what safe doc updates would be applied.
51
+
52
+ Autocorrect:
53
+ -a, --autocorrect Apply safe doc updates in place
54
+ (insert missing docs, merge existing doc-like blocks,
55
+ normalize tag order)
56
+ -A, --autocorrect-all Apply aggressive doc updates in place
57
+ (rebuild existing doc blocks)
58
+
59
+ Input / config:
60
+ --stdin Read code from STDIN and print rewritten output
61
+ -C, --config PATH Path to config YAML (default: docscribe.yml)
62
+
63
+ Type information:
64
+ --rbs Use RBS signatures for @param/@return when available
65
+ --sig-dir DIR Add an RBS signature directory (repeatable). Implies `--rbs`.
66
+ --sorbet Use Sorbet signatures from inline sigs / RBI files when available
67
+ --rbi-dir DIR Add a Sorbet RBI directory (repeatable). Implies --sorbet.
68
+
69
+ Filtering:
70
+ --include PATTERN Include PATTERN (method id or file path; glob or /regex/)
71
+ --exclude PATTERN Exclude PATTERN (method id or file path; glob or /regex/)
72
+ --include-file PATTERN Only process files matching PATTERN (glob or /regex/)
73
+ --exclude-file PATTERN Skip files matching PATTERN (glob or /regex/)
74
+
75
+ Output:
76
+ --verbose Print per-file actions
77
+ -e, --explain Show detailed reasons for changes
78
+
79
+ Other:
80
+ -v, --version Print version and exit
81
+ -h, --help Show this help
82
+ TEXT
83
+
84
+ opts.on('-a', '--autocorrect', 'Apply safe doc updates in place') do
85
+ autocorrect_mode = :safe
86
+ end
87
+
88
+ opts.on('-A', '--autocorrect-all', 'Apply aggressive doc updates in place') do
89
+ autocorrect_mode = :aggressive
90
+ end
91
+
92
+ opts.on('--stdin', 'Read code from STDIN and print rewritten output') do
93
+ options[:stdin] = true
94
+ end
95
+
96
+ opts.on('-C', '--config PATH', 'Path to config YAML (default: docscribe.yml)') do |v|
97
+ options[:config] = v
98
+ end
99
+
100
+ opts.on('--rbs', 'Use RBS signatures for @param/@return when available (falls back to inference)') do
101
+ options[:rbs] = true
102
+ end
103
+
104
+ opts.on('--sig-dir DIR', 'Add an RBS signature directory (repeatable). Implies --rbs.') do |v|
105
+ options[:rbs] = true
106
+ options[:sig_dirs] << v
107
+ end
108
+
109
+ opts.on('--sorbet', 'Use Sorbet signatures from inline sigs / RBI files when available') do
110
+ options[:sorbet] = true
111
+ end
112
+
113
+ opts.on('--rbi-dir DIR', 'Add a Sorbet RBI directory (repeatable). Implies --sorbet.') do |v|
114
+ options[:sorbet] = true
115
+ options[:rbi_dirs] << v
116
+ end
117
+
118
+ opts.on('--include PATTERN', 'Include PATTERN (method id or file path; glob or /regex/)') do |v|
119
+ route_include_exclude(options, :include, v)
120
+ end
121
+
122
+ opts.on('--exclude PATTERN',
123
+ 'Exclude PATTERN (method id or file path; glob or /regex/). Exclude wins.') do |v|
124
+ route_include_exclude(options, :exclude, v)
125
+ end
126
+
127
+ opts.on('--include-file PATTERN', 'Only process files matching PATTERN (glob or /regex/)') do |v|
128
+ options[:include_file] << v
129
+ end
130
+
131
+ opts.on('--exclude-file PATTERN', 'Skip files matching PATTERN (glob or /regex/). Exclude wins.') do |v|
132
+ options[:exclude_file] << v
133
+ end
134
+
135
+ opts.on('--verbose', 'Print per-file actions') do
136
+ options[:verbose] = true
137
+ end
138
+
139
+ opts.on('-e', '--explain', 'Show detailed reasons for changes') do
140
+ options[:explain] = true
141
+ end
142
+
143
+ opts.on('-v', '--version', 'Print version and exit') do
144
+ require 'docscribe/version'
145
+ puts Docscribe::VERSION
146
+ exit 0
147
+ end
148
+
149
+ opts.on('-h', '--help', 'Show this help') do
150
+ puts opts
151
+ exit 0
152
+ end
153
+ end
154
+
155
+ parser.parse!(argv)
156
+
157
+ if options[:stdin]
158
+ options[:mode] = :stdin
159
+ options[:strategy] = autocorrect_mode || :safe
160
+ elsif autocorrect_mode
161
+ options[:mode] = :write
162
+ options[:strategy] = autocorrect_mode
163
+ else
164
+ options[:mode] = :check
165
+ options[:strategy] = :safe
166
+ end
167
+
168
+ options
169
+ end
170
+
171
+ # Route an include/exclude pattern into method filters or file filters.
172
+ #
173
+ # Regex-looking patterns (`/…/`) are treated as method-id filters.
174
+ # File-like patterns are routed into `*_file`.
175
+ #
176
+ # @note module_function: when included, also defines #route_include_exclude (instance visibility: private)
177
+ # @param [Hash] options mutable parsed options hash
178
+ # @param [Symbol] kind either :include or :exclude
179
+ # @param [String] value raw pattern from the CLI
180
+ # @return [void]
181
+ def route_include_exclude(options, kind, value)
182
+ if looks_like_file_pattern?(value)
183
+ options[:"#{kind}_file"] << value
184
+ else
185
+ options[kind] << value
186
+ end
187
+ end
188
+
189
+ # Heuristically decide whether a pattern looks like a file path or file glob.
190
+ #
191
+ # Regex syntax (`/.../`) is intentionally treated as a method-id pattern,
192
+ # not a file pattern.
193
+ #
194
+ # @note module_function: when included, also defines #looks_like_file_pattern? (instance visibility: private)
195
+ # @param [String] pat pattern passed via CLI
196
+ # @return [Boolean]
197
+ def looks_like_file_pattern?(pat)
198
+ return false if pat.start_with?('/') && pat.end_with?('/') && pat.length >= 2
199
+
200
+ pat.include?('/') || pat.include?('**') || pat.end_with?('.rb')
201
+ end
202
+ end
203
+ end
204
+ end