docscribe 1.5.1 → 1.6.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.
- checksums.yaml +4 -4
- data/README.md +197 -75
- data/exe/docscribe-client +51 -1
- data/lib/docscribe/cli/config_dump.rb +70 -0
- data/lib/docscribe/cli/coverage.rb +313 -0
- data/lib/docscribe/cli/init.rb +81 -4
- data/lib/docscribe/cli/options.rb +16 -0
- data/lib/docscribe/cli/rbs_gen.rb +3 -2
- data/lib/docscribe/cli/run.rb +134 -0
- data/lib/docscribe/cli/sigs.rb +1 -1
- data/lib/docscribe/cli.rb +7 -5
- data/lib/docscribe/config.rb +10 -0
- data/lib/docscribe/infer/behavior.rb +96 -0
- data/lib/docscribe/infer/params.rb +2 -4
- data/lib/docscribe/infer/returns.rb +1 -1
- data/lib/docscribe/infer.rb +35 -0
- data/lib/docscribe/inline_rewriter/doc_builder.rb +23 -10
- data/lib/docscribe/inline_rewriter.rb +142 -20
- data/lib/docscribe/server.rb +258 -29
- data/lib/docscribe/types/overload_selector.rb +79 -0
- data/lib/docscribe/types/provider_chain.rb +25 -2
- data/lib/docscribe/types/signature.rb +5 -0
- data/lib/docscribe/version.rb +1 -1
- metadata +7 -3
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'docscribe/config'
|
|
5
|
+
require 'docscribe/cli/config_builder'
|
|
6
|
+
require 'docscribe/cli/options'
|
|
7
|
+
|
|
8
|
+
module Docscribe
|
|
9
|
+
module CLI
|
|
10
|
+
# Generate documentation coverage report for Ruby files.
|
|
11
|
+
module Coverage
|
|
12
|
+
BANNER = <<~TEXT
|
|
13
|
+
Usage: docscribe coverage [options] [paths]
|
|
14
|
+
|
|
15
|
+
Generate documentation coverage report.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
TEXT
|
|
19
|
+
|
|
20
|
+
# @!attribute [rw] total_methods
|
|
21
|
+
# @return [Integer]
|
|
22
|
+
# @param [Integer] value
|
|
23
|
+
#
|
|
24
|
+
# @!attribute [rw] documented_methods
|
|
25
|
+
# @return [Integer]
|
|
26
|
+
# @param [Integer] value
|
|
27
|
+
#
|
|
28
|
+
# @!attribute [rw] total_params
|
|
29
|
+
# @return [Integer]
|
|
30
|
+
# @param [Integer] value
|
|
31
|
+
#
|
|
32
|
+
# @!attribute [rw] documented_params
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
# @param [Integer] value
|
|
35
|
+
#
|
|
36
|
+
# @!attribute [rw] total_returns
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
# @param [Integer] value
|
|
39
|
+
#
|
|
40
|
+
# @!attribute [rw] documented_returns
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
# @param [Integer] value
|
|
43
|
+
CoverageStats = Struct.new(
|
|
44
|
+
:total_methods, :documented_methods,
|
|
45
|
+
:total_params, :documented_params,
|
|
46
|
+
:total_returns, :documented_returns,
|
|
47
|
+
keyword_init: true
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Struct tracking documentation coverage metrics per file.
|
|
51
|
+
class CoverageStats
|
|
52
|
+
# @return [Integer, Float]
|
|
53
|
+
def method_coverage
|
|
54
|
+
total_methods.zero? ? 100.0 : (documented_methods.to_f / total_methods * 100).round(1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer, Float]
|
|
58
|
+
def param_coverage
|
|
59
|
+
total_params.zero? ? 100.0 : (documented_params.to_f / total_params * 100).round(1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Integer, Float]
|
|
63
|
+
def return_coverage
|
|
64
|
+
total_returns.zero? ? 100.0 : (documented_returns.to_f / total_returns * 100).round(1)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
# @param [Array<String>] argv
|
|
70
|
+
# @return [Integer]
|
|
71
|
+
def run(argv)
|
|
72
|
+
opts = parse_options(argv)
|
|
73
|
+
return 0 if opts[:help]
|
|
74
|
+
|
|
75
|
+
conf = Docscribe::Config.load(opts[:config])
|
|
76
|
+
paths = expand_paths(argv, conf)
|
|
77
|
+
|
|
78
|
+
stats = analyze_coverage(paths, conf)
|
|
79
|
+
|
|
80
|
+
print_report(stats, opts)
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# @private
|
|
87
|
+
# @param [Array<String>] argv
|
|
88
|
+
# @return [Hash<Symbol, Object>]
|
|
89
|
+
def parse_options(argv)
|
|
90
|
+
opts = { config: nil, format: 'text' }
|
|
91
|
+
build_parser(opts).parse!(argv)
|
|
92
|
+
opts
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @private
|
|
96
|
+
# @param [Hash<Symbol, Object>] opts
|
|
97
|
+
# @return [OptionParser]
|
|
98
|
+
def build_parser(opts)
|
|
99
|
+
OptionParser.new do |o|
|
|
100
|
+
o.banner = BANNER
|
|
101
|
+
o.on('--config PATH', 'Path to config file') { |v| opts[:config] = v }
|
|
102
|
+
o.on('--format FORMAT', 'Output format (text, json)') { |v| opts[:format] = v }
|
|
103
|
+
o.on('-h', '--help', 'Show help') do
|
|
104
|
+
opts[:help] = true
|
|
105
|
+
puts o
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @private
|
|
111
|
+
# @param [Array<String>] argv
|
|
112
|
+
# @param [Docscribe::Config] conf
|
|
113
|
+
# @return [Array<String>]
|
|
114
|
+
def expand_paths(argv, conf)
|
|
115
|
+
require 'pathname'
|
|
116
|
+
args = argv.empty? ? ['.'] : argv
|
|
117
|
+
files = expand_files(args)
|
|
118
|
+
files.uniq.sort.select { |p| conf.process_file?(p) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @private
|
|
122
|
+
# @param [Array<String>] args
|
|
123
|
+
# @return [Array<String>]
|
|
124
|
+
def expand_files(args)
|
|
125
|
+
files = [] #: Array[String]
|
|
126
|
+
args.each do |path|
|
|
127
|
+
if File.directory?(path)
|
|
128
|
+
files.concat(Dir.glob(File.join(path, '**', '*.rb')))
|
|
129
|
+
elsif File.file?(path)
|
|
130
|
+
files << path
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
files
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @private
|
|
137
|
+
# @param [Array<String>] paths
|
|
138
|
+
# @param [Docscribe::Config] _conf
|
|
139
|
+
# @return [Docscribe::CLI::Coverage::CoverageStats]
|
|
140
|
+
def analyze_coverage(paths, _conf)
|
|
141
|
+
require 'docscribe/parsing'
|
|
142
|
+
require 'parser/current'
|
|
143
|
+
|
|
144
|
+
stats = init_stats
|
|
145
|
+
paths.each { |path| analyze_file(path, stats) }
|
|
146
|
+
stats
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @private
|
|
150
|
+
# @return [Docscribe::CLI::Coverage::CoverageStats]
|
|
151
|
+
def init_stats
|
|
152
|
+
CoverageStats.new(
|
|
153
|
+
total_methods: 0, documented_methods: 0,
|
|
154
|
+
total_params: 0, documented_params: 0,
|
|
155
|
+
total_returns: 0, documented_returns: 0
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @private
|
|
160
|
+
# @param [String] path
|
|
161
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
162
|
+
# @raise [StandardError]
|
|
163
|
+
# @return [void]
|
|
164
|
+
# @return [nil] if StandardError
|
|
165
|
+
def analyze_file(path, stats)
|
|
166
|
+
src = File.read(path)
|
|
167
|
+
buffer = Parser::Source::Buffer.new(path, source: src)
|
|
168
|
+
ast = Docscribe::Parsing.parse_buffer(buffer)
|
|
169
|
+
analyze_node(ast, stats, src) if ast
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
warn "Skipping #{path}: #{e.message}" if ENV.fetch('DOCSCRIBE_DEBUG', false)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @private
|
|
175
|
+
# @param [Object] node
|
|
176
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
177
|
+
# @param [String] src
|
|
178
|
+
# @return [void]
|
|
179
|
+
def analyze_node(node, stats, src)
|
|
180
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
181
|
+
|
|
182
|
+
analyze_method_node(node, stats, src) if %i[def defs].include?(node.type)
|
|
183
|
+
|
|
184
|
+
node.children.each { |child| analyze_node(child, stats, src) if child.is_a?(Parser::AST::Node) }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @private
|
|
188
|
+
# @param [Object] node
|
|
189
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
190
|
+
# @param [String] src
|
|
191
|
+
# @return [void]
|
|
192
|
+
def analyze_method_node(node, stats, src)
|
|
193
|
+
stats.total_methods += 1
|
|
194
|
+
doc_comment = extract_doc_comment(src, node.loc.expression.line)
|
|
195
|
+
|
|
196
|
+
if doc_comment
|
|
197
|
+
analyze_documented_method(node, stats, doc_comment)
|
|
198
|
+
else
|
|
199
|
+
stats.total_returns += 1
|
|
200
|
+
count_method_params(node, stats)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @private
|
|
205
|
+
# @param [Object] node
|
|
206
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
207
|
+
# @param [String] doc_comment
|
|
208
|
+
# @return [void]
|
|
209
|
+
def analyze_documented_method(node, stats, doc_comment)
|
|
210
|
+
stats.documented_methods += 1
|
|
211
|
+
stats.documented_returns += 1 if doc_comment.match?(/@return\b/)
|
|
212
|
+
stats.total_returns += 1
|
|
213
|
+
|
|
214
|
+
param_matches = doc_comment.scan(/@param\b/)
|
|
215
|
+
param_count = count_params(node)
|
|
216
|
+
stats.total_params += param_count
|
|
217
|
+
stats.documented_params += [param_matches.size, param_count].min
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @private
|
|
221
|
+
# @param [Object] node
|
|
222
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
223
|
+
# @return [void]
|
|
224
|
+
def count_method_params(node, stats)
|
|
225
|
+
stats.total_params += count_params(node)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @private
|
|
229
|
+
# @param [Object] node
|
|
230
|
+
# @return [Integer]
|
|
231
|
+
def count_params(node)
|
|
232
|
+
args_node = node.children[2] || node.children[1]
|
|
233
|
+
return 0 unless args_node.is_a?(Parser::AST::Node) && args_node.type == :args
|
|
234
|
+
|
|
235
|
+
args_node.children.count { |a| %i[arg optarg kwarg kwoptarg restarg].include?(a.type) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# @private
|
|
239
|
+
# @param [String] src
|
|
240
|
+
# @param [Integer] method_line
|
|
241
|
+
# @return [String?]
|
|
242
|
+
def extract_doc_comment(src, method_line)
|
|
243
|
+
lines = src.lines
|
|
244
|
+
comment_lines = collect_comment_lines(lines, method_line)
|
|
245
|
+
comment_lines.empty? ? nil : comment_lines.join
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @private
|
|
249
|
+
# @param [Array<String>] lines
|
|
250
|
+
# @param [Integer] method_line
|
|
251
|
+
# @return [Array<String>]
|
|
252
|
+
def collect_comment_lines(lines, method_line)
|
|
253
|
+
comment_lines = [] #: Array[String]
|
|
254
|
+
idx = method_line - 2
|
|
255
|
+
while idx >= 0
|
|
256
|
+
line = lines[idx]
|
|
257
|
+
break unless line =~ /^\s*#/
|
|
258
|
+
|
|
259
|
+
comment_lines.unshift(line)
|
|
260
|
+
idx -= 1
|
|
261
|
+
end
|
|
262
|
+
comment_lines
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @private
|
|
266
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
267
|
+
# @param [Hash<Symbol, Object>] opts
|
|
268
|
+
# @return [void]
|
|
269
|
+
def print_report(stats, opts)
|
|
270
|
+
case opts[:format]
|
|
271
|
+
when 'json'
|
|
272
|
+
print_json_report(stats)
|
|
273
|
+
else
|
|
274
|
+
print_text_report(stats)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @private
|
|
279
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
280
|
+
# @return [void]
|
|
281
|
+
def print_json_report(stats)
|
|
282
|
+
require 'json'
|
|
283
|
+
puts JSON.pretty_generate(
|
|
284
|
+
methods: report_entry(stats.total_methods, stats.documented_methods, stats.method_coverage),
|
|
285
|
+
params: report_entry(stats.total_params, stats.documented_params, stats.param_coverage),
|
|
286
|
+
returns: report_entry(stats.total_returns, stats.documented_returns, stats.return_coverage)
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @private
|
|
291
|
+
# @param [Integer] total
|
|
292
|
+
# @param [Integer] documented
|
|
293
|
+
# @param [Integer, Float] coverage
|
|
294
|
+
# @return [Hash<Symbol, Integer, Float>]
|
|
295
|
+
def report_entry(total, documented, coverage)
|
|
296
|
+
{ total: total, documented: documented, coverage: coverage }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# @private
|
|
300
|
+
# @param [Docscribe::CLI::Coverage::CoverageStats] stats
|
|
301
|
+
# @return [void]
|
|
302
|
+
def print_text_report(stats)
|
|
303
|
+
puts 'Documentation Coverage Report'
|
|
304
|
+
puts '============================='
|
|
305
|
+
puts
|
|
306
|
+
puts "Methods: #{stats.documented_methods}/#{stats.total_methods} (#{stats.method_coverage}%)"
|
|
307
|
+
puts "Params: #{stats.documented_params}/#{stats.total_params} (#{stats.param_coverage}%)"
|
|
308
|
+
puts "Returns: #{stats.documented_returns}/#{stats.total_returns} (#{stats.return_coverage}%)"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
data/lib/docscribe/cli/init.rb
CHANGED
|
@@ -58,7 +58,7 @@ module Docscribe
|
|
|
58
58
|
# @private
|
|
59
59
|
# @return [Hash<Symbol, String, Boolean>]
|
|
60
60
|
def default_init_options
|
|
61
|
-
{ config: 'docscribe.yml', force: false, stdout: false, help: false }
|
|
61
|
+
{ config: 'docscribe.yml', force: false, stdout: false, help: false, pre_commit: false }
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Build and return an OptionParser for the init command.
|
|
@@ -69,9 +69,7 @@ module Docscribe
|
|
|
69
69
|
def build_init_parser(opts)
|
|
70
70
|
OptionParser.new do |o|
|
|
71
71
|
o.banner = BANNER
|
|
72
|
-
o
|
|
73
|
-
o.on('-f', '--force', 'Overwrite if the file already exists') { opts[:force] = true }
|
|
74
|
-
o.on('--stdout', 'Print config template to STDOUT instead of writing a file') { opts[:stdout] = true }
|
|
72
|
+
add_init_options(o, opts)
|
|
75
73
|
o.on('-h', '--help', 'Show this help') do
|
|
76
74
|
opts[:help] = true
|
|
77
75
|
puts o
|
|
@@ -79,6 +77,19 @@ module Docscribe
|
|
|
79
77
|
end
|
|
80
78
|
end
|
|
81
79
|
|
|
80
|
+
# Add init-specific CLI options to the parser.
|
|
81
|
+
#
|
|
82
|
+
# @private
|
|
83
|
+
# @param [OptionParser] parser
|
|
84
|
+
# @param [Hash<Symbol, Object>] opts options hash
|
|
85
|
+
# @return [void]
|
|
86
|
+
def add_init_options(parser, opts)
|
|
87
|
+
parser.on('--config PATH', 'Where to write the config (default: docscribe.yml)') { |v| opts[:config] = v }
|
|
88
|
+
parser.on('-f', '--force', 'Overwrite if the file already exists') { opts[:force] = true }
|
|
89
|
+
parser.on('--stdout', 'Print config template to STDOUT instead of writing a file') { opts[:stdout] = true }
|
|
90
|
+
parser.on('--pre-commit', 'Install pre-commit hook for docscribe check') { opts[:pre_commit] = true }
|
|
91
|
+
end
|
|
92
|
+
|
|
82
93
|
# Write the config template to a file.
|
|
83
94
|
#
|
|
84
95
|
# @private
|
|
@@ -86,6 +97,8 @@ module Docscribe
|
|
|
86
97
|
# @param [String] yaml config template content
|
|
87
98
|
# @return [Integer] exit code
|
|
88
99
|
def write_init_config(opts, yaml)
|
|
100
|
+
return install_pre_commit_hook(opts) if opts[:pre_commit]
|
|
101
|
+
|
|
89
102
|
path = opts[:config]
|
|
90
103
|
if File.exist?(path) && !opts[:force]
|
|
91
104
|
warn "Config already exists: #{path} (use --force to overwrite)"
|
|
@@ -96,6 +109,70 @@ module Docscribe
|
|
|
96
109
|
puts "Created: #{path}"
|
|
97
110
|
0
|
|
98
111
|
end
|
|
112
|
+
|
|
113
|
+
# @private
|
|
114
|
+
# @param [Hash<Symbol, Object>] opts
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
def install_pre_commit_hook(opts)
|
|
117
|
+
hook_dir = File.join('.git', 'hooks')
|
|
118
|
+
hook_path = File.join(hook_dir, 'pre-commit')
|
|
119
|
+
|
|
120
|
+
return 1 unless hook_preconditions_met?(hook_dir, hook_path, opts)
|
|
121
|
+
|
|
122
|
+
File.write(hook_path, pre_commit_hook_script)
|
|
123
|
+
File.chmod(0o755, hook_path)
|
|
124
|
+
puts "Installed pre-commit hook: #{hook_path}"
|
|
125
|
+
0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check pre-commit hook installation preconditions.
|
|
129
|
+
#
|
|
130
|
+
# @private
|
|
131
|
+
# @param [String] hook_dir
|
|
132
|
+
# @param [String] hook_path
|
|
133
|
+
# @param [Hash<Symbol, Object>] opts
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def hook_preconditions_met?(hook_dir, hook_path, opts)
|
|
136
|
+
unless Dir.exist?(hook_dir)
|
|
137
|
+
warn 'No .git/hooks directory found. Are you in a git repository?'
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if File.exist?(hook_path) && !opts[:force]
|
|
142
|
+
warn "Pre-commit hook already exists: #{hook_path} (use --force to overwrite)"
|
|
143
|
+
return false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Generate the pre-commit hook shell script content.
|
|
150
|
+
#
|
|
151
|
+
# @private
|
|
152
|
+
# @return [String]
|
|
153
|
+
def pre_commit_hook_script
|
|
154
|
+
<<~HOOK
|
|
155
|
+
#!/bin/sh
|
|
156
|
+
# Docscribe pre-commit hook
|
|
157
|
+
# Runs docscribe check on staged Ruby files
|
|
158
|
+
|
|
159
|
+
STAGED_RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.rb')
|
|
160
|
+
if [ -z "$STAGED_RUBY_FILES" ]; then
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
echo "Checking documentation with docscribe..."
|
|
165
|
+
bundle exec docscribe check $STAGED_RUBY_FILES
|
|
166
|
+
RESULT=$?
|
|
167
|
+
|
|
168
|
+
if [ $RESULT -ne 0 ]; then
|
|
169
|
+
echo "Documentation check failed. Please add documentation before committing."
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
exit 0
|
|
174
|
+
HOOK
|
|
175
|
+
end
|
|
99
176
|
end
|
|
100
177
|
end
|
|
101
178
|
end
|
|
@@ -27,6 +27,7 @@ module Docscribe
|
|
|
27
27
|
keep_descriptions: false,
|
|
28
28
|
no_boilerplate: false,
|
|
29
29
|
progress: false,
|
|
30
|
+
parallel: false,
|
|
30
31
|
server: false
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
@@ -76,6 +77,8 @@ module Docscribe
|
|
|
76
77
|
-q, --quiet Only show status, no details
|
|
77
78
|
--format FORMAT Output format: text (default), json, or sarif
|
|
78
79
|
|
|
80
|
+
Performance:
|
|
81
|
+
--parallel Process files in parallel (default: sequential)
|
|
79
82
|
|
|
80
83
|
Other:
|
|
81
84
|
-v, --version Print version and exit
|
|
@@ -335,6 +338,7 @@ module Docscribe
|
|
|
335
338
|
def define_output_options(opts, options)
|
|
336
339
|
define_verbose_option(opts, options)
|
|
337
340
|
define_progress_option(opts, options)
|
|
341
|
+
define_parallel_option(opts, options)
|
|
338
342
|
define_explain_option(opts, options)
|
|
339
343
|
define_quiet_option(opts, options)
|
|
340
344
|
define_format_option(opts, options)
|
|
@@ -367,6 +371,18 @@ module Docscribe
|
|
|
367
371
|
end
|
|
368
372
|
end
|
|
369
373
|
|
|
374
|
+
# Define parallel option
|
|
375
|
+
#
|
|
376
|
+
# @note module_function: defines #define_parallel_option (visibility: private)
|
|
377
|
+
# @param [Hash<Symbol, Object>] opts the option parser to configure
|
|
378
|
+
# @param [Hash<Symbol, Object>] options mutable parsed options hash
|
|
379
|
+
# @return [void]
|
|
380
|
+
def define_parallel_option(opts, options)
|
|
381
|
+
opts.on('--parallel', 'Process files in parallel (default: sequential)') do
|
|
382
|
+
options[:parallel] = true
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
370
386
|
# Define explain option
|
|
371
387
|
#
|
|
372
388
|
# @note module_function: defines #define_explain_option (visibility: private)
|
|
@@ -85,7 +85,8 @@ module Docscribe
|
|
|
85
85
|
# @!attribute [rw] inside_sclass
|
|
86
86
|
# @return [Boolean]
|
|
87
87
|
# @param [Boolean] value
|
|
88
|
-
WalkContext = Struct.new(:containers, :method_defs, :path, :comment_map, :src_lines, :inside_sclass,
|
|
88
|
+
WalkContext = Struct.new(:containers, :method_defs, :path, :comment_map, :src_lines, :inside_sclass,
|
|
89
|
+
keyword_init: true)
|
|
89
90
|
|
|
90
91
|
class << self
|
|
91
92
|
# @param [Array<String>] argv
|
|
@@ -167,7 +168,7 @@ module Docscribe
|
|
|
167
168
|
# @return [Boolean] if StandardError
|
|
168
169
|
def generate_for_file(path, options)
|
|
169
170
|
process_source?(File.read(path), path, options)
|
|
170
|
-
rescue Parser::SyntaxError => e
|
|
171
|
+
rescue Parser::SyntaxError => e
|
|
171
172
|
warn "Syntax error in #{path}: #{e.message}"
|
|
172
173
|
false
|
|
173
174
|
rescue StandardError => e
|
data/lib/docscribe/cli/run.rb
CHANGED
|
@@ -35,6 +35,8 @@ module Docscribe
|
|
|
35
35
|
processed: 0
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
38
|
+
PARALLEL_DEFAULT_THREADS = 4
|
|
39
|
+
|
|
38
40
|
CLI_OVERRIDE_KEYS = %i[
|
|
39
41
|
keep_descriptions no_boilerplate
|
|
40
42
|
include exclude include_file exclude_file
|
|
@@ -382,6 +384,8 @@ module Docscribe
|
|
|
382
384
|
# @param [Array<String>] paths Ruby file paths to process
|
|
383
385
|
# @return [Integer] process exit code
|
|
384
386
|
def run_files(options:, conf:, paths:)
|
|
387
|
+
return run_files_parallel(options: options, conf: conf, paths: paths) if options[:parallel] && paths.size > 1
|
|
388
|
+
|
|
385
389
|
$stdout.sync = true
|
|
386
390
|
|
|
387
391
|
state = initial_run_state
|
|
@@ -397,6 +401,136 @@ module Docscribe
|
|
|
397
401
|
run_exit_code(options, state)
|
|
398
402
|
end
|
|
399
403
|
|
|
404
|
+
# Process files in parallel using a Thread pool.
|
|
405
|
+
#
|
|
406
|
+
# @param [Docscribe::CLI::Formatters::opts] options parsed CLI options
|
|
407
|
+
# @param [Docscribe::Config] conf effective config
|
|
408
|
+
# @param [Array<String>] paths Ruby file paths to process
|
|
409
|
+
# @return [Integer] process exit code
|
|
410
|
+
def run_files_parallel(options:, conf:, paths:)
|
|
411
|
+
$stdout.sync = true
|
|
412
|
+
state = initial_run_state
|
|
413
|
+
state[:total] = paths.size
|
|
414
|
+
run_parallel_workers(options, conf, paths, state)
|
|
415
|
+
finalize_run(options, state)
|
|
416
|
+
run_exit_code(options, state)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# @param [Object] options
|
|
420
|
+
# @param [Object] conf
|
|
421
|
+
# @param [Array<String>] paths
|
|
422
|
+
# @param [Docscribe::CLI::Formatters::state] state
|
|
423
|
+
# @return [void]
|
|
424
|
+
def run_parallel_workers(options, conf, paths, state)
|
|
425
|
+
pool = {
|
|
426
|
+
paths: paths, state: state, options: options, conf: conf,
|
|
427
|
+
pwd: Pathname.pwd, mutex: Mutex.new, error_mutex: Mutex.new
|
|
428
|
+
}
|
|
429
|
+
thread_count = [ENV.fetch('DOCSCRIBE_THREADS', PARALLEL_DEFAULT_THREADS).to_i, 1].max
|
|
430
|
+
thread_count.times.map { parallel_worker(pool) }.each(&:join)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# @param [Hash<Symbol, Object>] pool
|
|
434
|
+
# @return [Thread]
|
|
435
|
+
def parallel_worker(pool)
|
|
436
|
+
Thread.new do
|
|
437
|
+
loop do
|
|
438
|
+
path = fetch_work(pool[:paths], pool[:mutex]) or break
|
|
439
|
+
run_and_merge(path, pool)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# @param [Array<String>] paths
|
|
445
|
+
# @param [Thread::Mutex] mutex
|
|
446
|
+
# @return [String?]
|
|
447
|
+
def fetch_work(paths, mutex)
|
|
448
|
+
path = nil
|
|
449
|
+
mutex.synchronize { path = paths.shift }
|
|
450
|
+
path
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# @param [String] path
|
|
454
|
+
# @param [Hash<Symbol, Object>] pool
|
|
455
|
+
# @raise [StandardError]
|
|
456
|
+
# @return [void]
|
|
457
|
+
# @return [Object] if StandardError
|
|
458
|
+
def run_and_merge(path, pool)
|
|
459
|
+
local_state = initial_run_state
|
|
460
|
+
process_one_file(path, options: pool[:options], conf: pool[:conf], pwd: pool[:pwd], state: local_state)
|
|
461
|
+
merge_state(pool[:state], local_state, pool[:error_mutex])
|
|
462
|
+
rescue StandardError => e
|
|
463
|
+
handle_worker_error(e, path, pool[:state], pool[:error_mutex])
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# @param [StandardError] exception
|
|
467
|
+
# @param [String] path
|
|
468
|
+
# @param [Docscribe::CLI::Formatters::state] state
|
|
469
|
+
# @param [Thread::Mutex] error_mutex
|
|
470
|
+
# @return [void]
|
|
471
|
+
def handle_worker_error(exception, path, state, error_mutex)
|
|
472
|
+
error_mutex.synchronize do
|
|
473
|
+
state[:had_errors] = true
|
|
474
|
+
state[:error_paths] << path
|
|
475
|
+
state[:error_messages][path] = "#{exception.class}: #{exception.message}"
|
|
476
|
+
end
|
|
477
|
+
$stderr.print('E')
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Merge a thread-local state into the shared state under mutex protection.
|
|
481
|
+
#
|
|
482
|
+
# @param [Docscribe::CLI::Formatters::state] target shared state to merge into
|
|
483
|
+
# @param [Docscribe::CLI::Formatters::state] source thread-local state to merge from
|
|
484
|
+
# @param [Thread::Mutex] mutex mutex protecting the target state
|
|
485
|
+
# @return [void]
|
|
486
|
+
def merge_state(target, source, mutex)
|
|
487
|
+
mutex.synchronize do
|
|
488
|
+
merge_state_flags(target, source)
|
|
489
|
+
merge_state_counts(target, source)
|
|
490
|
+
merge_state_arrays(target, source)
|
|
491
|
+
merge_state_hashes(target, source)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# @param [Docscribe::CLI::Formatters::state] target
|
|
496
|
+
# @param [Docscribe::CLI::Formatters::state] source
|
|
497
|
+
# @return [void]
|
|
498
|
+
def merge_state_flags(target, source)
|
|
499
|
+
target[:changed] ||= source[:changed]
|
|
500
|
+
target[:had_errors] ||= source[:had_errors]
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# @param [Docscribe::CLI::Formatters::state] target
|
|
504
|
+
# @param [Docscribe::CLI::Formatters::state] source
|
|
505
|
+
# @return [void]
|
|
506
|
+
def merge_state_counts(target, source)
|
|
507
|
+
target[:checked_ok] += source[:checked_ok]
|
|
508
|
+
target[:checked_fail] += source[:checked_fail]
|
|
509
|
+
target[:corrected] += source[:corrected]
|
|
510
|
+
target[:processed] += source[:processed]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# @param [Docscribe::CLI::Formatters::state] target
|
|
514
|
+
# @param [Docscribe::CLI::Formatters::state] source
|
|
515
|
+
# @return [void]
|
|
516
|
+
def merge_state_arrays(target, source)
|
|
517
|
+
target[:corrected_paths].concat(source[:corrected_paths])
|
|
518
|
+
target[:fail_paths].concat(source[:fail_paths])
|
|
519
|
+
target[:error_paths].concat(source[:error_paths])
|
|
520
|
+
target[:type_mismatch_paths].concat(source[:type_mismatch_paths])
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# @param [Docscribe::CLI::Formatters::state] target
|
|
524
|
+
# @param [Docscribe::CLI::Formatters::state] source
|
|
525
|
+
# @return [void]
|
|
526
|
+
def merge_state_hashes(target, source)
|
|
527
|
+
%i[corrected_changes fail_changes error_messages type_mismatch_changes].each do |key|
|
|
528
|
+
t = target[key] #: Hash[String, untyped]
|
|
529
|
+
s = source[key] #: Hash[String, untyped]
|
|
530
|
+
s.each { |k, v| t[k] = v }
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
400
534
|
# @param [Docscribe::CLI::Formatters::opts] options
|
|
401
535
|
# @return [Hash<String, Object>]
|
|
402
536
|
def extract_cli_overrides(options)
|
data/lib/docscribe/cli/sigs.rb
CHANGED
|
@@ -169,7 +169,7 @@ module Docscribe
|
|
|
169
169
|
return unless ast
|
|
170
170
|
|
|
171
171
|
walk_for_methods(ast, [], methods, path)
|
|
172
|
-
rescue Parser::SyntaxError => e
|
|
172
|
+
rescue Parser::SyntaxError => e
|
|
173
173
|
warn "Syntax error in #{path}: #{e.message}"
|
|
174
174
|
rescue StandardError => e
|
|
175
175
|
warn "Error parsing #{path}: #{e.class}: #{e.message}"
|
data/lib/docscribe/cli.rb
CHANGED
|
@@ -18,13 +18,15 @@ module Docscribe
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
COMMANDS = {
|
|
21
|
-
'
|
|
21
|
+
'check_for_comments' => :CheckForComments,
|
|
22
|
+
'config' => :ConfigDump,
|
|
23
|
+
'coverage' => :Coverage,
|
|
22
24
|
'generate' => :Generate,
|
|
23
|
-
'
|
|
25
|
+
'init' => :Init,
|
|
24
26
|
'rbs' => :RbsGen,
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
27
|
+
'server' => :ServerCmd,
|
|
28
|
+
'sigs' => :Sigs,
|
|
29
|
+
'update_types' => :UpdateTypes
|
|
28
30
|
}.freeze
|
|
29
31
|
|
|
30
32
|
private
|
data/lib/docscribe/config.rb
CHANGED
|
@@ -26,6 +26,16 @@ module Docscribe
|
|
|
26
26
|
@raw = deep_merge(DEFAULT, raw)
|
|
27
27
|
@config_path = config_path
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# @return [Object]
|
|
31
|
+
def to_h
|
|
32
|
+
hash = {} #: Hash[String, Object]
|
|
33
|
+
instance_variables.each do |var|
|
|
34
|
+
key = var.to_s.sub('@', '')
|
|
35
|
+
hash[key] = instance_variable_get(var)
|
|
36
|
+
end
|
|
37
|
+
hash
|
|
38
|
+
end
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
|