docscribe 1.4.0 → 1.4.2
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 +194 -8
- data/lib/docscribe/cli/config_builder.rb +125 -35
- data/lib/docscribe/cli/generate.rb +288 -117
- data/lib/docscribe/cli/init.rb +49 -13
- data/lib/docscribe/cli/options.rb +303 -127
- data/lib/docscribe/cli/run.rb +391 -135
- data/lib/docscribe/cli.rb +23 -5
- data/lib/docscribe/config/defaults.rb +11 -11
- data/lib/docscribe/config/emit.rb +1 -0
- data/lib/docscribe/config/filtering.rb +24 -11
- data/lib/docscribe/config/loader.rb +7 -4
- data/lib/docscribe/config/plugin.rb +1 -0
- data/lib/docscribe/config/rbs.rb +31 -22
- data/lib/docscribe/config/sorbet.rb +41 -15
- data/lib/docscribe/config/sorting.rb +1 -0
- data/lib/docscribe/config/template.rb +1 -0
- data/lib/docscribe/config/utils.rb +3 -2
- data/lib/docscribe/config.rb +1 -0
- data/lib/docscribe/infer/constants.rb +15 -0
- data/lib/docscribe/infer/literals.rb +43 -25
- data/lib/docscribe/infer/names.rb +24 -15
- data/lib/docscribe/infer/params.rb +52 -6
- data/lib/docscribe/infer/raises.rb +24 -14
- data/lib/docscribe/infer/returns.rb +365 -182
- data/lib/docscribe/infer.rb +10 -9
- data/lib/docscribe/inline_rewriter/collector.rb +766 -375
- data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
- data/lib/docscribe/inline_rewriter/doc_builder.rb +1495 -583
- data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
- data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
- data/lib/docscribe/inline_rewriter.rb +1052 -457
- data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
- data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
- data/lib/docscribe/plugin/registry.rb +34 -7
- data/lib/docscribe/plugin.rb +48 -17
- data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
- data/lib/docscribe/types/rbs/provider.rb +75 -26
- data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
- data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
- data/lib/docscribe/version.rb +1 -1
- metadata +2 -2
|
@@ -42,112 +42,176 @@ module Docscribe
|
|
|
42
42
|
# @param [Symbol, nil] strategy :safe or :aggressive
|
|
43
43
|
# @param [Boolean, nil] rewrite compatibility alias for aggressive strategy
|
|
44
44
|
# @param [Boolean, nil] merge compatibility alias for safe strategy
|
|
45
|
-
# @param [
|
|
46
|
-
# @param [String] file source name used for parser locations/debugging
|
|
45
|
+
# @param [Hash] options additional keyword arguments forwarded to rewrite_with_report
|
|
47
46
|
# @return [String]
|
|
48
|
-
def insert_comments(code, strategy: nil, rewrite: nil, merge: nil,
|
|
47
|
+
def insert_comments(code, strategy: nil, rewrite: nil, merge: nil, **options)
|
|
49
48
|
strategy = normalize_strategy(strategy: strategy, rewrite: rewrite, merge: merge)
|
|
50
49
|
|
|
51
|
-
rewrite_with_report(
|
|
52
|
-
code,
|
|
53
|
-
strategy: strategy,
|
|
54
|
-
config: config,
|
|
55
|
-
file: file
|
|
56
|
-
)[:output]
|
|
50
|
+
rewrite_with_report(code, strategy: strategy, **options)[:output]
|
|
57
51
|
end
|
|
58
52
|
|
|
59
53
|
# Rewrite source and return both output and structured change information.
|
|
60
54
|
#
|
|
61
|
-
# The result hash includes:
|
|
62
|
-
# - `:output` => rewritten source
|
|
63
|
-
# - `:changes` => structured change records used by CLI explanation output
|
|
64
|
-
#
|
|
65
55
|
# @param [String] code Ruby source
|
|
66
56
|
# @param [Symbol, nil] strategy :safe or :aggressive
|
|
67
57
|
# @param [Boolean, nil] rewrite compatibility alias for aggressive strategy
|
|
68
58
|
# @param [Boolean, nil] merge compatibility alias for safe strategy
|
|
69
|
-
# @param [
|
|
70
|
-
# @param [
|
|
71
|
-
# @param [nil] core_rbs_provider Param documentation.
|
|
59
|
+
# @param [Hash] **options remaining options (config:, file:, core_rbs_provider:)
|
|
60
|
+
# @param [Hash] options additional keyword arguments forwarded to downstream helpers
|
|
72
61
|
# @raise [Docscribe::ParseError]
|
|
73
62
|
# @raise [StandardError]
|
|
74
63
|
# @return [Hash]
|
|
75
|
-
def rewrite_with_report(code, strategy: nil, rewrite: nil, merge: nil,
|
|
76
|
-
core_rbs_provider: nil, file: '(inline)')
|
|
64
|
+
def rewrite_with_report(code, strategy: nil, rewrite: nil, merge: nil, **options)
|
|
77
65
|
strategy = normalize_strategy(strategy: strategy, rewrite: rewrite, merge: merge)
|
|
78
66
|
validate_strategy!(strategy)
|
|
67
|
+
parsed = setup_rewrite_env(code, options)
|
|
68
|
+
pipeline = build_rewrite_pipeline(parsed[:buffer], parsed[:ast])
|
|
69
|
+
dispatch_rewrite_insertions(pipeline, parsed[:buffer],
|
|
70
|
+
config: parsed[:config], signature_provider: parsed[:signature_provider],
|
|
71
|
+
core_rbs_provider: parsed[:core_rbs_provider], strategy: strategy,
|
|
72
|
+
file: parsed[:file])
|
|
73
|
+
{ output: pipeline[:rewriter].process, changes: pipeline[:changes] }
|
|
74
|
+
end
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
# Build the insertion pipeline: collector, plugin insertions, dedup, rewriter, merge_inserts, changes.
|
|
77
|
+
#
|
|
78
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
79
|
+
# @param [Object] ast the parsed AST of the source code
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
def build_rewrite_pipeline(buffer, ast)
|
|
82
|
+
all = collect_insertions(buffer, ast)
|
|
83
|
+
method_overrides_by_pos = {} #: Hash[Integer, untyped]
|
|
84
|
+
all = deduplicate_insertions(all, method_overrides_by_pos: method_overrides_by_pos)
|
|
85
|
+
rewriter = Parser::Source::TreeRewriter.new(buffer)
|
|
86
|
+
merge_inserts = Hash.new { |h, k| h[k] = [] } #: Hash[Integer, untyped]
|
|
87
|
+
changes = [] #: Array[untyped]
|
|
88
|
+
|
|
89
|
+
{ all: all, method_overrides_by_pos: method_overrides_by_pos, rewriter: rewriter,
|
|
90
|
+
merge_inserts: merge_inserts, changes: changes }
|
|
91
|
+
end
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
# Dispatch all insertions to the appropriate handler.
|
|
94
|
+
#
|
|
95
|
+
# @param [Object] pipeline the pipeline hash with rewriter, insertions, and tracking state
|
|
96
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
97
|
+
# @param [Hash] options additional kwargs (config, signature_provider, core_rbs_provider, strategy, file)
|
|
98
|
+
# @return [Object]
|
|
99
|
+
def dispatch_rewrite_insertions(pipeline, buffer, **options)
|
|
100
|
+
pipeline[:all].sort_by { |(kind, ins)| plugin_insertion_pos(kind, ins) }
|
|
101
|
+
.reverse_each do |kind, ins|
|
|
102
|
+
case kind
|
|
103
|
+
when :method then dispatch_method_insertion(ins, pipeline, buffer, **options)
|
|
104
|
+
when :attr then dispatch_attr_insertion(ins, pipeline, buffer, **options)
|
|
105
|
+
when :plugin then dispatch_plugin_insertion(ins, pipeline, buffer, **options)
|
|
106
|
+
end
|
|
91
107
|
end
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
apply_merge_inserts!(rewriter: pipeline[:rewriter], buffer: buffer, merge_inserts: pipeline[:merge_inserts])
|
|
110
|
+
end
|
|
95
111
|
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
# Dispatch a method insertion.
|
|
113
|
+
#
|
|
114
|
+
# @private
|
|
115
|
+
# @param [Object] ins
|
|
116
|
+
# @param [Hash] pipeline
|
|
117
|
+
# @param [Object] buffer
|
|
118
|
+
# @param [Hash] options
|
|
119
|
+
# @return [void]
|
|
120
|
+
def dispatch_method_insertion(ins, pipeline, buffer, **options)
|
|
121
|
+
pos = plugin_insertion_pos(:method, ins)
|
|
122
|
+
method_override = pipeline[:method_overrides_by_pos][pos]
|
|
123
|
+
|
|
124
|
+
apply_method_insertion!(
|
|
125
|
+
rewriter: pipeline[:rewriter], buffer: buffer, insertion: ins,
|
|
126
|
+
config: options[:config], signature_provider: options[:signature_provider],
|
|
127
|
+
core_rbs_provider: options[:core_rbs_provider], strategy: options[:strategy],
|
|
128
|
+
changes: pipeline[:changes], file: options[:file],
|
|
129
|
+
method_override: method_override
|
|
130
|
+
)
|
|
131
|
+
end
|
|
98
132
|
|
|
99
|
-
|
|
100
|
-
|
|
133
|
+
# Dispatch an attr insertion.
|
|
134
|
+
#
|
|
135
|
+
# @private
|
|
136
|
+
# @param [Object] ins
|
|
137
|
+
# @param [Hash] pipeline
|
|
138
|
+
# @param [Object] buffer
|
|
139
|
+
# @param [Hash] options
|
|
140
|
+
# @return [void]
|
|
141
|
+
def dispatch_attr_insertion(ins, pipeline, buffer, **options)
|
|
142
|
+
apply_attr_insertion!(
|
|
143
|
+
rewriter: pipeline[:rewriter], buffer: buffer, insertion: ins,
|
|
144
|
+
config: options[:config], signature_provider: options[:signature_provider],
|
|
145
|
+
strategy: options[:strategy], merge_inserts: pipeline[:merge_inserts]
|
|
146
|
+
)
|
|
147
|
+
end
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
# Dispatch a plugin insertion.
|
|
150
|
+
#
|
|
151
|
+
# @private
|
|
152
|
+
# @param [Object] ins
|
|
153
|
+
# @param [Hash] pipeline
|
|
154
|
+
# @param [Object] buffer
|
|
155
|
+
# @param [Hash] options
|
|
156
|
+
# @return [void]
|
|
157
|
+
def dispatch_plugin_insertion(ins, pipeline, buffer, **options)
|
|
158
|
+
apply_plugin_insertion!(
|
|
159
|
+
rewriter: pipeline[:rewriter], buffer: buffer, insertion: ins,
|
|
160
|
+
strategy: options[:strategy], config: options[:config]
|
|
161
|
+
)
|
|
162
|
+
end
|
|
109
163
|
|
|
110
|
-
|
|
111
|
-
.reverse_each do |kind, ins|
|
|
112
|
-
case kind
|
|
113
|
-
when :method
|
|
114
|
-
apply_method_insertion!(
|
|
115
|
-
rewriter: rewriter,
|
|
116
|
-
buffer: buffer,
|
|
117
|
-
insertion: ins,
|
|
118
|
-
config: config,
|
|
119
|
-
signature_provider: signature_provider,
|
|
120
|
-
core_rbs_provider: core_rbs_provider,
|
|
121
|
-
strategy: strategy,
|
|
122
|
-
changes: changes,
|
|
123
|
-
file: file.to_s
|
|
124
|
-
)
|
|
125
|
-
when :attr
|
|
126
|
-
apply_attr_insertion!(
|
|
127
|
-
rewriter: rewriter,
|
|
128
|
-
buffer: buffer,
|
|
129
|
-
insertion: ins,
|
|
130
|
-
config: config,
|
|
131
|
-
signature_provider: signature_provider,
|
|
132
|
-
strategy: strategy,
|
|
133
|
-
merge_inserts: merge_inserts
|
|
134
|
-
)
|
|
135
|
-
when :plugin
|
|
136
|
-
apply_plugin_insertion!(
|
|
137
|
-
rewriter: rewriter,
|
|
138
|
-
buffer: buffer,
|
|
139
|
-
insertion: ins,
|
|
140
|
-
strategy: strategy
|
|
141
|
-
)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
164
|
+
private
|
|
144
165
|
|
|
145
|
-
|
|
166
|
+
# Setup the parsing environment for rewrite_with_report.
|
|
167
|
+
# @private
|
|
168
|
+
# @param [Object] code the Ruby source code string to parse and rewrite
|
|
169
|
+
# @param [Object] options hash containing :config, :file, and :core_rbs_provider
|
|
170
|
+
# @raise [Docscribe::ParseError]
|
|
171
|
+
# @return [Hash]
|
|
172
|
+
def setup_rewrite_env(code, options)
|
|
173
|
+
config = options[:config] || Docscribe::Config.load
|
|
174
|
+
file = (options[:file] || '(inline)').to_s
|
|
175
|
+
core_rbs_provider = options[:core_rbs_provider]
|
|
176
|
+
buffer = Parser::Source::Buffer.new(file, source: code)
|
|
177
|
+
ast = Docscribe::Parsing.parse_buffer(buffer)
|
|
178
|
+
raise Docscribe::ParseError, "Failed to parse #{file}" unless ast
|
|
146
179
|
|
|
147
|
-
{
|
|
180
|
+
{ config: config, file: file, buffer: buffer, ast: ast,
|
|
181
|
+
signature_provider: build_signature_provider(config, code, file),
|
|
182
|
+
core_rbs_provider: load_core_rbs_provider(config, core_rbs_provider) }
|
|
148
183
|
end
|
|
149
184
|
|
|
150
|
-
|
|
185
|
+
# Load core RBS provider from config with safe fallback.
|
|
186
|
+
#
|
|
187
|
+
# @private
|
|
188
|
+
# @param [Object] config the active Docscribe::Config
|
|
189
|
+
# @param [Object] core_rbs_provider optional externally-provided core RBS provider
|
|
190
|
+
# @raise [StandardError]
|
|
191
|
+
# @return [Object]
|
|
192
|
+
# @return [nil] if StandardError
|
|
193
|
+
def load_core_rbs_provider(config, core_rbs_provider)
|
|
194
|
+
core_rbs_provider || (config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil)
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
warn "Docscribe: failed to load core RBS provider: #{e.message}" if ENV.fetch('DOCSCRIBE_DEBUG', false)
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Collect insertions from collector and plugins into a combined list.
|
|
201
|
+
# @private
|
|
202
|
+
# @param [Object] buffer the source buffer to collect insertions from
|
|
203
|
+
# @param [Object] ast the parsed AST to traverse for collection
|
|
204
|
+
# @return [Object]
|
|
205
|
+
def collect_insertions(buffer, ast)
|
|
206
|
+
collector = Docscribe::InlineRewriter::Collector.new(buffer)
|
|
207
|
+
collector.process(ast)
|
|
208
|
+
plugin_insertions = Docscribe::Plugin.run_collector_plugins(ast, buffer)
|
|
209
|
+
method_insertions = collector.insertions
|
|
210
|
+
attr_insertions = collector.respond_to?(:attr_insertions) ? collector.attr_insertions : [] #: Array[untyped]
|
|
211
|
+
method_insertions.map { |i| [:method, i] } +
|
|
212
|
+
attr_insertions.map { |i| [:attr, i] } +
|
|
213
|
+
plugin_insertions.map { |i| [:plugin, i] }
|
|
214
|
+
end
|
|
151
215
|
|
|
152
216
|
# Deduplicate insertions by source position.
|
|
153
217
|
#
|
|
@@ -161,59 +225,238 @@ module Docscribe
|
|
|
161
225
|
#
|
|
162
226
|
# @private
|
|
163
227
|
# @param [Array<Array(Symbol,Object)>] insertions tagged insertion list
|
|
228
|
+
# @param [nil] method_overrides_by_pos method-level overrides keyed by insertion position
|
|
164
229
|
# @return [Array<Array(Symbol,Object)>]
|
|
165
|
-
def deduplicate_insertions(insertions)
|
|
166
|
-
|
|
230
|
+
def deduplicate_insertions(insertions, method_overrides_by_pos: nil)
|
|
231
|
+
group_by_position(insertions).each_with_object([]) do |(pos, items), result|
|
|
232
|
+
process_dedup_group(pos, items, result, method_overrides_by_pos)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
167
235
|
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
# Process one group of deduplication at a given position.
|
|
237
|
+
# @private
|
|
238
|
+
# @param [Object] pos the source begin_pos for the group
|
|
239
|
+
# @param [Object] items all [kind, insertion] pairs at this position
|
|
240
|
+
# @param [Object] result the accumulator array for surviving insertions
|
|
241
|
+
# @param [Object] method_overrides_by_pos hash mapping position to method override data
|
|
242
|
+
# @return [Object]
|
|
243
|
+
def process_dedup_group(pos, items, result, method_overrides_by_pos)
|
|
244
|
+
plugin_items = items.select { |pair| pair.first == :plugin }
|
|
245
|
+
return result.concat(items) if plugin_items.empty?
|
|
246
|
+
|
|
247
|
+
method_items = items.select { |pair| pair.first == :method }
|
|
248
|
+
override_items = find_override_items(plugin_items)
|
|
249
|
+
if override_items.any? && method_items.any?
|
|
250
|
+
handle_override_case(result, items, override_items, method_overrides_by_pos, pos)
|
|
251
|
+
else
|
|
252
|
+
result.concat(deduplicate_items(items, plugin_items, pos, method_items))
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Group insertions by their source position.
|
|
257
|
+
#
|
|
258
|
+
# @private
|
|
259
|
+
# @param [Array<Array(Symbol,Object)>] insertions
|
|
260
|
+
# @return [Hash{Integer => Array<Array(Symbol,Object)>}]
|
|
261
|
+
def group_by_position(insertions)
|
|
262
|
+
groups = {} #: Hash[Integer, untyped]
|
|
263
|
+
insertions.each do |kind, ins|
|
|
170
264
|
pos = plugin_insertion_pos(kind, ins)
|
|
171
|
-
(groups[pos] ||= []) <<
|
|
265
|
+
(groups[pos] ||= []) << [kind, ins]
|
|
266
|
+
end
|
|
267
|
+
groups
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Find plugin items that have a method_override hash.
|
|
271
|
+
#
|
|
272
|
+
# @private
|
|
273
|
+
# @param [Array<Array(Symbol,Object)>] plugin_items
|
|
274
|
+
# @return [Array<Array(Symbol,Object)>]
|
|
275
|
+
def find_override_items(plugin_items)
|
|
276
|
+
plugin_items.select do |_k, ins|
|
|
277
|
+
ins.is_a?(Hash) && ins[:method_override].is_a?(Hash)
|
|
172
278
|
end
|
|
279
|
+
end
|
|
173
280
|
|
|
174
|
-
|
|
281
|
+
# Handle a method_override case: record the winning override and remove override items.
|
|
282
|
+
#
|
|
283
|
+
# @private
|
|
284
|
+
# @param [Array<Array(Symbol,Object)>] result
|
|
285
|
+
# @param [Array<Array(Symbol,Object)>] items
|
|
286
|
+
# @param [Array<Array(Symbol,Object)>] override_items
|
|
287
|
+
# @param [Hash, nil] method_overrides_by_pos
|
|
288
|
+
# @param [Integer] pos
|
|
289
|
+
# @return [Object]
|
|
290
|
+
def handle_override_case(result, items, override_items, method_overrides_by_pos, pos)
|
|
291
|
+
if method_overrides_by_pos
|
|
292
|
+
winning_ins = pick_highest_priority_override_insertion(override_items, pos: pos)
|
|
293
|
+
method_overrides_by_pos[pos] = winning_ins[:method_override] if winning_ins
|
|
294
|
+
end
|
|
175
295
|
|
|
176
|
-
|
|
177
|
-
|
|
296
|
+
items = items.reject { |k, ins| k == :plugin && ins.is_a?(Hash) && ins.key?(:method_override) }
|
|
297
|
+
result.concat(items)
|
|
298
|
+
end
|
|
178
299
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
300
|
+
# Handle items where no method_override applies (plugin-doc case or fallback).
|
|
301
|
+
#
|
|
302
|
+
# @private
|
|
303
|
+
# @param [Array<Array(Symbol,Object)>] items
|
|
304
|
+
# @param [Array<Array(Symbol,Object)>] plugin_items
|
|
305
|
+
# @param [Integer] pos
|
|
306
|
+
# @param [Object] _method_items method insertion pairs at this position (unused)
|
|
307
|
+
# @return [Array<Array(Symbol,Object)>]
|
|
308
|
+
def deduplicate_items(items, plugin_items, pos, _method_items)
|
|
309
|
+
plugin_doc_items = plugin_items.select { |pair| plugin_doc_item?(pair) }
|
|
184
310
|
|
|
185
|
-
|
|
186
|
-
items
|
|
311
|
+
if plugin_doc_items.any?
|
|
312
|
+
deduplicate_plugin_doc_case(items, plugin_doc_items, pos)
|
|
313
|
+
else
|
|
314
|
+
items.reject { |pair| override_or_plugin_method?(pair) }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
187
317
|
|
|
188
|
-
|
|
189
|
-
|
|
318
|
+
# Predicate: insertion pair has a doc key with non-empty content.
|
|
319
|
+
# @private
|
|
320
|
+
# @param [Object] pair the [kind, insertion] tuple to inspect
|
|
321
|
+
# @return [Object]
|
|
322
|
+
def plugin_doc_item?(pair)
|
|
323
|
+
_k, ins = pair
|
|
324
|
+
ins.is_a?(Hash) && ins[:doc] && !ins[:doc].empty?
|
|
325
|
+
end
|
|
190
326
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
327
|
+
# Deduplicate plugin doc items, keeping only highest-priority entries.
|
|
328
|
+
#
|
|
329
|
+
# @private
|
|
330
|
+
# @param [Array<Array(Symbol,Object)>] items
|
|
331
|
+
# @param [Array<Array(Symbol,Object)>] plugin_doc_items
|
|
332
|
+
# @param [Integer] pos
|
|
333
|
+
# @return [Array<Array(Symbol,Object)>]
|
|
334
|
+
def deduplicate_plugin_doc_case(items, plugin_doc_items, pos)
|
|
335
|
+
items = items.reject { |k, _| k == :method }
|
|
336
|
+
items = items.reject { |pair| override_or_plugin_method?(pair) }
|
|
194
337
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if kept_plugin_labels.size > 1
|
|
204
|
-
line = plugin_insertion_line(plugin_items.first[1])
|
|
205
|
-
loc = +"pos=#{pos}"
|
|
206
|
-
loc << " line=#{line}" if line
|
|
207
|
-
|
|
208
|
-
warn "Docscribe: CollectorPlugin conflict at #{loc} (priority=#{max_prio}): " \
|
|
209
|
-
"#{kept_plugin_labels.join(', ')} — keeping all. Set explicit priority to resolve."
|
|
210
|
-
end
|
|
211
|
-
end
|
|
338
|
+
max_prio = max_plugin_priority(plugin_doc_items)
|
|
339
|
+
dropped = filter_lower_priority_plugins(items, max_prio)
|
|
340
|
+
items = items.reject { |k, ins| dropped.include?([k, ins]) }
|
|
341
|
+
|
|
342
|
+
warn_plugin_conflict!(dropped, plugin_doc_items, max_prio, pos) if Docscribe::Plugin.debug? && dropped.any?
|
|
343
|
+
|
|
344
|
+
items
|
|
345
|
+
end
|
|
212
346
|
|
|
213
|
-
|
|
347
|
+
# Predicate: insertion pair is a plugin with method_override.
|
|
348
|
+
# @private
|
|
349
|
+
# @param [Object] pair the [kind, insertion] tuple to inspect
|
|
350
|
+
# @return [Object]
|
|
351
|
+
def override_or_plugin_method?(pair)
|
|
352
|
+
k, ins = pair
|
|
353
|
+
k == :plugin && ins.is_a?(Hash) && ins.key?(:method_override)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Find the maximum priority among plugin doc items.
|
|
357
|
+
#
|
|
358
|
+
# @private
|
|
359
|
+
# @param [Array<Array(Symbol,Object)>] plugin_items
|
|
360
|
+
# @return [Integer]
|
|
361
|
+
def max_plugin_priority(plugin_items)
|
|
362
|
+
plugin_items.map { |_k, ins| plugin_insertion_priority(ins) }.max || 0
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Filter plugin items that fall below the given priority threshold.
|
|
366
|
+
#
|
|
367
|
+
# @private
|
|
368
|
+
# @param [Array<Array(Symbol,Object)>] items
|
|
369
|
+
# @param [Integer] threshold
|
|
370
|
+
# @return [Array<Array(Symbol,Object)>]
|
|
371
|
+
def filter_lower_priority_plugins(items, threshold)
|
|
372
|
+
items.select do |k, ins|
|
|
373
|
+
k == :plugin && ins.is_a?(Hash) && ins[:doc] && plugin_insertion_priority(ins) < threshold
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Warn about conflicting collector plugins at a given position.
|
|
378
|
+
#
|
|
379
|
+
# @private
|
|
380
|
+
# @param [Array<Array(Symbol,Object)>] dropped
|
|
381
|
+
# @param [Array<Array(Symbol,Object)>] plugin_items
|
|
382
|
+
# @param [Integer] max_prio
|
|
383
|
+
# @param [Integer] pos
|
|
384
|
+
# @return [Object]
|
|
385
|
+
def warn_plugin_conflict!(dropped, plugin_items, max_prio, pos)
|
|
386
|
+
kept_labels = plugin_items.map { |_k, ins| plugin_insertion_label(ins) }.uniq
|
|
387
|
+
dropped_labels = dropped.map { |_k, ins| plugin_insertion_label(ins) }.uniq
|
|
388
|
+
loc = conflict_location_str(pos, plugin_items)
|
|
389
|
+
warn "Docscribe: CollectorPlugin conflict at #{loc} — " \
|
|
390
|
+
"#{dropped_labels.join(', ')} (pri=#{dropped.map { |_k, ins| plugin_insertion_priority(ins) }.max}) " \
|
|
391
|
+
"dropped in favor of #{kept_labels.join(', ')} (pri=#{max_prio}). " \
|
|
392
|
+
'Set explicit priority or adjust anchor_node to avoid collision.'
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Build a human-readable location string for a conflict warning.
|
|
396
|
+
# @private
|
|
397
|
+
# @param [Object] pos the source position of the conflict
|
|
398
|
+
# @param [Object] plugin_items the plugin insertion pairs involved in the conflict
|
|
399
|
+
# @return [Object]
|
|
400
|
+
def conflict_location_str(pos, plugin_items)
|
|
401
|
+
line = plugin_insertion_line(plugin_items.first[1])
|
|
402
|
+
loc = +"pos=#{pos}"
|
|
403
|
+
loc << " line=#{line}" if line
|
|
404
|
+
loc
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# @private
|
|
408
|
+
# @param override_items [Array<Array(Symbol, Hash)>] list of [:plugin, insertion_hash]
|
|
409
|
+
# that include :method_override
|
|
410
|
+
# @param pos [Integer] begin_pos (used only for debug output)
|
|
411
|
+
# @return [Hash, nil] winning insertion hash (the one whose override will be applied)
|
|
412
|
+
def pick_highest_priority_override_insertion(override_items, pos:)
|
|
413
|
+
return nil if override_items.empty?
|
|
414
|
+
|
|
415
|
+
max_prio = max_plugin_priority_for(override_items)
|
|
416
|
+
winners = override_items.select { |_k, ins| plugin_insertion_priority(ins) == max_prio }
|
|
417
|
+
winners_sorted = sort_winners_by_order(winners)
|
|
418
|
+
|
|
419
|
+
warn_override_conflict!(winners_sorted, max_prio, pos)
|
|
420
|
+
|
|
421
|
+
winners_sorted.first[1]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Compute max priority among override items.
|
|
425
|
+
# @private
|
|
426
|
+
# @param [Object] override_items plugin insertion pairs containing :method_override
|
|
427
|
+
# @return [Object]
|
|
428
|
+
def max_plugin_priority_for(override_items)
|
|
429
|
+
override_items.map { |_k, ins| plugin_insertion_priority(ins) }.max || 0
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Sort override winners deterministically by plugin order.
|
|
433
|
+
# @private
|
|
434
|
+
# @param [Object] winners override insertion pairs tied at the highest priority
|
|
435
|
+
# @return [Object]
|
|
436
|
+
def sort_winners_by_order(winners)
|
|
437
|
+
winners.sort_by do |_k, ins|
|
|
438
|
+
order = ins.is_a?(Hash) ? ins[:__docscribe_plugin_order] : nil
|
|
439
|
+
order || 0
|
|
214
440
|
end
|
|
441
|
+
end
|
|
215
442
|
|
|
216
|
-
|
|
443
|
+
# Warn about override conflicts in debug mode.
|
|
444
|
+
# @private
|
|
445
|
+
# @param [Object] winners_sorted sorted override winners at max priority
|
|
446
|
+
# @param [Object] max_prio the maximum priority value
|
|
447
|
+
# @param [Object] pos the source position of the conflict
|
|
448
|
+
# @return [Object]
|
|
449
|
+
def warn_override_conflict!(winners_sorted, max_prio, pos)
|
|
450
|
+
return unless Docscribe::Plugin.debug?
|
|
451
|
+
|
|
452
|
+
labels = winners_sorted.map { |_k, ins| plugin_insertion_label(ins) }.uniq
|
|
453
|
+
return unless labels.size > 1
|
|
454
|
+
|
|
455
|
+
line = plugin_insertion_line(winners_sorted.first[1])
|
|
456
|
+
loc = +"pos=#{pos}"
|
|
457
|
+
loc << " line=#{line}" if line
|
|
458
|
+
warn "Docscribe: method_override conflict at #{loc} (priority=#{max_prio}): " \
|
|
459
|
+
"#{labels.join(', ')} — using first by registration order."
|
|
217
460
|
end
|
|
218
461
|
|
|
219
462
|
# @private
|
|
@@ -248,7 +491,9 @@ module Docscribe
|
|
|
248
491
|
def plugin_insertion_line(insertion)
|
|
249
492
|
return nil unless insertion.is_a?(Hash)
|
|
250
493
|
|
|
251
|
-
insertion[:anchor_node]
|
|
494
|
+
anchor_node = insertion[:anchor_node]
|
|
495
|
+
expression = anchor_node&.loc&.expression
|
|
496
|
+
expression&.line
|
|
252
497
|
rescue StandardError
|
|
253
498
|
nil
|
|
254
499
|
end
|
|
@@ -279,24 +524,32 @@ module Docscribe
|
|
|
279
524
|
# @param [Parser::Source::Buffer] buffer
|
|
280
525
|
# @param [Hash] insertion { anchor_node:, doc: }
|
|
281
526
|
# @param [Symbol] strategy
|
|
527
|
+
# @param [Docscribe::Config] config
|
|
282
528
|
# @return [void]
|
|
283
|
-
def apply_plugin_insertion!(rewriter:, buffer:, insertion:, strategy:)
|
|
284
|
-
anchor_node = insertion
|
|
285
|
-
doc = insertion[:doc]
|
|
529
|
+
def apply_plugin_insertion!(rewriter:, buffer:, insertion:, strategy:, config:)
|
|
530
|
+
anchor_node, doc = insertion.values_at(:anchor_node, :doc)
|
|
286
531
|
return unless anchor_node && doc && !doc.empty?
|
|
287
532
|
|
|
288
533
|
indent = SourceHelpers.line_indent(anchor_node)
|
|
289
|
-
doc
|
|
534
|
+
doc = normalize_plugin_doc(doc, indent, config: config, anchor_node: anchor_node)
|
|
290
535
|
bol_range = SourceHelpers.line_start_range(buffer, anchor_node)
|
|
536
|
+
insert_plugin_doc(rewriter, buffer, bol_range, doc, strategy)
|
|
537
|
+
end
|
|
291
538
|
|
|
539
|
+
# Insert plugin doc according to strategy, handling comment removal.
|
|
540
|
+
# @private
|
|
541
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
542
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
543
|
+
# @param [Object] bol_range the beginning-of-line range for the anchor node
|
|
544
|
+
# @param [Object] doc the normalized documentation string to insert
|
|
545
|
+
# @param [Object] strategy :safe or :aggressive rewrite mode
|
|
546
|
+
# @return [Object]
|
|
547
|
+
def insert_plugin_doc(rewriter, buffer, bol_range, doc, strategy)
|
|
292
548
|
case strategy
|
|
293
549
|
when :aggressive
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
rewriter.remove(range)
|
|
297
|
-
end
|
|
550
|
+
range = any_comment_block_removal_range(buffer, bol_range.begin_pos)
|
|
551
|
+
rewriter.remove(range) if range
|
|
298
552
|
rewriter.insert_before(bol_range, doc)
|
|
299
|
-
|
|
300
553
|
when :safe
|
|
301
554
|
return if SourceHelpers.already_has_doc_immediately_above?(buffer, bol_range.begin_pos)
|
|
302
555
|
|
|
@@ -317,31 +570,122 @@ module Docscribe
|
|
|
317
570
|
def any_comment_block_removal_range(buffer, bol_pos)
|
|
318
571
|
src = buffer.source
|
|
319
572
|
lines = src.lines
|
|
320
|
-
|
|
321
|
-
|
|
573
|
+
i = nearest_comment_line_index(src, lines, bol_pos)
|
|
574
|
+
return nil unless i
|
|
322
575
|
|
|
323
|
-
|
|
324
|
-
|
|
576
|
+
start_idx = comment_block_start_index(lines, i)
|
|
577
|
+
|
|
578
|
+
removable_start_idx = skip_preserved_lines(lines, start_idx, i)
|
|
579
|
+
return nil if removable_start_idx > i
|
|
580
|
+
|
|
581
|
+
start_pos = removable_start_idx.positive? ? (lines[0...removable_start_idx] || []).join.length : 0
|
|
582
|
+
Parser::Source::Range.new(buffer, start_pos, bol_pos)
|
|
583
|
+
end
|
|
325
584
|
|
|
326
|
-
|
|
585
|
+
# Find the nearest comment line index above a position.
|
|
586
|
+
# @private
|
|
587
|
+
# @param [Object] src the full source string of the buffer
|
|
588
|
+
# @param [Object] lines array of source code lines
|
|
589
|
+
# @param [Object] bol_pos character position of the beginning of the anchor line
|
|
590
|
+
# @return [Object]
|
|
591
|
+
def nearest_comment_line_index(src, lines, bol_pos)
|
|
592
|
+
def_line_idx = (src[0...bol_pos] || '').count("\n")
|
|
593
|
+
i = def_line_idx - 1
|
|
594
|
+
i -= 1 while i >= 0 && lines[i].strip.empty?
|
|
327
595
|
return nil unless i >= 0 && lines[i] =~ /^\s*#/
|
|
328
596
|
|
|
329
|
-
|
|
330
|
-
|
|
597
|
+
i
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Walk upward through contiguous comment block to find the start.
|
|
601
|
+
# @private
|
|
602
|
+
# @param [Object] lines array of source code lines
|
|
603
|
+
# @param [Object] i line index of the bottommost comment in the contiguous block
|
|
604
|
+
# @param [Object] def_line_idx the index in lines of the method definition (anchor) line
|
|
605
|
+
# @return [Object]
|
|
606
|
+
def comment_block_start_index(lines, def_line_idx)
|
|
607
|
+
start_idx = def_line_idx
|
|
331
608
|
start_idx -= 1 while start_idx >= 0 && lines[start_idx] =~ /^\s*#/
|
|
332
|
-
start_idx
|
|
609
|
+
start_idx + 1
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Skip preserved directive-style lines at the top of the comment block.
|
|
613
|
+
# @private
|
|
614
|
+
# @param [Object] lines array of source code lines
|
|
615
|
+
# @param [Object] start_idx index of the first line of the comment block
|
|
616
|
+
# @param [Object] i current line index while walking upward through the block
|
|
617
|
+
# @param [Object] def_line_idx the index in lines of the method definition (anchor) line
|
|
618
|
+
# @return [Object]
|
|
619
|
+
def skip_preserved_lines(lines, start_idx, def_line_idx)
|
|
620
|
+
idx = start_idx
|
|
621
|
+
idx += 1 while idx <= def_line_idx && SourceHelpers.preserved_comment_line?(lines[idx])
|
|
622
|
+
idx
|
|
623
|
+
end
|
|
333
624
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
625
|
+
# Normalize a CollectorPlugin-provided doc string before insertion.
|
|
626
|
+
#
|
|
627
|
+
# Responsibilities:
|
|
628
|
+
# - apply indentation based on the anchor node
|
|
629
|
+
# - trim trailing whitespace-only lines
|
|
630
|
+
# - (optionally) prepend the configured default message for `def/defs` anchors
|
|
631
|
+
# when the plugin output contains only tags (no prose)
|
|
632
|
+
#
|
|
633
|
+
# @private
|
|
634
|
+
# @param doc [String] Raw doc string returned by a CollectorPlugin insertion (`:doc`)
|
|
635
|
+
# @param indent [String] Indentation to apply to every doc line
|
|
636
|
+
# @param config [Docscribe::Config] Effective Docscribe config for this run
|
|
637
|
+
# @param anchor_node [Parser::AST::Node, nil] AST node used as insertion anchor
|
|
638
|
+
# @return [String] Normalized doc string ready to be inserted
|
|
639
|
+
def normalize_plugin_doc(doc, indent, config:, anchor_node:)
|
|
640
|
+
doc = normalize_plugin_doc_indent(doc, indent)
|
|
641
|
+
doc = trim_trailing_blank_lines(doc)
|
|
642
|
+
|
|
643
|
+
if anchor_node && %i[def defs].include?(anchor_node.type) && config.include_default_message?
|
|
644
|
+
doc = prepend_default_message_if_no_prose(doc, anchor_node, indent, config)
|
|
339
645
|
end
|
|
340
646
|
|
|
341
|
-
|
|
647
|
+
doc
|
|
648
|
+
end
|
|
342
649
|
|
|
343
|
-
|
|
344
|
-
|
|
650
|
+
# Trim trailing blank lines from a doc string.
|
|
651
|
+
# @private
|
|
652
|
+
# @param [Object] doc the documentation string to trim
|
|
653
|
+
# @return [Object]
|
|
654
|
+
def trim_trailing_blank_lines(doc)
|
|
655
|
+
lines = doc.lines
|
|
656
|
+
lines.pop while lines.any? && lines.last.strip.empty?
|
|
657
|
+
result = lines.join
|
|
658
|
+
result.end_with?("\n") ? result : "#{result}\n"
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Prepend default message if the doc block has only tags.
|
|
662
|
+
# @private
|
|
663
|
+
# @param [Object] doc the plugin-generated documentation string
|
|
664
|
+
# @param [Object] anchor_node the AST node used as the insertion anchor
|
|
665
|
+
# @param [Object] indent whitespace indentation prefix derived from the anchor node
|
|
666
|
+
# @param [Object] config the active Docscribe::Config
|
|
667
|
+
# @return [Object]
|
|
668
|
+
def prepend_default_message_if_no_prose(doc, anchor_node, indent, config)
|
|
669
|
+
return doc if doc_has_prose?(doc)
|
|
670
|
+
|
|
671
|
+
scope = anchor_node.type == :defs ? :class : :instance
|
|
672
|
+
msg = config.default_message(scope, :public)
|
|
673
|
+
"#{indent}# #{msg}\n#{indent}#\n" + doc
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Check if a doc block has any prose content.
|
|
677
|
+
# @private
|
|
678
|
+
# @param [Object] doc the documentation string to inspect
|
|
679
|
+
# @return [Boolean]
|
|
680
|
+
def doc_has_prose?(doc)
|
|
681
|
+
doc.lines.any? do |l|
|
|
682
|
+
s = l.strip
|
|
683
|
+
next false if s.empty? || s == '#'
|
|
684
|
+
next false if s.start_with?('# @')
|
|
685
|
+
next false if s.start_with?('# +')
|
|
686
|
+
|
|
687
|
+
true
|
|
688
|
+
end
|
|
345
689
|
end
|
|
346
690
|
|
|
347
691
|
# Normalize indentation of a plugin-generated doc block.
|
|
@@ -405,166 +749,299 @@ module Docscribe
|
|
|
405
749
|
# - insert a fresh regenerated block
|
|
406
750
|
#
|
|
407
751
|
# @private
|
|
408
|
-
# @param [
|
|
409
|
-
# @param [Parser::Source::Buffer] buffer
|
|
410
|
-
# @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
|
|
411
|
-
# @param [Docscribe::Config] config
|
|
412
|
-
# @param [Object, nil] signature_provider
|
|
413
|
-
# @param [Symbol] strategy
|
|
414
|
-
# @param [Array<Hash>] changes structured change records
|
|
415
|
-
# @param [String] file
|
|
416
|
-
# @param [Object] core_rbs_provider Param documentation.
|
|
752
|
+
# @param [Hash] options kwargs with insertion, config, rewriter, buffer, strategy, changes, file, doc params
|
|
417
753
|
# @return [void]
|
|
418
|
-
def apply_method_insertion!(
|
|
419
|
-
|
|
754
|
+
def apply_method_insertion!(**options)
|
|
755
|
+
insertion = options[:insertion]
|
|
756
|
+
config = options[:config]
|
|
757
|
+
return unless method_insertion_allowed?(insertion, config)
|
|
758
|
+
|
|
759
|
+
anchor_bol_range, = method_bol_ranges(options[:buffer], insertion)
|
|
760
|
+
params = build_method_insertion_params(insertion, config, options[:signature_provider],
|
|
761
|
+
options[:core_rbs_provider], options[:method_override])
|
|
762
|
+
doc = build_method_doc(insertion, **params)
|
|
763
|
+
dispatch_method_insertion_by_strategy!(anchor_bol_range, options, params, doc)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Dispatch method insertion to the aggressive or safe handler.
|
|
767
|
+
# @private
|
|
768
|
+
# @param [Object] anchor_bol_range the beginning-of-line range for the anchor node
|
|
769
|
+
# @param [Object] options the full keyword options hash passed to apply_method_insertion!
|
|
770
|
+
# @param [Object] params precomputed insertion parameters (types, overrides, config)
|
|
771
|
+
# @param [Object] doc the generated documentation block string
|
|
772
|
+
# @return [Object]
|
|
773
|
+
def dispatch_method_insertion_by_strategy!(anchor_bol_range, options, params, doc)
|
|
774
|
+
base = { anchor_bol_range: anchor_bol_range, insertion: options[:insertion],
|
|
775
|
+
rewriter: options[:rewriter], buffer: options[:buffer],
|
|
776
|
+
changes: options[:changes], file: options[:file] }
|
|
777
|
+
case options[:strategy]
|
|
778
|
+
when :aggressive then apply_method_insertion_aggressive!(**base, doc: doc)
|
|
779
|
+
when :safe then apply_method_insertion_safe!(**base, strategy: options[:strategy], **params)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Validate and prepare for method insertion.
|
|
784
|
+
#
|
|
785
|
+
# @private
|
|
786
|
+
# @param [Collector::Insertion] insertion
|
|
787
|
+
# @param [Docscribe::Config] config
|
|
788
|
+
# @return [Boolean] true if insertion should proceed
|
|
789
|
+
def method_insertion_allowed?(insertion, config)
|
|
420
790
|
name = SourceHelpers.node_name(insertion.node)
|
|
791
|
+
config.process_method?(container: insertion.container, scope: insertion.scope,
|
|
792
|
+
visibility: insertion.visibility || :public, name: name)
|
|
793
|
+
end
|
|
421
794
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
795
|
+
# Build all parameters needed for method insertion.
|
|
796
|
+
#
|
|
797
|
+
# @private
|
|
798
|
+
# @param [Collector::Insertion] insertion
|
|
799
|
+
# @param [Docscribe::Config] config
|
|
800
|
+
# @param [Object, nil] signature_provider
|
|
801
|
+
# @param [Object, nil] core_rbs_provider
|
|
802
|
+
# @param [Hash, nil] method_override
|
|
803
|
+
# @return [Hash]
|
|
804
|
+
def build_method_insertion_params(insertion, config, signature_provider, core_rbs_provider, method_override)
|
|
805
|
+
override = extract_method_override!(method_override)
|
|
806
|
+
effective = build_effective_params(insertion, config: config, signature_provider: signature_provider,
|
|
807
|
+
core_rbs_provider: core_rbs_provider, override: override)
|
|
808
|
+
{ **effective, config: config, signature_provider: signature_provider,
|
|
809
|
+
core_rbs_provider: core_rbs_provider }
|
|
810
|
+
end
|
|
428
811
|
|
|
429
|
-
|
|
812
|
+
# Build effective parameters merging external signatures and overrides.
|
|
813
|
+
#
|
|
814
|
+
# @private
|
|
815
|
+
# @param [Collector::Insertion] insertion
|
|
816
|
+
# @param [Hash] options keyword options
|
|
817
|
+
# @return [Hash{Symbol => Object}]
|
|
818
|
+
def build_effective_params(insertion, **options)
|
|
819
|
+
external_sig = resolve_external_signature(insertion, options[:signature_provider])
|
|
820
|
+
param_types = resolve_param_types(insertion, external_sig, options[:config])
|
|
821
|
+
override = options[:override]
|
|
822
|
+
|
|
823
|
+
param_types = (param_types || {}).merge(override[:param_types]) if override[:param_types]&.any?
|
|
430
824
|
|
|
431
|
-
|
|
432
|
-
|
|
825
|
+
{ param_types: param_types, return_type_override: override[:return_type], override_tags: override[:tags] }
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Resolve external signature for an insertion.
|
|
829
|
+
# @private
|
|
830
|
+
# @param [Object] insertion the collected method insertion
|
|
831
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
832
|
+
# @return [Object]
|
|
833
|
+
def resolve_external_signature(insertion, signature_provider)
|
|
834
|
+
signature_provider&.signature_for(
|
|
433
835
|
container: insertion.container,
|
|
434
836
|
scope: insertion.scope,
|
|
435
837
|
name: SourceHelpers.node_name(insertion.node)
|
|
436
838
|
)
|
|
839
|
+
end
|
|
437
840
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
841
|
+
# Resolve param types from signature or fall back to node parsing.
|
|
842
|
+
# @private
|
|
843
|
+
# @param [Object] insertion the collected method insertion
|
|
844
|
+
# @param [Object] external_sig the resolved signature from the signature provider
|
|
845
|
+
# @param [Object] config the active Docscribe::Config
|
|
846
|
+
# @return [Object]
|
|
847
|
+
def resolve_param_types(insertion, external_sig, config)
|
|
848
|
+
external_sig&.param_types || DocBuilder.build_param_types_from_node(
|
|
849
|
+
insertion.node, external_sig: external_sig, config: config
|
|
850
|
+
)
|
|
851
|
+
end
|
|
443
852
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
853
|
+
# Apply method insertion in aggressive strategy mode.
|
|
854
|
+
#
|
|
855
|
+
# @private
|
|
856
|
+
# @param [Hash] options keyword options
|
|
857
|
+
# @return [void]
|
|
858
|
+
def apply_method_insertion_aggressive!(**options)
|
|
859
|
+
rewriter = options[:rewriter]
|
|
860
|
+
buffer = options[:buffer]
|
|
861
|
+
insertion = options[:insertion]
|
|
862
|
+
doc = options[:doc]
|
|
863
|
+
|
|
864
|
+
remove_method_comment_block(rewriter, buffer, insertion)
|
|
865
|
+
return if doc.nil? || doc.empty?
|
|
866
|
+
|
|
867
|
+
rewriter.insert_before(options[:anchor_bol_range], doc)
|
|
868
|
+
add_change(changes: options[:changes], type: :insert_full_doc_block,
|
|
869
|
+
insertion: insertion, file: options[:file], message: 'missing docs')
|
|
870
|
+
end
|
|
449
871
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
872
|
+
# Remove the comment block above a method when present.
|
|
873
|
+
# @private
|
|
874
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
875
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
876
|
+
# @param [Object] insertion the collected method insertion
|
|
877
|
+
# @return [Object]
|
|
878
|
+
def remove_method_comment_block(rewriter, buffer, insertion)
|
|
879
|
+
range = method_comment_block_removal_range(buffer, insertion)
|
|
880
|
+
rewriter.remove(range) if range
|
|
881
|
+
end
|
|
453
882
|
|
|
454
|
-
|
|
883
|
+
# Apply method insertion in safe strategy mode.
|
|
884
|
+
#
|
|
885
|
+
# @private
|
|
886
|
+
# @param [Hash] options keyword options
|
|
887
|
+
# @return [void]
|
|
888
|
+
def apply_method_insertion_safe!(**options)
|
|
889
|
+
info = method_doc_comment_info(options[:buffer], options[:insertion])
|
|
455
890
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
891
|
+
if info
|
|
892
|
+
apply_method_insertion_safe_with_info!(**options, info: info)
|
|
893
|
+
else
|
|
894
|
+
apply_method_insertion_safe_without_info!(**options)
|
|
895
|
+
end
|
|
896
|
+
end
|
|
463
897
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
sorted_existing_doc_lines = Docscribe::InlineRewriter::DocBlock.merge(
|
|
482
|
-
info[:doc_lines],
|
|
483
|
-
missing_lines: [],
|
|
484
|
-
sort_tags: config.sort_tags?,
|
|
485
|
-
tag_order: config.tag_order
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
merged_doc_lines = Docscribe::InlineRewriter::DocBlock.merge(
|
|
489
|
-
info[:doc_lines],
|
|
490
|
-
missing_lines: missing_lines,
|
|
491
|
-
sort_tags: config.sort_tags?,
|
|
492
|
-
tag_order: config.tag_order
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
existing_order_changed = sorted_existing_doc_lines != info[:doc_lines]
|
|
496
|
-
new_block = (info[:preserved_lines] + merged_doc_lines).join
|
|
497
|
-
old_block = info[:lines].join
|
|
498
|
-
|
|
499
|
-
if new_block != old_block
|
|
500
|
-
range = Parser::Source::Range.new(buffer, info[:start_pos], info[:end_pos])
|
|
501
|
-
rewriter.replace(range, new_block)
|
|
502
|
-
|
|
503
|
-
if existing_order_changed
|
|
504
|
-
add_change(
|
|
505
|
-
changes,
|
|
506
|
-
type: :unsorted_tags,
|
|
507
|
-
insertion: insertion,
|
|
508
|
-
file: file,
|
|
509
|
-
message: 'unsorted tags'
|
|
510
|
-
)
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
type_mismatch_reasons = reason_specs.select { |r| %i[updated_param updated_return].include?(r[:type]) }
|
|
515
|
-
|
|
516
|
-
if new_block != old_block || type_mismatch_reasons.any?
|
|
517
|
-
reason_specs.each do |reason|
|
|
518
|
-
add_change(
|
|
519
|
-
changes,
|
|
520
|
-
type: reason[:type],
|
|
521
|
-
insertion: insertion,
|
|
522
|
-
file: file,
|
|
523
|
-
message: reason[:message],
|
|
524
|
-
extra: reason[:extra] || {}
|
|
525
|
-
)
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
return
|
|
530
|
-
end
|
|
898
|
+
# Apply method insertion in safe mode when existing doc info is present.
|
|
899
|
+
#
|
|
900
|
+
# @private
|
|
901
|
+
# @param [Hash] options keyword options
|
|
902
|
+
# @return [void]
|
|
903
|
+
def apply_method_insertion_safe_with_info!(**options)
|
|
904
|
+
i = options[:info]
|
|
905
|
+
dp = filter_doc_params(options)
|
|
906
|
+
mr = build_missing_method_merge_result(options[:insertion], existing_lines: i[:doc_lines],
|
|
907
|
+
strategy: options[:strategy], **dp)
|
|
908
|
+
changed, n, ob = compute_doc_replacement(i, mr[:lines], strategy: options[:strategy], **dp)
|
|
909
|
+
commit_safe_doc_outcome(options[:rewriter], options[:buffer], i, n,
|
|
910
|
+
old_block: ob, merge_result: mr,
|
|
911
|
+
existing_order_changed: changed,
|
|
912
|
+
insertion: options[:insertion], changes: options[:changes],
|
|
913
|
+
file: options[:file])
|
|
914
|
+
end
|
|
531
915
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
916
|
+
# Commit doc replacement and log changes for safe mode.
|
|
917
|
+
# @private
|
|
918
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
919
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
920
|
+
# @param [Object] info hash containing existing doc comment block data
|
|
921
|
+
# @param [Object] new_block the newly constructed replacement doc block string
|
|
922
|
+
# @param [Hash] rest additional kwargs (old_block, merge_result, existing_order_changed, insertion, changes, file)
|
|
923
|
+
# @return [Object]
|
|
924
|
+
def commit_safe_doc_outcome(rewriter, buffer, info, new_block, **rest)
|
|
925
|
+
handle_doc_replacement(rewriter, buffer, info, new_block,
|
|
926
|
+
insertion: rest[:insertion], changes: rest[:changes],
|
|
927
|
+
file: rest[:file],
|
|
928
|
+
existing_order_changed: rest[:existing_order_changed])
|
|
929
|
+
log_method_doc_changes!(insertion: rest[:insertion], merge_result: rest[:merge_result],
|
|
930
|
+
new_block: new_block, old_block: rest[:old_block],
|
|
931
|
+
changes: rest[:changes], file: rest[:file])
|
|
932
|
+
end
|
|
536
933
|
|
|
537
|
-
|
|
934
|
+
# Filter doc params from options hash.
|
|
935
|
+
# @private
|
|
936
|
+
# @param [Object] options the full options hash to filter
|
|
937
|
+
# @return [Object]
|
|
938
|
+
def filter_doc_params(options)
|
|
939
|
+
options.reject { |k, _| %i[rewriter buffer insertion anchor_bol_range info changes file strategy].include?(k) }
|
|
940
|
+
end
|
|
538
941
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
942
|
+
# Handle replacement when doc block content changed.
|
|
943
|
+
# @private
|
|
944
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
945
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
946
|
+
# @param [Object] info hash containing existing doc comment block data (start_pos, end_pos, lines)
|
|
947
|
+
# @param [Object] new_block the newly constructed replacement doc block string
|
|
948
|
+
# @param [Object] existing_order_changed boolean indicating tag order was modified by sorting
|
|
949
|
+
# @param [Object] insertion the collected method insertion
|
|
950
|
+
# @param [Object] changes array of structured change records for reporting
|
|
951
|
+
# @param [Object] file the source file path string
|
|
952
|
+
# @param [Hash] log_opts additional keyword arguments for logging and recording changes
|
|
953
|
+
# @return [Object]
|
|
954
|
+
def handle_doc_replacement(rewriter, buffer, info, new_block, **log_opts)
|
|
955
|
+
range = Parser::Source::Range.new(buffer, info[:start_pos], info[:end_pos])
|
|
956
|
+
rewriter.replace(range, new_block)
|
|
957
|
+
|
|
958
|
+
return unless log_opts[:existing_order_changed]
|
|
959
|
+
|
|
960
|
+
add_change(changes: log_opts[:changes], type: :unsorted_tags,
|
|
961
|
+
insertion: log_opts[:insertion], file: log_opts[:file],
|
|
962
|
+
message: 'unsorted tags')
|
|
547
963
|
end
|
|
548
964
|
|
|
549
|
-
#
|
|
965
|
+
# Compute merged doc lines and determine if replacement is needed.
|
|
966
|
+
#
|
|
967
|
+
# @private
|
|
968
|
+
# @param [Hash] info existing doc info
|
|
969
|
+
# @param [Array<String>] missing_lines
|
|
970
|
+
# @param [Hash] options keyword options
|
|
971
|
+
# @return [Array] [existing_order_changed, new_block, old_block]
|
|
972
|
+
def compute_doc_replacement(info, missing_lines, **options)
|
|
973
|
+
dc = options[:config]
|
|
974
|
+
sorted = Docscribe::InlineRewriter::DocBlock.merge(
|
|
975
|
+
info[:doc_lines], missing_lines: [], sort_tags: dc.sort_tags?, tag_order: dc.tag_order
|
|
976
|
+
)
|
|
977
|
+
merged = Docscribe::InlineRewriter::DocBlock.merge(
|
|
978
|
+
info[:doc_lines], missing_lines: missing_lines, sort_tags: dc.sort_tags?, tag_order: dc.tag_order
|
|
979
|
+
)
|
|
980
|
+
[sorted != info[:doc_lines], (info[:preserved_lines] + merged).join, info[:lines].join]
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
# Log changes for method doc updates.
|
|
550
984
|
#
|
|
551
985
|
# @private
|
|
986
|
+
# @param [Collector::Insertion] insertion
|
|
987
|
+
# @param [Hash] merge_result
|
|
988
|
+
# @param [String] new_block
|
|
989
|
+
# @param [String] old_block
|
|
552
990
|
# @param [Array<Hash>] changes
|
|
553
|
-
# @param [Symbol] type
|
|
554
|
-
# @param [Docscribe::InlineRewriter::Collector::Insertion] insertion
|
|
555
991
|
# @param [String] file
|
|
556
|
-
# @param [
|
|
557
|
-
# @
|
|
558
|
-
|
|
992
|
+
# @param [Hash] rest additional keyword arguments forwarded to add_change
|
|
993
|
+
# @return [Object]
|
|
994
|
+
def log_method_doc_changes!(insertion:, merge_result:, **rest)
|
|
995
|
+
reason_specs = merge_result[:reasons] || []
|
|
996
|
+
type_mismatch_reasons = reason_specs.select { |r| %i[updated_param updated_return].include?(r[:type]) }
|
|
997
|
+
|
|
998
|
+
return unless rest[:new_block] != rest[:old_block] || type_mismatch_reasons.any?
|
|
999
|
+
|
|
1000
|
+
reason_specs.each do |reason|
|
|
1001
|
+
add_change(changes: rest[:changes], type: reason[:type], insertion: insertion,
|
|
1002
|
+
file: rest[:file], message: reason[:message], extra: reason[:extra] || {})
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Apply method insertion in safe mode when no existing doc info is present.
|
|
1007
|
+
#
|
|
1008
|
+
# @private
|
|
1009
|
+
# @param [Hash] options keyword options
|
|
559
1010
|
# @return [void]
|
|
560
|
-
def
|
|
1011
|
+
def apply_method_insertion_safe_without_info!(**options)
|
|
1012
|
+
rewriter = options[:rewriter]
|
|
1013
|
+
insertion = options[:insertion]
|
|
1014
|
+
anchor_bol_range = options[:anchor_bol_range]
|
|
1015
|
+
doc = build_method_doc(insertion, **filter_method_doc_params(options))
|
|
1016
|
+
return if doc.nil? || doc.empty?
|
|
1017
|
+
|
|
1018
|
+
rewriter.insert_before(anchor_bol_range, doc)
|
|
1019
|
+
add_change(changes: options[:changes], type: :insert_full_doc_block,
|
|
1020
|
+
insertion: insertion, file: options[:file], message: 'missing docs')
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# Filter options to keep only doc-building params for safe-without-info mode.
|
|
1024
|
+
# @private
|
|
1025
|
+
# @param [Object] options the full options hash to filter
|
|
1026
|
+
# @return [Object]
|
|
1027
|
+
def filter_method_doc_params(options)
|
|
1028
|
+
options.reject { |k, _| %i[rewriter buffer insertion anchor_bol_range changes file strategy].include?(k) }
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# Append a structured change record.
|
|
1032
|
+
#
|
|
1033
|
+
# @private
|
|
1034
|
+
# @param [Hash] options kwargs for change record (type, file, line, method, message, insertion, changes, extra)
|
|
1035
|
+
# @return [void]
|
|
1036
|
+
def add_change(**options)
|
|
1037
|
+
changes = options[:changes]
|
|
561
1038
|
changes << {
|
|
562
|
-
type: type,
|
|
563
|
-
file: file,
|
|
564
|
-
line: line || method_line_for(insertion),
|
|
565
|
-
method: method_id_for(insertion),
|
|
566
|
-
message: message
|
|
567
|
-
}.merge(extra)
|
|
1039
|
+
type: options[:type],
|
|
1040
|
+
file: options[:file],
|
|
1041
|
+
line: options[:line] || method_line_for(options[:insertion]),
|
|
1042
|
+
method: method_id_for(options[:insertion]),
|
|
1043
|
+
message: options[:message]
|
|
1044
|
+
}.merge(options[:extra] || {})
|
|
568
1045
|
end
|
|
569
1046
|
|
|
570
1047
|
# Build a printable method identifier from a collected insertion.
|
|
@@ -580,154 +1057,201 @@ module Docscribe
|
|
|
580
1057
|
# Apply one attribute insertion according to the selected strategy.
|
|
581
1058
|
#
|
|
582
1059
|
# @private
|
|
583
|
-
# @param [
|
|
584
|
-
# @param [Parser::Source::Buffer] buffer
|
|
585
|
-
# @param [Docscribe::InlineRewriter::Collector::AttrInsertion] insertion
|
|
586
|
-
# @param [Docscribe::Config] config
|
|
587
|
-
# @param [Object, nil] signature_provider
|
|
588
|
-
# @param [Symbol] strategy
|
|
589
|
-
# @param [Hash] merge_inserts
|
|
1060
|
+
# @param [Hash] options kwargs (insertion, config, rewriter, buffer, strategy, signature_provider, merge_inserts)
|
|
590
1061
|
# @return [void]
|
|
591
|
-
def apply_attr_insertion!(
|
|
1062
|
+
def apply_attr_insertion!(**options)
|
|
1063
|
+
config = options[:config]
|
|
592
1064
|
return unless config.respond_to?(:emit_attributes?) && config.emit_attributes?
|
|
593
|
-
return unless attribute_allowed?(config, insertion)
|
|
1065
|
+
return unless attribute_allowed?(config, options[:insertion])
|
|
594
1066
|
|
|
595
|
-
bol_range = SourceHelpers.line_start_range(buffer, insertion.node)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if (range = SourceHelpers.comment_block_removal_range(buffer, bol_range.begin_pos))
|
|
600
|
-
rewriter.remove(range)
|
|
601
|
-
end
|
|
1067
|
+
bol_range = SourceHelpers.line_start_range(options[:buffer], options[:insertion].node)
|
|
1068
|
+
params = attr_insertion_params(options[:insertion], config, options[:signature_provider], bol_range)
|
|
1069
|
+
dispatch_attr_strategy(params, options)
|
|
1070
|
+
end
|
|
602
1071
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1072
|
+
#
|
|
1073
|
+
# @private
|
|
1074
|
+
# @param [Object] insertion the collected attribute insertion
|
|
1075
|
+
# @param [Object] config the active Docscribe::Config
|
|
1076
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
1077
|
+
# @param [Object] bol_range the beginning-of-line range for the attribute node
|
|
1078
|
+
# @return [Hash]
|
|
1079
|
+
def attr_insertion_params(insertion, config, signature_provider, bol_range)
|
|
1080
|
+
{
|
|
1081
|
+
insertion: insertion, config: config,
|
|
1082
|
+
signature_provider: signature_provider, bol_range: bol_range
|
|
1083
|
+
}
|
|
1084
|
+
end
|
|
609
1085
|
|
|
610
|
-
|
|
1086
|
+
# Dispatch attr insertion to the aggressive or safe handler.
|
|
1087
|
+
# @private
|
|
1088
|
+
# @param [Object] params precomputed attribute insertion parameters
|
|
1089
|
+
# @param [Object] options the full keyword options hash
|
|
1090
|
+
# @return [Object]
|
|
1091
|
+
def dispatch_attr_strategy(params, options)
|
|
1092
|
+
case options[:strategy]
|
|
1093
|
+
when :aggressive then apply_attr_aggressive!(params, options[:rewriter])
|
|
1094
|
+
when :safe then apply_attr_safe!(params, options[:merge_inserts], options[:rewriter], options[:buffer])
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
611
1097
|
|
|
612
|
-
|
|
613
|
-
|
|
1098
|
+
#
|
|
1099
|
+
# @private
|
|
1100
|
+
# @param [Object] params precomputed attribute insertion parameters
|
|
1101
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
1102
|
+
# @return [Object]
|
|
1103
|
+
def apply_attr_aggressive!(params, rewriter)
|
|
1104
|
+
if (range = SourceHelpers.comment_block_removal_range(params[:bol_range].begin_pos))
|
|
1105
|
+
rewriter.remove(range)
|
|
1106
|
+
end
|
|
614
1107
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
existing_lines: info[:lines],
|
|
619
|
-
config: config,
|
|
620
|
-
signature_provider: signature_provider
|
|
621
|
-
)
|
|
1108
|
+
doc = build_attr_doc_for_node(params[:insertion], config: params[:config],
|
|
1109
|
+
signature_provider: params[:signature_provider])
|
|
1110
|
+
return if doc.nil? || doc.empty?
|
|
622
1111
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
end
|
|
1112
|
+
rewriter.insert_before(params[:bol_range], doc)
|
|
1113
|
+
end
|
|
626
1114
|
|
|
627
|
-
|
|
628
|
-
|
|
1115
|
+
#
|
|
1116
|
+
# @private
|
|
1117
|
+
# @param [Object] params precomputed attribute insertion parameters
|
|
1118
|
+
# @param [Object] merge_inserts hash mapping end positions to arrays of doc additions
|
|
1119
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
1120
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
1121
|
+
# @return [Object]
|
|
1122
|
+
def apply_attr_safe!(params, merge_inserts, rewriter, buffer)
|
|
1123
|
+
info = SourceHelpers.doc_comment_block_info(buffer, params[:bol_range].begin_pos)
|
|
1124
|
+
|
|
1125
|
+
if info
|
|
1126
|
+
merge_attr_additions!(insertion: params[:insertion], info: info, merge_inserts: merge_inserts,
|
|
1127
|
+
config: params[:config], signature_provider: params[:signature_provider])
|
|
1128
|
+
return
|
|
1129
|
+
end
|
|
629
1130
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
signature_provider: signature_provider
|
|
634
|
-
)
|
|
635
|
-
return if doc.nil? || doc.empty?
|
|
1131
|
+
doc = build_attr_doc_for_node(params[:insertion], config: params[:config],
|
|
1132
|
+
signature_provider: params[:signature_provider])
|
|
1133
|
+
return if doc.nil? || doc.empty?
|
|
636
1134
|
|
|
637
|
-
|
|
638
|
-
end
|
|
1135
|
+
rewriter.insert_before(params[:bol_range], doc)
|
|
639
1136
|
end
|
|
640
1137
|
|
|
641
|
-
# Apply aggregated merge inserts at shared end positions.
|
|
642
1138
|
#
|
|
643
|
-
#
|
|
1139
|
+
# @private
|
|
1140
|
+
# @param [Object] insertion the collected attribute insertion
|
|
1141
|
+
# @param [Object] info hash containing existing doc comment block data
|
|
1142
|
+
# @param [Object] merge_inserts hash mapping end positions to arrays of doc additions
|
|
1143
|
+
# @param [Object] config the active Docscribe::Config
|
|
1144
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
1145
|
+
# @return [Object]
|
|
1146
|
+
def merge_attr_additions!(insertion:, info:, merge_inserts:, config:, signature_provider:)
|
|
1147
|
+
additions = build_attr_merge_additions(ins: insertion, existing_lines: info[:lines],
|
|
1148
|
+
config: config, signature_provider: signature_provider)
|
|
1149
|
+
return unless additions && !additions.empty?
|
|
1150
|
+
|
|
1151
|
+
merge_inserts[info[:end_pos]] << [insertion.node.loc.expression.begin_pos, additions]
|
|
1152
|
+
end
|
|
1153
|
+
|
|
644
1154
|
#
|
|
645
1155
|
# @private
|
|
646
|
-
# @param [
|
|
647
|
-
# @param [
|
|
648
|
-
# @param [
|
|
649
|
-
# @return [
|
|
1156
|
+
# @param [Object] rewriter the TreeRewriter accumulating source transformations
|
|
1157
|
+
# @param [Object] buffer the source buffer being rewritten
|
|
1158
|
+
# @param [Object] merge_inserts hash mapping end positions to arrays of attribute doc additions
|
|
1159
|
+
# @return [Object]
|
|
650
1160
|
def apply_merge_inserts!(rewriter:, buffer:, merge_inserts:)
|
|
651
|
-
sep_re = /^\s*#\s*\r?\n$/
|
|
652
|
-
|
|
653
1161
|
merge_inserts.keys.sort.reverse_each do |end_pos|
|
|
654
|
-
|
|
655
|
-
next if
|
|
1162
|
+
text = merge_text_for_pos(merge_inserts[end_pos])
|
|
1163
|
+
next if text.nil? || text.empty?
|
|
656
1164
|
|
|
657
|
-
|
|
1165
|
+
range = Parser::Source::Range.new(buffer, end_pos, end_pos)
|
|
1166
|
+
rewriter.insert_before(range, text)
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
658
1169
|
|
|
659
|
-
|
|
1170
|
+
#
|
|
1171
|
+
# @private
|
|
1172
|
+
# @param [Object] chunks array of [sort_key, doc_text] pairs for a given merge position
|
|
1173
|
+
# @return [Object, nil]
|
|
1174
|
+
def merge_text_for_pos(chunks)
|
|
1175
|
+
return nil if chunks.empty?
|
|
660
1176
|
|
|
661
|
-
|
|
662
|
-
|
|
1177
|
+
chunks = chunks.sort_by { |(sort_key, _s)| sort_key }
|
|
1178
|
+
out_lines = [] #: Array[String]
|
|
1179
|
+
sep_re = /^\s*#\s*\r?\n$/
|
|
663
1180
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
seps << lines.shift while !lines.empty? && lines.first.match?(sep_re)
|
|
1181
|
+
chunks.each do |(_k, chunk)|
|
|
1182
|
+
next if chunk.nil? || chunk.empty?
|
|
667
1183
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
out_lines.concat(lines)
|
|
671
|
-
end
|
|
1184
|
+
merge_chunk_into_out(chunk, out_lines, sep_re)
|
|
1185
|
+
end
|
|
672
1186
|
|
|
673
|
-
|
|
674
|
-
|
|
1187
|
+
text = out_lines.join
|
|
1188
|
+
text.empty? ? nil : text
|
|
1189
|
+
end
|
|
675
1190
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1191
|
+
# Merge a single chunk into the output lines array.
|
|
1192
|
+
# @private
|
|
1193
|
+
# @param [Object] chunk the doc text string to merge
|
|
1194
|
+
# @param [Object] out_lines the accumulated output lines array
|
|
1195
|
+
# @param [Object] sep_re regex matching separator comment lines (# followed by newline)
|
|
1196
|
+
# @return [Object]
|
|
1197
|
+
def merge_chunk_into_out(chunk, out_lines, sep_re)
|
|
1198
|
+
lines = chunk.lines
|
|
1199
|
+
seps = extract_separators(lines, sep_re)
|
|
1200
|
+
sep = seps.first
|
|
1201
|
+
out_lines << sep if sep && (out_lines.empty? || !out_lines.last.match?(sep_re))
|
|
1202
|
+
out_lines.concat(lines)
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
# Extract leading separator lines from a chunk.
|
|
1206
|
+
# @private
|
|
1207
|
+
# @param [Object] lines array of lines from the chunk
|
|
1208
|
+
# @param [Object] sep_re regex matching separator comment lines
|
|
1209
|
+
# @return [Object]
|
|
1210
|
+
def extract_separators(lines, sep_re)
|
|
1211
|
+
seps = [] #: Array[String]
|
|
1212
|
+
seps << lines.shift while !lines.empty? && lines.first.match?(sep_re)
|
|
1213
|
+
seps
|
|
679
1214
|
end
|
|
680
1215
|
|
|
681
|
-
# Build plain-text merge additions for an attribute doc block.
|
|
682
1216
|
#
|
|
683
1217
|
# @private
|
|
684
|
-
# @param [
|
|
685
|
-
# @param [
|
|
686
|
-
# @param [Docscribe::Config
|
|
687
|
-
# @param [Object] signature_provider
|
|
1218
|
+
# @param [Object] ins the attribute insertion object
|
|
1219
|
+
# @param [Object] existing_lines array of existing doc comment lines
|
|
1220
|
+
# @param [Object] config the active Docscribe::Config
|
|
1221
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
688
1222
|
# @raise [StandardError]
|
|
689
|
-
# @return [
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
existing = existing_attr_names(existing_lines)
|
|
694
|
-
missing = ins.names.reject { |name_sym| existing[name_sym.to_s] }
|
|
1223
|
+
# @return [Object]
|
|
1224
|
+
# @return [nil] if StandardError
|
|
1225
|
+
def build_attr_merge_additions(ins:, existing_lines:, config:, signature_provider:)
|
|
1226
|
+
missing = missing_attr_names(ins, existing_lines)
|
|
695
1227
|
return '' if missing.empty?
|
|
696
1228
|
|
|
697
|
-
|
|
1229
|
+
indent = SourceHelpers.line_indent(ins.node)
|
|
1230
|
+
lines = [] #: Array[String]
|
|
698
1231
|
lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
attr_name = name_sym.to_s
|
|
702
|
-
mode = ins.access.to_s
|
|
703
|
-
attr_type = attribute_type(ins, name_sym, config, signature_provider: signature_provider)
|
|
704
|
-
|
|
705
|
-
lines << "#{indent}# @!attribute [#{mode}] #{attr_name}"
|
|
706
|
-
|
|
707
|
-
if config.emit_visibility_tags?
|
|
708
|
-
lines << "#{indent}# @private" if ins.visibility == :private
|
|
709
|
-
lines << "#{indent}# @protected" if ins.visibility == :protected
|
|
710
|
-
end
|
|
711
|
-
|
|
712
|
-
lines << "#{indent}# @return [#{attr_type}]" if %i[r rw].include?(ins.access)
|
|
713
|
-
if %i[w rw].include?(ins.access)
|
|
714
|
-
lines << format_attribute_param_tag(indent, 'value', attr_type, style: param_tag_style)
|
|
715
|
-
end
|
|
716
|
-
lines << "#{indent}#" if idx < missing.length - 1
|
|
717
|
-
end
|
|
718
|
-
|
|
1232
|
+
lines.concat(build_attr_doc_lines(ins, indent: indent, config: config,
|
|
1233
|
+
signature_provider: signature_provider, names: missing))
|
|
719
1234
|
lines.map { |l| "#{l}\n" }.join
|
|
720
1235
|
rescue StandardError
|
|
721
1236
|
nil
|
|
722
1237
|
end
|
|
723
1238
|
|
|
724
|
-
# Extract already documented attribute names from existing `@!attribute` lines.
|
|
725
1239
|
#
|
|
726
1240
|
# @private
|
|
727
|
-
# @param [
|
|
728
|
-
# @
|
|
1241
|
+
# @param [Object] ins the attribute insertion object
|
|
1242
|
+
# @param [Object] existing_lines array of existing doc comment lines
|
|
1243
|
+
# @return [Object]
|
|
1244
|
+
def missing_attr_names(ins, existing_lines)
|
|
1245
|
+
existing = existing_attr_names(existing_lines)
|
|
1246
|
+
ins.names.reject { |name_sym| existing[name_sym.to_s] }
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
#
|
|
1250
|
+
# @private
|
|
1251
|
+
# @param [Object] lines array of existing doc comment lines
|
|
1252
|
+
# @return [Object]
|
|
729
1253
|
def existing_attr_names(lines)
|
|
730
|
-
names = {}
|
|
1254
|
+
names = {} #: Hash[String, bool]
|
|
731
1255
|
|
|
732
1256
|
Array(lines).each do |line|
|
|
733
1257
|
if (m = line.match(/^\s*#\s*@!attribute\b(?:\s+\[[^\]]+\])?\s+(\S+)/))
|
|
@@ -746,66 +1270,129 @@ module Docscribe
|
|
|
746
1270
|
# @return [Boolean]
|
|
747
1271
|
def attribute_allowed?(config, ins)
|
|
748
1272
|
ins.names.any? do |name_sym|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
ok ||= config.process_method?(
|
|
753
|
-
container: ins.container,
|
|
754
|
-
scope: ins.scope,
|
|
755
|
-
visibility: ins.visibility,
|
|
756
|
-
name: name_sym
|
|
757
|
-
)
|
|
758
|
-
end
|
|
1273
|
+
allowed_for_access?(config, ins, name_sym)
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
759
1276
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1277
|
+
#
|
|
1278
|
+
# @private
|
|
1279
|
+
# @param [Object] config the active Docscribe::Config
|
|
1280
|
+
# @param [Object] ins the attribute insertion object
|
|
1281
|
+
# @param [Object] name_sym the attribute name as a Symbol
|
|
1282
|
+
# @return [Object]
|
|
1283
|
+
def allowed_for_access?(config, ins, name_sym)
|
|
1284
|
+
ok = false
|
|
1285
|
+
|
|
1286
|
+
if %i[r rw].include?(ins.access)
|
|
1287
|
+
ok ||= config.process_method?(container: ins.container, scope: ins.scope,
|
|
1288
|
+
visibility: ins.visibility, name: name_sym)
|
|
1289
|
+
end
|
|
768
1290
|
|
|
769
|
-
|
|
1291
|
+
if %i[w rw].include?(ins.access)
|
|
1292
|
+
ok ||= config.process_method?(container: ins.container, scope: ins.scope,
|
|
1293
|
+
visibility: ins.visibility, name: :"#{name_sym}=")
|
|
770
1294
|
end
|
|
1295
|
+
|
|
1296
|
+
ok
|
|
771
1297
|
end
|
|
772
1298
|
|
|
773
|
-
# Build a full `@!attribute` documentation block for one attribute insertion.
|
|
774
1299
|
#
|
|
775
1300
|
# @private
|
|
776
|
-
# @param [
|
|
777
|
-
# @param [Docscribe::Config
|
|
778
|
-
# @param [Object] signature_provider
|
|
1301
|
+
# @param [Object] ins the attribute insertion object
|
|
1302
|
+
# @param [Object] config the active Docscribe::Config
|
|
1303
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
779
1304
|
# @raise [StandardError]
|
|
780
|
-
# @return [
|
|
1305
|
+
# @return [Object]
|
|
1306
|
+
# @return [nil] if StandardError
|
|
781
1307
|
def build_attr_doc_for_node(ins, config:, signature_provider:)
|
|
782
1308
|
indent = SourceHelpers.line_indent(ins.node)
|
|
783
|
-
|
|
784
|
-
lines
|
|
1309
|
+
lines = build_attr_doc_lines(ins, indent: indent, config: config, signature_provider: signature_provider)
|
|
1310
|
+
lines.map { |l| "#{l}\n" }.join
|
|
1311
|
+
rescue StandardError
|
|
1312
|
+
nil
|
|
1313
|
+
end
|
|
785
1314
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1315
|
+
#
|
|
1316
|
+
# @private
|
|
1317
|
+
# @param [Object] ins the attribute insertion object
|
|
1318
|
+
# @param [Object] indent whitespace indentation prefix derived from the attribute node
|
|
1319
|
+
# @param [Object] config the active Docscribe::Config
|
|
1320
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
1321
|
+
# @param [nil] names optional subset of attribute names to document (defaults to all names)
|
|
1322
|
+
# @return [Object]
|
|
1323
|
+
def build_attr_doc_lines(ins, indent:, config:, signature_provider:, names: nil)
|
|
1324
|
+
names ||= ins.names
|
|
1325
|
+
lines = [] #: Array[untyped]
|
|
1326
|
+
|
|
1327
|
+
names.each_with_index do |name_sym, idx|
|
|
1328
|
+
lines.concat(build_single_attr_lines(ins, name_sym, indent: indent,
|
|
1329
|
+
config: config, signature_provider: signature_provider,
|
|
1330
|
+
idx: idx, total: names.length))
|
|
1331
|
+
end
|
|
790
1332
|
|
|
791
|
-
|
|
1333
|
+
lines
|
|
1334
|
+
end
|
|
792
1335
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1336
|
+
# Build doc lines for a single attribute.
|
|
1337
|
+
# @private
|
|
1338
|
+
# @param [Object] ins the attribute insertion object
|
|
1339
|
+
# @param [Object] name_sym the attribute name as a Symbol
|
|
1340
|
+
# @param [Object] idx index of the current attribute in the names array
|
|
1341
|
+
# @param [Object] total total number of attributes to document
|
|
1342
|
+
# @param [Object] indent whitespace indentation prefix
|
|
1343
|
+
# @param [Object] config the active Docscribe::Config
|
|
1344
|
+
# @param [Object] signature_provider external RBS signature provider
|
|
1345
|
+
# @param [Hash] opts additional keyword arguments forwarded from build_attr_doc_lines
|
|
1346
|
+
# @return [Object]
|
|
1347
|
+
def build_single_attr_lines(ins, name_sym, indent:, **opts)
|
|
1348
|
+
cfg = opts[:config]
|
|
1349
|
+
attr_type = attribute_type(ins, name_sym, cfg, signature_provider: opts[:signature_provider])
|
|
1350
|
+
lines = ["#{indent}# @!attribute [#{ins.access}] #{name_sym}"]
|
|
1351
|
+
lines.concat(attr_visibility_lines(indent, cfg, ins))
|
|
1352
|
+
append_attr_return_tag(lines, indent, attr_type, ins.access)
|
|
1353
|
+
append_attr_param_tag(lines, indent, attr_type, ins.access, cfg)
|
|
1354
|
+
lines << "#{indent}#" if opts[:idx] < opts[:total] - 1
|
|
1355
|
+
lines
|
|
1356
|
+
end
|
|
797
1357
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1358
|
+
# Append @return tag for readable attribute.
|
|
1359
|
+
# @private
|
|
1360
|
+
# @param [Object] lines the doc lines array being built
|
|
1361
|
+
# @param [Object] indent whitespace indentation prefix
|
|
1362
|
+
# @param [Object] attr_type the resolved type string for the attribute
|
|
1363
|
+
# @param [Object] access the access level (:r, :w, or :rw)
|
|
1364
|
+
# @return [Object]
|
|
1365
|
+
def append_attr_return_tag(lines, indent, attr_type, access)
|
|
1366
|
+
lines << "#{indent}# @return [#{attr_type}]" if %i[r rw].include?(access)
|
|
1367
|
+
end
|
|
802
1368
|
|
|
803
|
-
|
|
804
|
-
|
|
1369
|
+
# Append @param tag for writable attribute.
|
|
1370
|
+
# @private
|
|
1371
|
+
# @param [Object] lines the doc lines array being built
|
|
1372
|
+
# @param [Object] indent whitespace indentation prefix
|
|
1373
|
+
# @param [Object] attr_type the resolved type string for the attribute
|
|
1374
|
+
# @param [Object] access the access level (:r, :w, or :rw)
|
|
1375
|
+
# @param [Object] cfg the active Docscribe::Config
|
|
1376
|
+
# @return [Object]
|
|
1377
|
+
def append_attr_param_tag(lines, indent, attr_type, access, cfg)
|
|
1378
|
+
return unless %i[w rw].include?(access)
|
|
1379
|
+
|
|
1380
|
+
lines << format_attribute_param_tag(indent, 'value', attr_type, style: cfg.param_tag_style)
|
|
1381
|
+
end
|
|
805
1382
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1383
|
+
# Build visibility lines for an attribute.
|
|
1384
|
+
# @private
|
|
1385
|
+
# @param [Object] indent whitespace indentation prefix
|
|
1386
|
+
# @param [Object] config the active Docscribe::Config
|
|
1387
|
+
# @param [Object] ins the attribute insertion object
|
|
1388
|
+
# @return [Object]
|
|
1389
|
+
def attr_visibility_lines(indent, config, ins)
|
|
1390
|
+
return [] unless config.emit_visibility_tags?
|
|
1391
|
+
|
|
1392
|
+
lines = [] #: Array[String]
|
|
1393
|
+
lines << "#{indent}# @private" if ins.visibility == :private
|
|
1394
|
+
lines << "#{indent}# @protected" if ins.visibility == :protected
|
|
1395
|
+
lines
|
|
809
1396
|
end
|
|
810
1397
|
|
|
811
1398
|
# Format an attribute `@param` tag line using the configured param tag style.
|
|
@@ -835,7 +1422,7 @@ module Docscribe
|
|
|
835
1422
|
# @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins
|
|
836
1423
|
# @param [Symbol] name_sym
|
|
837
1424
|
# @param [Docscribe::Config] config
|
|
838
|
-
# @param [Object] signature_provider
|
|
1425
|
+
# @param [Object] signature_provider RBS signature provider
|
|
839
1426
|
# @raise [StandardError]
|
|
840
1427
|
# @return [String]
|
|
841
1428
|
def attribute_type(ins, name_sym, config, signature_provider:)
|
|
@@ -874,19 +1461,10 @@ module Docscribe
|
|
|
874
1461
|
#
|
|
875
1462
|
# @private
|
|
876
1463
|
# @param [Collector::Insertion] insertion the collected method insertion
|
|
877
|
-
# @param [
|
|
878
|
-
# @param [Object, nil] signature_provider external signature provider
|
|
879
|
-
# @param [Object, nil] core_rbs_provider RBS core type provider
|
|
880
|
-
# @param [Hash, nil] param_types parameter name -> type map
|
|
1464
|
+
# @param [Hash] options kwargs for DocBuilder.build (param_types, return_type_override, override_tags, config)
|
|
881
1465
|
# @return [String, nil] generated doc block or nil
|
|
882
|
-
def build_method_doc(insertion,
|
|
883
|
-
DocBuilder.build(
|
|
884
|
-
insertion,
|
|
885
|
-
config: config,
|
|
886
|
-
signature_provider: signature_provider,
|
|
887
|
-
core_rbs_provider: core_rbs_provider,
|
|
888
|
-
param_types: param_types
|
|
889
|
-
)
|
|
1466
|
+
def build_method_doc(insertion, **options)
|
|
1467
|
+
DocBuilder.build(insertion, **options)
|
|
890
1468
|
end
|
|
891
1469
|
|
|
892
1470
|
# Delegate to DocBuilder.build_missing_merge_result for generating missing doc lines only.
|
|
@@ -894,23 +1472,10 @@ module Docscribe
|
|
|
894
1472
|
# @private
|
|
895
1473
|
# @param [Collector::Insertion] insertion the collected method insertion
|
|
896
1474
|
# @param [Array<String>] existing_lines existing doc-like lines
|
|
897
|
-
# @param [
|
|
898
|
-
# @param [Object, nil] signature_provider external signature provider
|
|
899
|
-
# @param [Object, nil] core_rbs_provider RBS core type provider
|
|
900
|
-
# @param [Hash, nil] param_types parameter name -> type map
|
|
901
|
-
# @param [Object] strategy Param documentation.
|
|
1475
|
+
# @param [Hash] options keyword arguments forwarded to DocBuilder.build_missing_merge_result
|
|
902
1476
|
# @return [Hash] result with `:lines` and `:reasons` keys
|
|
903
|
-
def build_missing_method_merge_result(insertion, existing_lines:,
|
|
904
|
-
|
|
905
|
-
DocBuilder.build_missing_merge_result(
|
|
906
|
-
insertion,
|
|
907
|
-
existing_lines: existing_lines,
|
|
908
|
-
config: config,
|
|
909
|
-
signature_provider: signature_provider,
|
|
910
|
-
core_rbs_provider: core_rbs_provider,
|
|
911
|
-
param_types: param_types,
|
|
912
|
-
strategy: strategy
|
|
913
|
-
)
|
|
1477
|
+
def build_missing_method_merge_result(insertion, existing_lines:, **options)
|
|
1478
|
+
DocBuilder.build_missing_merge_result(insertion, existing_lines: existing_lines, **options)
|
|
914
1479
|
end
|
|
915
1480
|
|
|
916
1481
|
# Get doc comment block info (preceding comments) for a method insertion.
|
|
@@ -977,6 +1542,36 @@ module Docscribe
|
|
|
977
1542
|
insertion.node
|
|
978
1543
|
end
|
|
979
1544
|
end
|
|
1545
|
+
|
|
1546
|
+
# Extract method override data from an insertion hash.
|
|
1547
|
+
#
|
|
1548
|
+
# @private
|
|
1549
|
+
# @param [Object] method_override the raw override data
|
|
1550
|
+
# @return [Hash{Symbol => Object}] normalized override hash
|
|
1551
|
+
def extract_method_override!(method_override)
|
|
1552
|
+
return {} unless method_override.is_a?(Hash)
|
|
1553
|
+
|
|
1554
|
+
{
|
|
1555
|
+
return_type: method_override[:return_type],
|
|
1556
|
+
param_types: method_override[:param_types].is_a?(Hash) ? method_override[:param_types] : {},
|
|
1557
|
+
tags: normalize_override_tags(method_override[:tags])
|
|
1558
|
+
}
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# Normalize override tags into Plugin::Tag instances.
|
|
1562
|
+
#
|
|
1563
|
+
# @private
|
|
1564
|
+
# @param [Array<Object>] tags raw tag values
|
|
1565
|
+
# @return [Array<Docscribe::Plugin::Tag>]
|
|
1566
|
+
def normalize_override_tags(tags)
|
|
1567
|
+
Array(tags).filter_map do |tag|
|
|
1568
|
+
case tag
|
|
1569
|
+
when Docscribe::Plugin::Tag then tag
|
|
1570
|
+
when Hash
|
|
1571
|
+
Docscribe::Plugin::Tag.new(**tag.transform_keys(&:to_sym))
|
|
1572
|
+
end
|
|
1573
|
+
end
|
|
1574
|
+
end
|
|
980
1575
|
end
|
|
981
1576
|
end
|
|
982
1577
|
end
|