zard 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +27 -0
- data/CONTEXT.md +93 -0
- data/LICENSE +373 -0
- data/README.md +170 -0
- data/docs/adr/0001-contract-and-documentation-channels.md +13 -0
- data/docs/adr/0002-em-dash-zard-documentation-marker.md +29 -0
- data/docs/adr/0003-package-boundaries-and-rigor-lens.md +15 -0
- data/docs/agents/domain.md +15 -0
- data/docs/agents/issue-tracker.md +22 -0
- data/docs/agents/triage-labels.md +11 -0
- data/docs/supported-ruby-syntax.md +55 -0
- data/docs/usage.md +124 -0
- data/examples/catalog/expected.md +51 -0
- data/examples/catalog/lib/catalog/entry.rb +45 -0
- data/examples/catalog/lib/catalog.rb +5 -0
- data/lib/zard/model/v1.rb +111 -0
- data/lib/zard/parsing/parser.rb +1781 -0
- data/lib/zard/version.rb +5 -0
- data/lib/zard.rb +15 -0
- data/sig/zard.rbs +7 -0
- metadata +80 -0
|
@@ -0,0 +1,1781 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Zard
|
|
6
|
+
module Parsing
|
|
7
|
+
class Parser
|
|
8
|
+
def self.call(source, path:)
|
|
9
|
+
new(source, path).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(source, path)
|
|
13
|
+
@source = source
|
|
14
|
+
@path = path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
result = Prism.parse(@source)
|
|
19
|
+
diagnostics = result.errors.map do |error|
|
|
20
|
+
diagnostic("ruby.syntax", :error, error.message, span(error.location))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
declarations = DeclarationCollector.new(
|
|
24
|
+
@source,
|
|
25
|
+
@path,
|
|
26
|
+
result.comments,
|
|
27
|
+
diagnostics,
|
|
28
|
+
result.encoding,
|
|
29
|
+
result.source
|
|
30
|
+
).call(result.value)
|
|
31
|
+
|
|
32
|
+
Model::V1::Document.new(
|
|
33
|
+
path: @path,
|
|
34
|
+
declarations: declarations.freeze,
|
|
35
|
+
diagnostics: diagnostics.freeze
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def diagnostic(code, severity, message, location)
|
|
42
|
+
Model::V1::Diagnostic.new(code: code, severity: severity, message: message, span: location)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def span(location)
|
|
46
|
+
Model::V1::SourceSpan.new(
|
|
47
|
+
path: @path,
|
|
48
|
+
start_line: location.start_line,
|
|
49
|
+
start_column: location.start_column,
|
|
50
|
+
end_line: location.end_line,
|
|
51
|
+
end_column: location.end_column,
|
|
52
|
+
start_offset: location.start_offset,
|
|
53
|
+
end_offset: location.end_offset
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class DeclarationCollector < Prism::Visitor
|
|
59
|
+
ATTRIBUTE_KINDS = {
|
|
60
|
+
attr: :attribute_reader,
|
|
61
|
+
attr_reader: :attribute_reader,
|
|
62
|
+
attr_writer: :attribute_writer,
|
|
63
|
+
attr_accessor: :attribute_accessor
|
|
64
|
+
}.freeze
|
|
65
|
+
VISIBILITY_NAMES = %i[public protected private].freeze
|
|
66
|
+
CLASS_METHOD_VISIBILITY = {
|
|
67
|
+
private_class_method: :private,
|
|
68
|
+
public_class_method: :public
|
|
69
|
+
}.freeze
|
|
70
|
+
CONSTANT_VISIBILITY = {
|
|
71
|
+
private_constant: :private,
|
|
72
|
+
public_constant: :public
|
|
73
|
+
}.freeze
|
|
74
|
+
MIXIN_NAMES = %i[include prepend extend].freeze
|
|
75
|
+
METHOD_DEFINITION_NAMES = %i[define_method define_singleton_method].freeze
|
|
76
|
+
METHOD_REMOVAL_NAMES = %i[remove_method undef_method].freeze
|
|
77
|
+
EVALUATION_NAMES = %i[class_eval module_eval class_exec module_exec].freeze
|
|
78
|
+
INSTANCE_EVALUATION_NAMES = %i[instance_eval instance_exec].freeze
|
|
79
|
+
CONTAINER_BUILDERS = {
|
|
80
|
+
["Data", :define] => {kind: :class, attribute_kind: :attribute_reader}.freeze,
|
|
81
|
+
["Struct", :new] => {kind: :class, attribute_kind: :attribute_accessor}.freeze,
|
|
82
|
+
["Class", :new] => {kind: :class, attribute_kind: nil}.freeze,
|
|
83
|
+
["Module", :new] => {kind: :module, attribute_kind: nil}.freeze,
|
|
84
|
+
[nil, :DelegateClass] => {kind: :class, attribute_kind: nil}.freeze
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
def initialize(source, path, comments, diagnostics, encoding, prism_source)
|
|
88
|
+
@source = source
|
|
89
|
+
@path = path
|
|
90
|
+
@comments = comments.select { |comment| standalone?(comment) && !magic_comment?(comment) }
|
|
91
|
+
@diagnostics = diagnostics
|
|
92
|
+
@encoding = encoding
|
|
93
|
+
@prism_source = prism_source
|
|
94
|
+
@declarations = []
|
|
95
|
+
@namespace = []
|
|
96
|
+
@singleton_depth = 0
|
|
97
|
+
@singleton_receiver = nil
|
|
98
|
+
@singleton_receiver_span = nil
|
|
99
|
+
@refinement = nil
|
|
100
|
+
@refinement_span = nil
|
|
101
|
+
@instance_visibility = :public
|
|
102
|
+
@singleton_visibility = :public
|
|
103
|
+
@container_kind = nil
|
|
104
|
+
@module_function_mode = false
|
|
105
|
+
@method_depth = 0
|
|
106
|
+
@container_declaration_index = nil
|
|
107
|
+
@instance_evaluation_receiver = nil
|
|
108
|
+
@instance_evaluation_receiver_span = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def call(program)
|
|
112
|
+
program.accept(self)
|
|
113
|
+
@declarations.freeze
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def visit_module_node(node)
|
|
117
|
+
declaration_index = collect_path_declaration(:module, node.constant_path.location.slice, node)
|
|
118
|
+
within_namespace(node.constant_path.location.slice, :module, declaration_index) { node.body&.accept(self) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def visit_class_node(node)
|
|
122
|
+
builder_call = node.superclass if class_builder?(node.superclass)
|
|
123
|
+
declaration_index = collect_path_declaration(
|
|
124
|
+
:class,
|
|
125
|
+
node.constant_path.location.slice,
|
|
126
|
+
node,
|
|
127
|
+
superclass: node.superclass&.location&.slice,
|
|
128
|
+
superclass_span: node.superclass ? span(node.superclass.location) : nil
|
|
129
|
+
)
|
|
130
|
+
within_namespace(node.constant_path.location.slice, :class, declaration_index) do
|
|
131
|
+
collect_container_builder_attributes(builder_call) if builder_call
|
|
132
|
+
node.body&.accept(self)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def visit_constant_write_node(node)
|
|
137
|
+
path = node.name.to_s
|
|
138
|
+
visibility = constant_visibility_for(path)
|
|
139
|
+
replace_constant_declarations(path) unless self_referential_constant_guard?(node.value, path)
|
|
140
|
+
call = container_builder_call(node.value, guarded_name: path)
|
|
141
|
+
return visit_container_builder_write(path, node, call, visibility: visibility) if call
|
|
142
|
+
|
|
143
|
+
collect_path_declaration(:constant, path, node, visibility: visibility)
|
|
144
|
+
super
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def visit_constant_path_write_node(node)
|
|
148
|
+
path = node.target.location.slice
|
|
149
|
+
visibility = constant_visibility_for(path)
|
|
150
|
+
guarded_name = constant_reference_name(node.target)
|
|
151
|
+
replace_constant_declarations(path) unless self_referential_constant_guard?(node.value, guarded_name)
|
|
152
|
+
call = container_builder_call(node.value, guarded_name: guarded_name)
|
|
153
|
+
return visit_container_builder_write(path, node, call, visibility: visibility) if call
|
|
154
|
+
|
|
155
|
+
collect_path_declaration(:constant, path, node, visibility: visibility)
|
|
156
|
+
super
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def visit_constant_or_write_node(node)
|
|
160
|
+
call = container_builder_call(node.value)
|
|
161
|
+
return visit_container_builder_write(node.name.to_s, node, call) if call
|
|
162
|
+
|
|
163
|
+
super
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def visit_constant_path_or_write_node(node)
|
|
167
|
+
call = container_builder_call(node.value)
|
|
168
|
+
return visit_container_builder_write(node.target.location.slice, node, call) if call
|
|
169
|
+
|
|
170
|
+
super
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def visit_singleton_class_node(node)
|
|
174
|
+
previous_visibility = @singleton_visibility
|
|
175
|
+
previous_receiver = @singleton_receiver
|
|
176
|
+
previous_receiver_span = @singleton_receiver_span
|
|
177
|
+
@singleton_visibility = :public
|
|
178
|
+
@singleton_receiver = node.expression.location.slice
|
|
179
|
+
@singleton_receiver_span = span(node.expression.location)
|
|
180
|
+
@singleton_depth += 1
|
|
181
|
+
node.body&.accept(self)
|
|
182
|
+
ensure
|
|
183
|
+
@singleton_depth -= 1
|
|
184
|
+
@singleton_visibility = previous_visibility
|
|
185
|
+
@singleton_receiver = previous_receiver
|
|
186
|
+
@singleton_receiver_span = previous_receiver_span
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def visit_def_node(node)
|
|
190
|
+
collect_method_declarations(node)
|
|
191
|
+
|
|
192
|
+
within_method_body { node.body&.accept(self) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def visit_alias_method_node(node)
|
|
196
|
+
collect_method_alias(node.new_name.unescaped, node.old_name.unescaped, node)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def visit_undef_node(node)
|
|
200
|
+
return super if @method_depth.positive?
|
|
201
|
+
|
|
202
|
+
remove_method_declarations(node.names.filter_map { |name| attribute_name(name) })
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def visit_call_node(node)
|
|
206
|
+
return super if @method_depth.positive?
|
|
207
|
+
return visit_current_instance_evaluation_call(node) if current_instance_evaluation_call?(node)
|
|
208
|
+
return if foreign_instance_evaluation_call?(node)
|
|
209
|
+
return if foreign_evaluation_call?(node)
|
|
210
|
+
|
|
211
|
+
return visit_refinement_call(node) if refinement_call?(node)
|
|
212
|
+
return visit_method_definition_call(node) if method_definition_call?(node)
|
|
213
|
+
return visit_const_set_call(node) if const_set_call?(node)
|
|
214
|
+
return visit_remove_const_call(node) if remove_const_call?(node)
|
|
215
|
+
|
|
216
|
+
collect_autoload_declaration(node) if autoload_call?(node)
|
|
217
|
+
collect_mixin_references(node) if mixin_call?(node)
|
|
218
|
+
|
|
219
|
+
if node.name == :module_function &&
|
|
220
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
221
|
+
module_function_context?
|
|
222
|
+
return visit_module_function_call(node) { super }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
226
|
+
(class_visibility = CLASS_METHOD_VISIBILITY[node.name])
|
|
227
|
+
return visit_class_method_visibility_call(node, class_visibility) { super }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
231
|
+
(constant_visibility = CONSTANT_VISIBILITY[node.name])
|
|
232
|
+
return visit_constant_visibility_call(node, constant_visibility) { super }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
visibility = VISIBILITY_NAMES.find do |name|
|
|
236
|
+
node.name == name && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
237
|
+
end
|
|
238
|
+
return visit_visibility_call(node, visibility) { super } if visibility
|
|
239
|
+
|
|
240
|
+
if node.name == :alias_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
241
|
+
collect_alias_method_call(node)
|
|
242
|
+
return super
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
if method_removal_call?(node)
|
|
246
|
+
names = node.arguments.arguments.filter_map { |argument| attribute_name(argument) }
|
|
247
|
+
remove_method_declarations(names)
|
|
248
|
+
return super
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
collect_attribute_declarations(node)
|
|
252
|
+
super
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def current_instance_evaluation_call?(node)
|
|
258
|
+
instance_evaluation_call?(node) &&
|
|
259
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def foreign_instance_evaluation_call?(node)
|
|
263
|
+
instance_evaluation_call?(node) && node.receiver &&
|
|
264
|
+
!node.receiver.is_a?(Prism::SelfNode)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def instance_evaluation_call?(node)
|
|
268
|
+
INSTANCE_EVALUATION_NAMES.include?(node.name) && node.block
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def visit_current_instance_evaluation_call(node)
|
|
272
|
+
previous_receiver = @instance_evaluation_receiver
|
|
273
|
+
previous_receiver_span = @instance_evaluation_receiver_span
|
|
274
|
+
@instance_evaluation_receiver = node.receiver&.location&.slice || "self"
|
|
275
|
+
@instance_evaluation_receiver_span = span(node.receiver.location) if node.receiver
|
|
276
|
+
node.block.body&.accept(self)
|
|
277
|
+
ensure
|
|
278
|
+
@instance_evaluation_receiver = previous_receiver
|
|
279
|
+
@instance_evaluation_receiver_span = previous_receiver_span
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def foreign_evaluation_call?(node)
|
|
283
|
+
EVALUATION_NAMES.include?(node.name) && node.block && node.receiver &&
|
|
284
|
+
!node.receiver.is_a?(Prism::SelfNode)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def autoload_call?(node)
|
|
288
|
+
return false unless node.name == :autoload && node.block.nil? && node.arguments&.arguments&.length == 2
|
|
289
|
+
return false if @singleton_depth.positive? || @refinement
|
|
290
|
+
|
|
291
|
+
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def collect_autoload_declaration(node)
|
|
295
|
+
name = attribute_name(node.arguments.arguments.first)
|
|
296
|
+
collect_path_declaration(:constant, name, node) if name
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def const_set_call?(node)
|
|
300
|
+
return false unless node.name == :const_set && node.block.nil? && node.arguments&.arguments&.length == 2
|
|
301
|
+
return false if @namespace.empty? || @singleton_depth.positive? || @refinement
|
|
302
|
+
|
|
303
|
+
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def visit_const_set_call(node)
|
|
307
|
+
name_node, value = node.arguments.arguments
|
|
308
|
+
name = attribute_name(name_node)
|
|
309
|
+
return unless name
|
|
310
|
+
|
|
311
|
+
visibility = constant_visibility_for(name)
|
|
312
|
+
replace_constant_declarations(name)
|
|
313
|
+
call = container_builder_call(value)
|
|
314
|
+
return visit_container_builder_write(name, node, call, visibility: visibility) if call
|
|
315
|
+
|
|
316
|
+
collect_path_declaration(:constant, name, node, visibility: visibility)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def remove_const_call?(node)
|
|
320
|
+
return false unless node.name == :remove_const && node.block.nil? && node.arguments&.arguments&.one?
|
|
321
|
+
return false if @namespace.empty? || @singleton_depth.positive? || @refinement
|
|
322
|
+
|
|
323
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
324
|
+
attribute_name(node.arguments.arguments.first)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def visit_remove_const_call(node)
|
|
328
|
+
remove_constant_declarations(attribute_name(node.arguments.arguments.first))
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def remove_constant_declarations(name)
|
|
332
|
+
remove_constant_path("#{current_namespace}::#{name}")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def replace_constant_declarations(path)
|
|
336
|
+
remove_constant_path(namespace_parts(path).join("::"))
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def constant_visibility_for(path)
|
|
340
|
+
full_path = namespace_parts(path).join("::")
|
|
341
|
+
declaration = @declarations.reverse_each.find { |item| constant_declaration_path(item) == full_path }
|
|
342
|
+
declaration ? declaration.visibility : :public
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def remove_constant_path(path)
|
|
346
|
+
container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
|
|
347
|
+
@declarations.reject! do |declaration|
|
|
348
|
+
constant_declaration_path(declaration) == path ||
|
|
349
|
+
declaration.namespace == path ||
|
|
350
|
+
declaration.namespace&.start_with?("#{path}::")
|
|
351
|
+
end
|
|
352
|
+
@container_declaration_index = @declarations.index(container_declaration) if container_declaration
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def self_referential_constant_guard?(node, name)
|
|
356
|
+
node.is_a?(Prism::OrNode) && constant_reference_name(node.left) == name
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def constant_declaration_path(declaration)
|
|
360
|
+
return unless %i[class module constant].include?(declaration.kind)
|
|
361
|
+
|
|
362
|
+
[declaration.namespace, declaration.name].compact.join("::")
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def class_builder?(node)
|
|
366
|
+
descriptor = container_builder_descriptor(node)
|
|
367
|
+
descriptor && descriptor.fetch(:kind) == :class
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def container_builder_descriptor(node)
|
|
371
|
+
call = container_builder_call(node)
|
|
372
|
+
return unless call
|
|
373
|
+
|
|
374
|
+
CONTAINER_BUILDERS[container_builder_key(call)]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def visit_container_builder_write(path, node, call, visibility: constant_visibility_for(path))
|
|
378
|
+
descriptor = CONTAINER_BUILDERS.fetch(container_builder_key(call))
|
|
379
|
+
builder, builder_span = container_builder_reference(call)
|
|
380
|
+
declaration_index = collect_path_declaration(
|
|
381
|
+
descriptor.fetch(:kind),
|
|
382
|
+
path,
|
|
383
|
+
node,
|
|
384
|
+
visibility: visibility,
|
|
385
|
+
container_builder: builder,
|
|
386
|
+
container_builder_span: builder_span
|
|
387
|
+
)
|
|
388
|
+
within_namespace(path, descriptor.fetch(:kind), declaration_index) do
|
|
389
|
+
collect_container_builder_attributes(call)
|
|
390
|
+
call.block&.body&.accept(self)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def container_builder_reference(call)
|
|
395
|
+
return [call.location.slice, span(call.location)] unless call.block
|
|
396
|
+
|
|
397
|
+
length = call.block.location.start_offset - call.location.start_offset
|
|
398
|
+
source = @source.byteslice(call.location.start_offset, length).rstrip
|
|
399
|
+
location = Prism::Location.new(@prism_source, call.location.start_offset, source.bytesize)
|
|
400
|
+
[source, span(location)]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def collect_container_builder_attributes(call)
|
|
404
|
+
call = attribute_builder_call(call)
|
|
405
|
+
return unless call
|
|
406
|
+
|
|
407
|
+
descriptor = CONTAINER_BUILDERS.fetch(container_builder_key(call))
|
|
408
|
+
attribute_kind = descriptor.fetch(:attribute_kind)
|
|
409
|
+
return unless attribute_kind
|
|
410
|
+
|
|
411
|
+
builder_attribute_arguments(call).each do |argument|
|
|
412
|
+
replace_method_declarations(
|
|
413
|
+
kind: :instance_method,
|
|
414
|
+
names: attribute_generated_method_names(argument.unescaped, attribute_kind),
|
|
415
|
+
namespace: current_namespace,
|
|
416
|
+
receiver: nil,
|
|
417
|
+
refinement: @refinement
|
|
418
|
+
)
|
|
419
|
+
append_declaration(
|
|
420
|
+
kind: :"instance_#{attribute_kind}",
|
|
421
|
+
name: argument.unescaped,
|
|
422
|
+
namespace: current_namespace,
|
|
423
|
+
visibility: :public,
|
|
424
|
+
parameters: [].freeze,
|
|
425
|
+
node: argument,
|
|
426
|
+
comments: [].freeze,
|
|
427
|
+
parsed: {documentation: [].freeze, contracts: [].freeze}
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def builder_attribute_arguments(call)
|
|
433
|
+
arguments = call.arguments&.arguments || []
|
|
434
|
+
arguments = arguments.drop(1) if container_builder_key(call) == ["Struct", :new] && arguments.first.is_a?(Prism::StringNode)
|
|
435
|
+
arguments.select { |argument| argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) }
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def attribute_builder_call(call)
|
|
439
|
+
wrapped = false
|
|
440
|
+
while class_new_builder?(call)
|
|
441
|
+
return if wrapped && call.block
|
|
442
|
+
|
|
443
|
+
arguments = call.arguments&.arguments
|
|
444
|
+
return unless arguments&.one?
|
|
445
|
+
|
|
446
|
+
call = arguments.first
|
|
447
|
+
wrapped = true
|
|
448
|
+
end
|
|
449
|
+
return unless call.is_a?(Prism::CallNode)
|
|
450
|
+
return if wrapped && call.block
|
|
451
|
+
|
|
452
|
+
descriptor = CONTAINER_BUILDERS[container_builder_key(call)]
|
|
453
|
+
call if descriptor&.fetch(:attribute_kind)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def class_new_builder?(call)
|
|
457
|
+
container_builder_key(call) == ["Class", :new]
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def container_builder_key(node)
|
|
461
|
+
return unless node.is_a?(Prism::CallNode)
|
|
462
|
+
|
|
463
|
+
receiver = node.receiver&.location&.slice&.sub(/\A::/, "")
|
|
464
|
+
[receiver, node.name]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def container_builder_call(node, guarded_name: nil)
|
|
468
|
+
node = unwrap_builder_guard(node, guarded_name) if guarded_name
|
|
469
|
+
node = node.receiver while freeze_tail?(node)
|
|
470
|
+
return unless node.is_a?(Prism::CallNode)
|
|
471
|
+
|
|
472
|
+
node if CONTAINER_BUILDERS.key?(container_builder_key(node))
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def unwrap_builder_guard(node, guarded_name)
|
|
476
|
+
return node unless node.is_a?(Prism::OrNode)
|
|
477
|
+
|
|
478
|
+
if constant_reference_name(node.left) == guarded_name
|
|
479
|
+
node.right
|
|
480
|
+
else
|
|
481
|
+
node
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def constant_reference_name(node)
|
|
486
|
+
return node.name.to_s if node.is_a?(Prism::ConstantReadNode)
|
|
487
|
+
return unless node.is_a?(Prism::ConstantPathNode)
|
|
488
|
+
|
|
489
|
+
parent = node.parent
|
|
490
|
+
return node.name.to_s unless parent
|
|
491
|
+
|
|
492
|
+
parent_name = constant_reference_name(parent)
|
|
493
|
+
"#{parent_name}::#{node.name}" if parent_name
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def freeze_tail?(node)
|
|
497
|
+
node.is_a?(Prism::CallNode) && node.name == :freeze && !node.safe_navigation? &&
|
|
498
|
+
node.receiver && node.arguments.nil? && node.block.nil?
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def method_definition_call?(node)
|
|
502
|
+
return false unless METHOD_DEFINITION_NAMES.include?(node.name)
|
|
503
|
+
return true if node.name == :define_singleton_method
|
|
504
|
+
|
|
505
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
506
|
+
(!@namespace.empty? || @refinement)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def visit_method_definition_call(node)
|
|
510
|
+
arguments = node.arguments&.arguments || []
|
|
511
|
+
name = attribute_name(arguments.first) if arguments.first
|
|
512
|
+
collect_call_method_declaration(node, name) if name
|
|
513
|
+
within_method_body { node.block&.body&.accept(self) }
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def collect_call_method_declaration(node, name)
|
|
517
|
+
parameters = parameter_names(node.block&.parameters&.parameters).freeze
|
|
518
|
+
singleton = node.name == :define_singleton_method
|
|
519
|
+
kind = singleton ? :singleton_method : current_method_kind
|
|
520
|
+
receiver = if singleton
|
|
521
|
+
node.receiver&.location&.slice || "self"
|
|
522
|
+
else
|
|
523
|
+
current_singleton_receiver
|
|
524
|
+
end
|
|
525
|
+
receiver_span = if singleton
|
|
526
|
+
span(node.receiver.location) if node.receiver
|
|
527
|
+
else
|
|
528
|
+
current_singleton_receiver_span
|
|
529
|
+
end
|
|
530
|
+
visibility = singleton ? @singleton_visibility : current_visibility
|
|
531
|
+
collect_method_entries(
|
|
532
|
+
kind: kind,
|
|
533
|
+
name: name,
|
|
534
|
+
namespace: current_namespace,
|
|
535
|
+
visibility: visibility,
|
|
536
|
+
parameters: parameters,
|
|
537
|
+
receiver: receiver,
|
|
538
|
+
receiver_span: receiver_span,
|
|
539
|
+
node: node
|
|
540
|
+
)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def refinement_call?(node)
|
|
544
|
+
node.name == :refine &&
|
|
545
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
546
|
+
node.block &&
|
|
547
|
+
@container_kind == :module &&
|
|
548
|
+
@singleton_depth.zero? &&
|
|
549
|
+
node.arguments&.arguments&.length == 1
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def visit_refinement_call(node)
|
|
553
|
+
target_node = node.arguments.arguments.fetch(0)
|
|
554
|
+
target = target_node.location.slice
|
|
555
|
+
collect_declaration(
|
|
556
|
+
kind: :refinement,
|
|
557
|
+
name: target,
|
|
558
|
+
namespace: current_namespace,
|
|
559
|
+
visibility: :public,
|
|
560
|
+
parameters: [].freeze,
|
|
561
|
+
node: node
|
|
562
|
+
)
|
|
563
|
+
within_refinement(target, span(target_node.location)) do
|
|
564
|
+
node.block.body&.accept(self)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def mixin_call?(node)
|
|
569
|
+
MIXIN_NAMES.include?(node.name) &&
|
|
570
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
571
|
+
%i[class module].include?(@container_kind) &&
|
|
572
|
+
@container_declaration_index &&
|
|
573
|
+
@singleton_depth.zero?
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def collect_mixin_references(node)
|
|
577
|
+
arguments = node.arguments&.arguments || []
|
|
578
|
+
return if arguments.empty?
|
|
579
|
+
|
|
580
|
+
declaration = @declarations.fetch(@container_declaration_index)
|
|
581
|
+
mixins = arguments.map do |argument|
|
|
582
|
+
Model::V1::MixinReference.new(
|
|
583
|
+
kind: node.name,
|
|
584
|
+
target: argument.location.slice,
|
|
585
|
+
span: span(argument.location)
|
|
586
|
+
)
|
|
587
|
+
end
|
|
588
|
+
@declarations[@container_declaration_index] = declaration_with_mixins(declaration, declaration.mixins + mixins)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def within_method_body
|
|
592
|
+
@method_depth += 1
|
|
593
|
+
yield
|
|
594
|
+
ensure
|
|
595
|
+
@method_depth -= 1
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def collect_alias_method_call(node)
|
|
599
|
+
arguments = node.arguments&.arguments || []
|
|
600
|
+
return unless arguments.length == 2
|
|
601
|
+
|
|
602
|
+
new_name, old_name = arguments.map { |argument| attribute_name(argument) }
|
|
603
|
+
collect_method_alias(new_name, old_name, node) if new_name && old_name
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def method_removal_call?(node)
|
|
607
|
+
METHOD_REMOVAL_NAMES.include?(node.name) &&
|
|
608
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
609
|
+
node.block.nil? &&
|
|
610
|
+
node.arguments
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def remove_method_declarations(names)
|
|
614
|
+
return if names.empty?
|
|
615
|
+
|
|
616
|
+
transform_method_declarations do |declaration|
|
|
617
|
+
if removable_method_declaration?(declaration)
|
|
618
|
+
declaration_after_method_removal(declaration, names)
|
|
619
|
+
else
|
|
620
|
+
declaration
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def removable_method_declaration?(declaration)
|
|
626
|
+
declaration.namespace == current_namespace &&
|
|
627
|
+
declaration_scope_matches?(declaration, current_singleton_receiver) &&
|
|
628
|
+
(declaration.kind == current_method_kind || attribute_in_scope?(declaration, current_attribute_scope))
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def declaration_after_method_removal(declaration, names)
|
|
632
|
+
unless declaration.kind.to_s.include?("attribute")
|
|
633
|
+
return if names.include?(declaration.name)
|
|
634
|
+
|
|
635
|
+
return declaration
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
remaining = attribute_method_names(declaration) - names
|
|
639
|
+
return if remaining.empty?
|
|
640
|
+
return declaration unless declaration.kind.to_s.end_with?("_attribute_accessor")
|
|
641
|
+
|
|
642
|
+
attribute_scope = declaration.kind.to_s.delete_suffix("accessor")
|
|
643
|
+
kind = if remaining == [declaration.name]
|
|
644
|
+
:"#{attribute_scope}reader"
|
|
645
|
+
elsif remaining == ["#{declaration.name}="]
|
|
646
|
+
:"#{attribute_scope}writer"
|
|
647
|
+
else
|
|
648
|
+
declaration.kind
|
|
649
|
+
end
|
|
650
|
+
declaration_with_kind_and_visibility(declaration, kind, declaration.visibility)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def replace_method_declarations(kind:, names:, namespace:, receiver:, refinement:)
|
|
654
|
+
attribute_scope = (kind == :singleton_method) ? "singleton_attribute_" : "instance_attribute_"
|
|
655
|
+
transform_method_declarations do |declaration|
|
|
656
|
+
matches = declaration.namespace == namespace &&
|
|
657
|
+
declaration_scope_matches_values?(declaration, receiver, refinement) &&
|
|
658
|
+
(declaration.kind == kind || attribute_in_scope?(declaration, attribute_scope))
|
|
659
|
+
matches ? declaration_after_method_removal(declaration, names) : declaration
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def transform_method_declarations
|
|
664
|
+
container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
|
|
665
|
+
@declarations = @declarations.filter_map { |declaration| yield declaration }
|
|
666
|
+
@container_declaration_index = @declarations.index(container_declaration) if container_declaration
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def inline_alias_method_name(node)
|
|
670
|
+
return unless node.is_a?(Prism::CallNode) && node.name == :alias_method
|
|
671
|
+
return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
672
|
+
|
|
673
|
+
arguments = node.arguments&.arguments || []
|
|
674
|
+
attribute_name(arguments.first) if arguments.length == 2
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def collect_method_alias(name, target, node)
|
|
678
|
+
kind = current_method_kind
|
|
679
|
+
original = alias_target_declaration(kind, target)
|
|
680
|
+
unless original
|
|
681
|
+
@diagnostics << Model::V1::Diagnostic.new(
|
|
682
|
+
code: "alias.unresolved-target",
|
|
683
|
+
severity: :warning,
|
|
684
|
+
message: "The alias target is not declared in this source scope.",
|
|
685
|
+
span: span(node.location)
|
|
686
|
+
)
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
replace_method_declarations(
|
|
690
|
+
kind: kind,
|
|
691
|
+
names: [name],
|
|
692
|
+
namespace: current_namespace,
|
|
693
|
+
receiver: current_singleton_receiver,
|
|
694
|
+
refinement: @refinement
|
|
695
|
+
)
|
|
696
|
+
comments, parsed = parse_comments(node)
|
|
697
|
+
append_declaration(
|
|
698
|
+
kind: kind,
|
|
699
|
+
name: name,
|
|
700
|
+
namespace: current_namespace,
|
|
701
|
+
visibility: original&.visibility || current_visibility,
|
|
702
|
+
parameters: alias_target_parameters(original, target),
|
|
703
|
+
receiver: current_singleton_receiver,
|
|
704
|
+
receiver_span: current_singleton_receiver_span,
|
|
705
|
+
refinement: @refinement,
|
|
706
|
+
refinement_span: @refinement_span,
|
|
707
|
+
alias_target: target,
|
|
708
|
+
node: node,
|
|
709
|
+
comments: comments,
|
|
710
|
+
parsed: parsed
|
|
711
|
+
)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def alias_target_declaration(kind, target)
|
|
715
|
+
attribute_scope = (kind == :singleton_method) ? "singleton_attribute_" : "instance_attribute_"
|
|
716
|
+
@declarations.reverse_each.find do |declaration|
|
|
717
|
+
next unless declaration.namespace == current_namespace
|
|
718
|
+
next unless declaration_scope_matches?(declaration, current_singleton_receiver)
|
|
719
|
+
|
|
720
|
+
(declaration.kind == kind && declaration.name == target) ||
|
|
721
|
+
(attribute_in_scope?(declaration, attribute_scope) && attribute_method_names(declaration).include?(target))
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def alias_target_parameters(declaration, target)
|
|
726
|
+
return [].freeze unless declaration
|
|
727
|
+
return declaration.parameters unless declaration.kind.to_s.include?("attribute")
|
|
728
|
+
|
|
729
|
+
target.end_with?("=") ? ["value"].freeze : [].freeze
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def collect_method_declarations(node)
|
|
733
|
+
instance_evaluation_method = instance_evaluation_method?(node)
|
|
734
|
+
kind = (singleton_method?(node) || instance_evaluation_method) ? :singleton_method : :instance_method
|
|
735
|
+
name = node.name.to_s
|
|
736
|
+
namespace = current_namespace
|
|
737
|
+
parameters = parameter_names(node.parameters).freeze
|
|
738
|
+
receiver = node.receiver&.location&.slice || current_singleton_receiver || @instance_evaluation_receiver
|
|
739
|
+
receiver_span = if node.receiver
|
|
740
|
+
span(node.receiver.location)
|
|
741
|
+
else
|
|
742
|
+
current_singleton_receiver_span || @instance_evaluation_receiver_span
|
|
743
|
+
end
|
|
744
|
+
collect_method_entries(
|
|
745
|
+
kind: kind,
|
|
746
|
+
name: name,
|
|
747
|
+
namespace: namespace,
|
|
748
|
+
visibility: instance_evaluation_method ? :public : method_visibility(node),
|
|
749
|
+
parameters: parameters,
|
|
750
|
+
receiver: receiver,
|
|
751
|
+
receiver_span: receiver_span,
|
|
752
|
+
node: node
|
|
753
|
+
)
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def instance_evaluation_method?(node)
|
|
757
|
+
@instance_evaluation_receiver && @singleton_depth.zero? && node.receiver.nil?
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def collect_method_entries(kind:, name:, namespace:, visibility:, parameters:, receiver:, receiver_span:, node:)
|
|
761
|
+
module_function = kind == :instance_method && module_function_definition?(node)
|
|
762
|
+
replace_method_declarations(
|
|
763
|
+
kind: kind,
|
|
764
|
+
names: [name],
|
|
765
|
+
namespace: namespace,
|
|
766
|
+
receiver: receiver,
|
|
767
|
+
refinement: @refinement
|
|
768
|
+
)
|
|
769
|
+
unless module_function
|
|
770
|
+
return collect_declaration(
|
|
771
|
+
kind: kind,
|
|
772
|
+
name: name,
|
|
773
|
+
namespace: namespace,
|
|
774
|
+
visibility: visibility,
|
|
775
|
+
parameters: parameters,
|
|
776
|
+
receiver: receiver,
|
|
777
|
+
receiver_span: receiver_span,
|
|
778
|
+
refinement: @refinement,
|
|
779
|
+
refinement_span: @refinement_span,
|
|
780
|
+
node: node
|
|
781
|
+
)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
replace_method_declarations(
|
|
785
|
+
kind: :singleton_method,
|
|
786
|
+
names: [name],
|
|
787
|
+
namespace: namespace,
|
|
788
|
+
receiver: receiver,
|
|
789
|
+
refinement: @refinement
|
|
790
|
+
)
|
|
791
|
+
comments, parsed = parse_comments(node)
|
|
792
|
+
append_declaration(
|
|
793
|
+
kind: :instance_method,
|
|
794
|
+
name: name,
|
|
795
|
+
namespace: namespace,
|
|
796
|
+
visibility: :private,
|
|
797
|
+
parameters: parameters,
|
|
798
|
+
receiver: receiver,
|
|
799
|
+
receiver_span: receiver_span,
|
|
800
|
+
refinement: @refinement,
|
|
801
|
+
refinement_span: @refinement_span,
|
|
802
|
+
node: node,
|
|
803
|
+
comments: comments,
|
|
804
|
+
parsed: parsed
|
|
805
|
+
)
|
|
806
|
+
append_declaration(
|
|
807
|
+
kind: :singleton_method,
|
|
808
|
+
name: name,
|
|
809
|
+
namespace: namespace,
|
|
810
|
+
visibility: :public,
|
|
811
|
+
parameters: parameters,
|
|
812
|
+
receiver: receiver,
|
|
813
|
+
receiver_span: receiver_span,
|
|
814
|
+
refinement: @refinement,
|
|
815
|
+
refinement_span: @refinement_span,
|
|
816
|
+
node: node,
|
|
817
|
+
comments: comments,
|
|
818
|
+
parsed: parsed
|
|
819
|
+
)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def collect_attribute_declarations(node)
|
|
823
|
+
attribute_kind = attribute_kind(node)
|
|
824
|
+
current_owner = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
825
|
+
return unless attribute_kind && current_owner && !@namespace.empty?
|
|
826
|
+
|
|
827
|
+
arguments = node.arguments&.arguments || []
|
|
828
|
+
names = arguments.filter_map { |argument| attribute_name(argument) }
|
|
829
|
+
return if names.empty?
|
|
830
|
+
|
|
831
|
+
comments, parsed = parse_comments(node)
|
|
832
|
+
scope = @singleton_depth.positive? ? :singleton : :instance
|
|
833
|
+
kind = :"#{scope}_#{attribute_kind}"
|
|
834
|
+
names.each do |name|
|
|
835
|
+
replace_method_declarations(
|
|
836
|
+
kind: :"#{scope}_method",
|
|
837
|
+
names: attribute_generated_method_names(name, attribute_kind),
|
|
838
|
+
namespace: current_namespace,
|
|
839
|
+
receiver: current_singleton_receiver,
|
|
840
|
+
refinement: @refinement
|
|
841
|
+
)
|
|
842
|
+
append_declaration(
|
|
843
|
+
kind: kind,
|
|
844
|
+
name: name,
|
|
845
|
+
namespace: @namespace.join("::"),
|
|
846
|
+
visibility: current_visibility,
|
|
847
|
+
parameters: [].freeze,
|
|
848
|
+
receiver: current_singleton_receiver,
|
|
849
|
+
receiver_span: current_singleton_receiver_span,
|
|
850
|
+
refinement: @refinement,
|
|
851
|
+
refinement_span: @refinement_span,
|
|
852
|
+
node: node,
|
|
853
|
+
comments: comments,
|
|
854
|
+
parsed: parsed
|
|
855
|
+
)
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
def attribute_name(argument)
|
|
860
|
+
argument.unescaped if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def attribute_kind(node)
|
|
864
|
+
kind = ATTRIBUTE_KINDS[node.name]
|
|
865
|
+
arguments = node.arguments&.arguments || []
|
|
866
|
+
return :attribute_accessor if node.name == :attr && arguments.length == 2 && arguments.last.is_a?(Prism::TrueNode)
|
|
867
|
+
|
|
868
|
+
kind
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def attribute_generated_method_names(name, attribute_kind)
|
|
872
|
+
case attribute_kind
|
|
873
|
+
when :attribute_reader
|
|
874
|
+
[name]
|
|
875
|
+
when :attribute_writer
|
|
876
|
+
["#{name}="]
|
|
877
|
+
else
|
|
878
|
+
[name, "#{name}="]
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def collect_path_declaration(kind, path, node, visibility: constant_visibility_for(path), superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
|
|
883
|
+
parts = namespace_parts(path)
|
|
884
|
+
collect_declaration(
|
|
885
|
+
kind: kind,
|
|
886
|
+
name: parts.last,
|
|
887
|
+
namespace: (parts.length > 1) ? parts[0...-1].join("::") : nil,
|
|
888
|
+
visibility: visibility,
|
|
889
|
+
parameters: [].freeze,
|
|
890
|
+
superclass: superclass,
|
|
891
|
+
superclass_span: superclass_span,
|
|
892
|
+
container_builder: container_builder,
|
|
893
|
+
container_builder_span: container_builder_span,
|
|
894
|
+
node: node
|
|
895
|
+
)
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def collect_declaration(kind:, name:, namespace:, visibility:, parameters:, node:, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
|
|
899
|
+
comments, parsed = parse_comments(node)
|
|
900
|
+
append_declaration(
|
|
901
|
+
kind: kind,
|
|
902
|
+
name: name,
|
|
903
|
+
namespace: namespace,
|
|
904
|
+
visibility: visibility,
|
|
905
|
+
parameters: parameters,
|
|
906
|
+
receiver: receiver,
|
|
907
|
+
receiver_span: receiver_span,
|
|
908
|
+
refinement: refinement,
|
|
909
|
+
refinement_span: refinement_span,
|
|
910
|
+
superclass: superclass,
|
|
911
|
+
superclass_span: superclass_span,
|
|
912
|
+
container_builder: container_builder,
|
|
913
|
+
container_builder_span: container_builder_span,
|
|
914
|
+
node: node,
|
|
915
|
+
comments: comments,
|
|
916
|
+
parsed: parsed
|
|
917
|
+
)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def append_declaration(kind:, name:, namespace:, visibility:, parameters:, node:, comments:, parsed:, receiver: nil, receiver_span: nil, refinement: nil, refinement_span: nil, alias_target: nil, superclass: nil, superclass_span: nil, container_builder: nil, container_builder_span: nil)
|
|
921
|
+
@declarations << Model::V1::Declaration.new(
|
|
922
|
+
kind: kind,
|
|
923
|
+
name: name,
|
|
924
|
+
namespace: namespace,
|
|
925
|
+
visibility: visibility,
|
|
926
|
+
parameters: parameters,
|
|
927
|
+
receiver: receiver,
|
|
928
|
+
receiver_span: receiver_span,
|
|
929
|
+
refinement: refinement,
|
|
930
|
+
refinement_span: refinement_span,
|
|
931
|
+
alias_target: alias_target,
|
|
932
|
+
superclass: superclass,
|
|
933
|
+
superclass_span: superclass_span,
|
|
934
|
+
container_builder: container_builder,
|
|
935
|
+
container_builder_span: container_builder_span,
|
|
936
|
+
span: span(node.location),
|
|
937
|
+
comment_span: comment_span(comments),
|
|
938
|
+
documentation: parsed.fetch(:documentation).freeze,
|
|
939
|
+
contracts: parsed.fetch(:contracts).freeze
|
|
940
|
+
)
|
|
941
|
+
@declarations.length - 1
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def parse_comments(node)
|
|
945
|
+
comments = comment_block_for(node.location.start_line)
|
|
946
|
+
parsed = CommentBlockParser.new(@source, @path, comments, @diagnostics, @encoding).call
|
|
947
|
+
[comments, parsed]
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def within_namespace(name, kind, declaration_index)
|
|
951
|
+
previous_namespace = @namespace
|
|
952
|
+
previous_instance_visibility = @instance_visibility
|
|
953
|
+
previous_singleton_visibility = @singleton_visibility
|
|
954
|
+
previous_container_kind = @container_kind
|
|
955
|
+
previous_module_function_mode = @module_function_mode
|
|
956
|
+
previous_container_span = @declarations.fetch(@container_declaration_index).span if @container_declaration_index
|
|
957
|
+
previous_refinement = @refinement
|
|
958
|
+
previous_refinement_span = @refinement_span
|
|
959
|
+
previous_instance_evaluation_receiver = @instance_evaluation_receiver
|
|
960
|
+
previous_instance_evaluation_receiver_span = @instance_evaluation_receiver_span
|
|
961
|
+
@namespace = namespace_parts(name)
|
|
962
|
+
@instance_visibility = :public
|
|
963
|
+
@singleton_visibility = :public
|
|
964
|
+
@container_kind = kind
|
|
965
|
+
@module_function_mode = false
|
|
966
|
+
@container_declaration_index = declaration_index
|
|
967
|
+
@refinement = nil
|
|
968
|
+
@refinement_span = nil
|
|
969
|
+
@instance_evaluation_receiver = nil
|
|
970
|
+
@instance_evaluation_receiver_span = nil
|
|
971
|
+
yield
|
|
972
|
+
ensure
|
|
973
|
+
@namespace = previous_namespace
|
|
974
|
+
@instance_visibility = previous_instance_visibility
|
|
975
|
+
@singleton_visibility = previous_singleton_visibility
|
|
976
|
+
@container_kind = previous_container_kind
|
|
977
|
+
@module_function_mode = previous_module_function_mode
|
|
978
|
+
@container_declaration_index = @declarations.index { |declaration| declaration.span.equal?(previous_container_span) }
|
|
979
|
+
@refinement = previous_refinement
|
|
980
|
+
@refinement_span = previous_refinement_span
|
|
981
|
+
@instance_evaluation_receiver = previous_instance_evaluation_receiver
|
|
982
|
+
@instance_evaluation_receiver_span = previous_instance_evaluation_receiver_span
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def within_refinement(target, target_span)
|
|
986
|
+
previous_instance_visibility = @instance_visibility
|
|
987
|
+
previous_singleton_visibility = @singleton_visibility
|
|
988
|
+
previous_container_kind = @container_kind
|
|
989
|
+
previous_module_function_mode = @module_function_mode
|
|
990
|
+
previous_container_span = @declarations.fetch(@container_declaration_index).span if @container_declaration_index
|
|
991
|
+
previous_refinement = @refinement
|
|
992
|
+
previous_refinement_span = @refinement_span
|
|
993
|
+
previous_instance_evaluation_receiver = @instance_evaluation_receiver
|
|
994
|
+
previous_instance_evaluation_receiver_span = @instance_evaluation_receiver_span
|
|
995
|
+
@instance_visibility = :public
|
|
996
|
+
@singleton_visibility = :public
|
|
997
|
+
@container_kind = :refinement
|
|
998
|
+
@module_function_mode = false
|
|
999
|
+
@container_declaration_index = nil
|
|
1000
|
+
@refinement = target
|
|
1001
|
+
@refinement_span = target_span
|
|
1002
|
+
@instance_evaluation_receiver = nil
|
|
1003
|
+
@instance_evaluation_receiver_span = nil
|
|
1004
|
+
yield
|
|
1005
|
+
ensure
|
|
1006
|
+
@instance_visibility = previous_instance_visibility
|
|
1007
|
+
@singleton_visibility = previous_singleton_visibility
|
|
1008
|
+
@container_kind = previous_container_kind
|
|
1009
|
+
@module_function_mode = previous_module_function_mode
|
|
1010
|
+
@container_declaration_index = @declarations.index { |declaration| declaration.span.equal?(previous_container_span) }
|
|
1011
|
+
@refinement = previous_refinement
|
|
1012
|
+
@refinement_span = previous_refinement_span
|
|
1013
|
+
@instance_evaluation_receiver = previous_instance_evaluation_receiver
|
|
1014
|
+
@instance_evaluation_receiver_span = previous_instance_evaluation_receiver_span
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def namespace_parts(name)
|
|
1018
|
+
prefix = name.start_with?("::") ? [] : @namespace
|
|
1019
|
+
prefix + name.sub(/\A::/, "").split("::")
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def singleton_method?(node)
|
|
1023
|
+
!node.receiver.nil? || @singleton_depth.positive?
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def method_visibility(node)
|
|
1027
|
+
return :public if node.receiver && @singleton_depth.zero?
|
|
1028
|
+
|
|
1029
|
+
current_visibility
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def current_visibility
|
|
1033
|
+
@singleton_depth.positive? ? @singleton_visibility : @instance_visibility
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def visit_visibility_call(node, visibility)
|
|
1037
|
+
arguments = node.arguments&.arguments
|
|
1038
|
+
if arguments.nil? || arguments.empty?
|
|
1039
|
+
@module_function_mode = false if module_function_context?
|
|
1040
|
+
set_current_visibility(visibility)
|
|
1041
|
+
return yield
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
if arguments.any? { |argument| visibility_declaration?(argument) }
|
|
1045
|
+
return with_visibility(visibility) { yield }
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
names = arguments.filter_map { |argument| attribute_name(argument) || inline_alias_method_name(argument) }
|
|
1049
|
+
yield
|
|
1050
|
+
unless names.empty?
|
|
1051
|
+
apply_named_visibility(
|
|
1052
|
+
names,
|
|
1053
|
+
visibility,
|
|
1054
|
+
node,
|
|
1055
|
+
method_kind: current_method_kind,
|
|
1056
|
+
attribute_scope: current_attribute_scope,
|
|
1057
|
+
receiver: current_singleton_receiver
|
|
1058
|
+
)
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
def visit_module_function_call(node)
|
|
1063
|
+
arguments = node.arguments&.arguments
|
|
1064
|
+
if arguments.nil? || arguments.empty?
|
|
1065
|
+
@instance_visibility = :private
|
|
1066
|
+
@module_function_mode = true
|
|
1067
|
+
return yield
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
if arguments.any? { |argument| module_function_declaration?(argument) }
|
|
1071
|
+
return with_module_function_mode { yield }
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
yield
|
|
1075
|
+
names = arguments.flat_map { |argument| module_function_argument_names(argument) }
|
|
1076
|
+
names.each { |name| apply_named_module_function(name) }
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
def module_function_argument_names(argument)
|
|
1080
|
+
name = attribute_name(argument) || inline_alias_method_name(argument)
|
|
1081
|
+
return [name] if name
|
|
1082
|
+
return [] unless argument.is_a?(Prism::SplatNode)
|
|
1083
|
+
|
|
1084
|
+
call = argument.expression
|
|
1085
|
+
return [] unless call.is_a?(Prism::CallNode) && ATTRIBUTE_KINDS.key?(call.name)
|
|
1086
|
+
return [] unless call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode)
|
|
1087
|
+
|
|
1088
|
+
kind = attribute_kind(call)
|
|
1089
|
+
(call.arguments&.arguments || []).filter_map { |item| attribute_name(item) }
|
|
1090
|
+
.flat_map { |item| attribute_generated_method_names(item, kind) }
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def module_function_context?
|
|
1094
|
+
@container_kind == :module && @singleton_depth.zero?
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
def module_function_definition?(node)
|
|
1098
|
+
@module_function_mode &&
|
|
1099
|
+
module_function_context? &&
|
|
1100
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
def module_function_declaration?(node)
|
|
1104
|
+
node.is_a?(Prism::DefNode) ||
|
|
1105
|
+
(node.is_a?(Prism::CallNode) && node.name == :define_method &&
|
|
1106
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)))
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def with_module_function_mode
|
|
1110
|
+
previous_visibility = @instance_visibility
|
|
1111
|
+
previous_mode = @module_function_mode
|
|
1112
|
+
@instance_visibility = :private
|
|
1113
|
+
@module_function_mode = true
|
|
1114
|
+
yield
|
|
1115
|
+
ensure
|
|
1116
|
+
@instance_visibility = previous_visibility
|
|
1117
|
+
@module_function_mode = previous_mode
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def apply_named_module_function(name)
|
|
1121
|
+
declaration = alias_target_declaration(:instance_method, name)
|
|
1122
|
+
return unless declaration
|
|
1123
|
+
|
|
1124
|
+
apply_module_function_instance_visibility(declaration, name)
|
|
1125
|
+
replace_method_declarations(
|
|
1126
|
+
kind: :singleton_method,
|
|
1127
|
+
names: [name],
|
|
1128
|
+
namespace: declaration.namespace,
|
|
1129
|
+
receiver: declaration.receiver,
|
|
1130
|
+
refinement: declaration.refinement
|
|
1131
|
+
)
|
|
1132
|
+
@declarations << declaration_with_kind_and_visibility(
|
|
1133
|
+
declaration,
|
|
1134
|
+
:singleton_method,
|
|
1135
|
+
:public,
|
|
1136
|
+
name: name,
|
|
1137
|
+
parameters: alias_target_parameters(declaration, name)
|
|
1138
|
+
)
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def apply_module_function_instance_visibility(declaration, name)
|
|
1142
|
+
index = @declarations.index(declaration)
|
|
1143
|
+
return unless index
|
|
1144
|
+
|
|
1145
|
+
replacements = if declaration.kind.to_s.end_with?("_attribute_accessor")
|
|
1146
|
+
target_kind, retained_kind = if name.end_with?("=")
|
|
1147
|
+
%i[instance_attribute_writer instance_attribute_reader]
|
|
1148
|
+
else
|
|
1149
|
+
%i[instance_attribute_reader instance_attribute_writer]
|
|
1150
|
+
end
|
|
1151
|
+
[
|
|
1152
|
+
declaration_with_kind_and_visibility(declaration, target_kind, :private),
|
|
1153
|
+
declaration_with_kind_and_visibility(declaration, retained_kind, declaration.visibility)
|
|
1154
|
+
]
|
|
1155
|
+
else
|
|
1156
|
+
[declaration_with_visibility(declaration, :private)]
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
container_declaration = @declarations[@container_declaration_index] if @container_declaration_index
|
|
1160
|
+
@declarations[index, 1] = replacements
|
|
1161
|
+
@container_declaration_index = @declarations.index(container_declaration) if container_declaration
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def visit_class_method_visibility_call(node, visibility)
|
|
1165
|
+
arguments = node.arguments&.arguments || []
|
|
1166
|
+
yield
|
|
1167
|
+
names = arguments.filter_map do |argument|
|
|
1168
|
+
if argument.is_a?(Prism::DefNode) && argument.receiver
|
|
1169
|
+
argument.name.to_s
|
|
1170
|
+
elsif argument.is_a?(Prism::CallNode) && argument.name == :define_singleton_method &&
|
|
1171
|
+
(argument.receiver.nil? || argument.receiver.is_a?(Prism::SelfNode))
|
|
1172
|
+
attribute_name(argument.arguments&.arguments&.first)
|
|
1173
|
+
else
|
|
1174
|
+
attribute_name(argument)
|
|
1175
|
+
end
|
|
1176
|
+
end
|
|
1177
|
+
unless names.empty?
|
|
1178
|
+
apply_named_visibility(
|
|
1179
|
+
names,
|
|
1180
|
+
visibility,
|
|
1181
|
+
node,
|
|
1182
|
+
method_kind: :singleton_method,
|
|
1183
|
+
attribute_scope: "singleton_attribute_",
|
|
1184
|
+
receiver: nil
|
|
1185
|
+
)
|
|
1186
|
+
end
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def visit_constant_visibility_call(node, visibility)
|
|
1190
|
+
arguments = node.arguments&.arguments || []
|
|
1191
|
+
yield
|
|
1192
|
+
names = arguments.filter_map { |argument| attribute_name(argument) }
|
|
1193
|
+
return if names.empty?
|
|
1194
|
+
|
|
1195
|
+
@declarations.map! do |declaration|
|
|
1196
|
+
if %i[class module constant].include?(declaration.kind) &&
|
|
1197
|
+
declaration.namespace == current_namespace &&
|
|
1198
|
+
names.include?(declaration.name)
|
|
1199
|
+
declaration_with_visibility(declaration, visibility)
|
|
1200
|
+
else
|
|
1201
|
+
declaration
|
|
1202
|
+
end
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
def visibility_declaration?(node)
|
|
1207
|
+
node.is_a?(Prism::DefNode) ||
|
|
1208
|
+
(node.is_a?(Prism::CallNode) && (ATTRIBUTE_KINDS.key?(node.name) ||
|
|
1209
|
+
(node.name == :define_method &&
|
|
1210
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)))))
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def set_current_visibility(visibility)
|
|
1214
|
+
if @singleton_depth.positive?
|
|
1215
|
+
@singleton_visibility = visibility
|
|
1216
|
+
else
|
|
1217
|
+
@instance_visibility = visibility
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
def with_visibility(visibility)
|
|
1222
|
+
previous_visibility = current_visibility
|
|
1223
|
+
set_current_visibility(visibility)
|
|
1224
|
+
yield
|
|
1225
|
+
ensure
|
|
1226
|
+
set_current_visibility(previous_visibility)
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def apply_named_visibility(names, visibility, node, method_kind:, attribute_scope:, receiver:)
|
|
1230
|
+
attribute_match = false
|
|
1231
|
+
@declarations.map! do |declaration|
|
|
1232
|
+
next declaration unless declaration.namespace == current_namespace
|
|
1233
|
+
next declaration unless declaration_scope_matches?(declaration, receiver)
|
|
1234
|
+
|
|
1235
|
+
if declaration.kind == method_kind && names.include?(declaration.name)
|
|
1236
|
+
declaration_with_visibility(declaration, visibility)
|
|
1237
|
+
elsif attribute_in_scope?(declaration, attribute_scope) && (attribute_method_names(declaration) & names).any?
|
|
1238
|
+
attribute_match = true
|
|
1239
|
+
(visibility == :public) ? declaration : declaration_with_visibility(declaration, visibility)
|
|
1240
|
+
else
|
|
1241
|
+
declaration
|
|
1242
|
+
end
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
return unless attribute_match
|
|
1246
|
+
|
|
1247
|
+
@diagnostics << Model::V1::Diagnostic.new(
|
|
1248
|
+
code: "visibility.named-attribute",
|
|
1249
|
+
severity: :warning,
|
|
1250
|
+
message: "Use a lexical or inline visibility modifier for an attribute declaration.",
|
|
1251
|
+
span: span(node.location)
|
|
1252
|
+
)
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
def declaration_scope_matches?(declaration, receiver)
|
|
1256
|
+
declaration_scope_matches_values?(declaration, receiver, @refinement)
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
def declaration_scope_matches_values?(declaration, receiver, refinement)
|
|
1260
|
+
receiver_matches = if receiver.nil? || receiver == "self"
|
|
1261
|
+
declaration.receiver.nil? || declaration.receiver == "self"
|
|
1262
|
+
else
|
|
1263
|
+
declaration.receiver == receiver
|
|
1264
|
+
end
|
|
1265
|
+
receiver_matches && declaration.refinement == refinement
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
def current_namespace
|
|
1269
|
+
@namespace.empty? ? nil : @namespace.join("::")
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
def current_method_kind
|
|
1273
|
+
@singleton_depth.positive? ? :singleton_method : :instance_method
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
def current_singleton_receiver
|
|
1277
|
+
@singleton_receiver if @singleton_depth.positive?
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def current_singleton_receiver_span
|
|
1281
|
+
@singleton_receiver_span if @singleton_depth.positive?
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def current_attribute_scope
|
|
1285
|
+
@singleton_depth.positive? ? "singleton_attribute_" : "instance_attribute_"
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def attribute_in_scope?(declaration, scope)
|
|
1289
|
+
declaration.kind.to_s.start_with?(scope)
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def attribute_method_names(declaration)
|
|
1293
|
+
case declaration.kind.to_s
|
|
1294
|
+
when /_attribute_reader\z/
|
|
1295
|
+
[declaration.name]
|
|
1296
|
+
when /_attribute_writer\z/
|
|
1297
|
+
["#{declaration.name}="]
|
|
1298
|
+
else
|
|
1299
|
+
[declaration.name, "#{declaration.name}="]
|
|
1300
|
+
end
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
def declaration_with_visibility(declaration, visibility)
|
|
1304
|
+
declaration_with_kind_and_visibility(declaration, declaration.kind, visibility)
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def declaration_with_mixins(declaration, mixins)
|
|
1308
|
+
declaration_with_kind_and_visibility(declaration, declaration.kind, declaration.visibility, mixins: mixins.freeze)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
def declaration_with_kind_and_visibility(declaration, kind, visibility, name: declaration.name, parameters: declaration.parameters, mixins: declaration.mixins)
|
|
1312
|
+
Model::V1::Declaration.new(
|
|
1313
|
+
kind: kind,
|
|
1314
|
+
name: name,
|
|
1315
|
+
namespace: declaration.namespace,
|
|
1316
|
+
visibility: visibility,
|
|
1317
|
+
parameters: parameters,
|
|
1318
|
+
receiver: declaration.receiver,
|
|
1319
|
+
receiver_span: declaration.receiver_span,
|
|
1320
|
+
refinement: declaration.refinement,
|
|
1321
|
+
refinement_span: declaration.refinement_span,
|
|
1322
|
+
alias_target: declaration.alias_target,
|
|
1323
|
+
superclass: declaration.superclass,
|
|
1324
|
+
superclass_span: declaration.superclass_span,
|
|
1325
|
+
container_builder: declaration.container_builder,
|
|
1326
|
+
container_builder_span: declaration.container_builder_span,
|
|
1327
|
+
mixins: mixins,
|
|
1328
|
+
span: declaration.span,
|
|
1329
|
+
comment_span: declaration.comment_span,
|
|
1330
|
+
documentation: declaration.documentation,
|
|
1331
|
+
contracts: declaration.contracts
|
|
1332
|
+
)
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def parameter_names(parameters)
|
|
1336
|
+
return [] unless parameters
|
|
1337
|
+
|
|
1338
|
+
names = []
|
|
1339
|
+
names.concat(parameters.requireds.map { |parameter| parameter_name(parameter) })
|
|
1340
|
+
names.concat(parameters.optionals.map { |parameter| parameter_name(parameter) })
|
|
1341
|
+
names << prefixed_name("*", parameters.rest) if parameters.rest
|
|
1342
|
+
names.concat(parameters.posts.map { |parameter| parameter_name(parameter) })
|
|
1343
|
+
names.concat(parameters.keywords.map { |parameter| "#{parameter.name}:" })
|
|
1344
|
+
names << prefixed_name("**", parameters.keyword_rest) if parameters.keyword_rest
|
|
1345
|
+
names << prefixed_name("&", parameters.block) if parameters.block
|
|
1346
|
+
names
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
def parameter_name(parameter)
|
|
1350
|
+
return parameter.location.slice unless parameter.respond_to?(:name)
|
|
1351
|
+
|
|
1352
|
+
parameter.name.to_s
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
def prefixed_name(prefix, parameter)
|
|
1356
|
+
return parameter.location.slice unless parameter.respond_to?(:name)
|
|
1357
|
+
|
|
1358
|
+
parameter.name ? "#{prefix}#{parameter.name}" : prefix
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def standalone?(comment)
|
|
1362
|
+
offset = comment.location.start_offset
|
|
1363
|
+
line_start = offset.zero? ? 0 : (@source.b.rindex("\n", offset - 1) || -1) + 1
|
|
1364
|
+
prefix = @source.byteslice(line_start, offset - line_start)
|
|
1365
|
+
prefix.match?(/\A[\t ]*\z/)
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
def magic_comment?(comment)
|
|
1369
|
+
return false if comment.location.start_line > 2
|
|
1370
|
+
|
|
1371
|
+
comment.location.slice.match?(/\A#\s*(?:en)?coding\s*[:=]|\A#\s*frozen_string_literal\s*:/i)
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
def comment_block_for(declaration_line)
|
|
1375
|
+
expected_line = declaration_line - 1
|
|
1376
|
+
block = []
|
|
1377
|
+
|
|
1378
|
+
@comments.reverse_each do |comment|
|
|
1379
|
+
next if comment.location.end_line > expected_line
|
|
1380
|
+
break if comment.location.end_line < expected_line
|
|
1381
|
+
|
|
1382
|
+
block << comment
|
|
1383
|
+
expected_line = comment.location.start_line - 1
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
block.reverse
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
def comment_span(comments)
|
|
1390
|
+
return nil if comments.empty?
|
|
1391
|
+
|
|
1392
|
+
first = comments.first.location
|
|
1393
|
+
last = comments.last.location
|
|
1394
|
+
Model::V1::SourceSpan.new(
|
|
1395
|
+
path: @path,
|
|
1396
|
+
start_line: first.start_line,
|
|
1397
|
+
start_column: first.start_column,
|
|
1398
|
+
end_line: last.end_line,
|
|
1399
|
+
end_column: last.end_column,
|
|
1400
|
+
start_offset: first.start_offset,
|
|
1401
|
+
end_offset: last.end_offset
|
|
1402
|
+
)
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
def span(location)
|
|
1406
|
+
Model::V1::SourceSpan.new(
|
|
1407
|
+
path: @path,
|
|
1408
|
+
start_line: location.start_line,
|
|
1409
|
+
start_column: location.start_column,
|
|
1410
|
+
end_line: location.end_line,
|
|
1411
|
+
end_column: location.end_column,
|
|
1412
|
+
start_offset: location.start_offset,
|
|
1413
|
+
end_offset: location.end_offset
|
|
1414
|
+
)
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
class CommentBlockParser
|
|
1419
|
+
def initialize(source, path, comments, diagnostics, encoding)
|
|
1420
|
+
@source = source
|
|
1421
|
+
@path = path
|
|
1422
|
+
@comments = comments
|
|
1423
|
+
@diagnostics = diagnostics
|
|
1424
|
+
@encoding = encoding
|
|
1425
|
+
end
|
|
1426
|
+
|
|
1427
|
+
def call
|
|
1428
|
+
documentation = []
|
|
1429
|
+
contracts = []
|
|
1430
|
+
seen_non_extrbs_annotation = false
|
|
1431
|
+
pending = nil
|
|
1432
|
+
|
|
1433
|
+
@comments.each do |comment|
|
|
1434
|
+
raw = comment.location.slice
|
|
1435
|
+
body = comment_body(raw)
|
|
1436
|
+
|
|
1437
|
+
if annotation_boundary?(body, raw)
|
|
1438
|
+
flush_pending(documentation, pending)
|
|
1439
|
+
pending = nil
|
|
1440
|
+
elsif pending
|
|
1441
|
+
append_continuation(pending, body, comment)
|
|
1442
|
+
next
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
if (tag = noncanonical_known_tag(body, raw))
|
|
1446
|
+
code = (tag == "@extrbs") ? "extrbs.noncanonical-spacing" : "documentation.noncanonical-spacing"
|
|
1447
|
+
message = (tag == "@extrbs") ? "Write @extrbs as '# @extrbs' with one space." : "Write ZARD annotations immediately after '# '."
|
|
1448
|
+
add_diagnostic(code, :warning, message, comment.location)
|
|
1449
|
+
pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
|
|
1450
|
+
seen_non_extrbs_annotation = true
|
|
1451
|
+
next
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
if non_utf8_documentation?(body, raw)
|
|
1455
|
+
add_diagnostic(
|
|
1456
|
+
"documentation.non-utf8",
|
|
1457
|
+
:error,
|
|
1458
|
+
"ZARD documentation requires UTF-8 source text.",
|
|
1459
|
+
comment.location
|
|
1460
|
+
)
|
|
1461
|
+
pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
|
|
1462
|
+
seen_non_extrbs_annotation = true
|
|
1463
|
+
next
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
if (payload = contract_payload(body, "@extrbs"))
|
|
1467
|
+
if seen_non_extrbs_annotation
|
|
1468
|
+
add_diagnostic(
|
|
1469
|
+
"extrbs.not-first",
|
|
1470
|
+
:warning,
|
|
1471
|
+
"Place @extrbs before the other annotations in this comment block.",
|
|
1472
|
+
comment.location
|
|
1473
|
+
)
|
|
1474
|
+
end
|
|
1475
|
+
contracts << contract(:extrbs, payload, raw, comment.location)
|
|
1476
|
+
elsif (payload = contract_payload(body, "@rbs"))
|
|
1477
|
+
contracts << contract(:rbs, payload, raw, comment.location)
|
|
1478
|
+
seen_non_extrbs_annotation = true
|
|
1479
|
+
elsif raw.start_with?("#:")
|
|
1480
|
+
contracts << contract(:rbs, raw.delete_prefix("#:").strip, raw, comment.location)
|
|
1481
|
+
seen_non_extrbs_annotation = true
|
|
1482
|
+
elsif body.match?(/\A@param(?:\s|\z)/)
|
|
1483
|
+
pending = pending_documentation(parse_named_tag(body, raw, comment.location, :param, "@param"), comment)
|
|
1484
|
+
seen_non_extrbs_annotation = true
|
|
1485
|
+
elsif body.match?(/\A@yieldparam(?:\s|\z)/)
|
|
1486
|
+
pending = pending_documentation(parse_named_tag(body, raw, comment.location, :yieldparam, "@yieldparam"), comment)
|
|
1487
|
+
seen_non_extrbs_annotation = true
|
|
1488
|
+
elsif body.match?(/\A@option(?:\s|\z)/)
|
|
1489
|
+
pending = pending_documentation(parse_option(body, raw, comment.location), comment)
|
|
1490
|
+
seen_non_extrbs_annotation = true
|
|
1491
|
+
elsif body.match?(/\A@raise(?:\s|\z)/)
|
|
1492
|
+
pending = pending_documentation(parse_named_tag(body, raw, comment.location, :raise, "@raise"), comment)
|
|
1493
|
+
seen_non_extrbs_annotation = true
|
|
1494
|
+
elsif body.match?(/\A@return(?:\s|\z)/)
|
|
1495
|
+
pending = pending_documentation(parse_nameless_tag(body, raw, comment.location, :return, "@return"), comment)
|
|
1496
|
+
seen_non_extrbs_annotation = true
|
|
1497
|
+
elsif body.match?(/\A@yieldreturn(?:\s|\z)/)
|
|
1498
|
+
pending = pending_documentation(parse_nameless_tag(body, raw, comment.location, :yieldreturn, "@yieldreturn"), comment)
|
|
1499
|
+
seen_non_extrbs_annotation = true
|
|
1500
|
+
elsif body.match?(/\A@note(?:\s|\z)/)
|
|
1501
|
+
pending = pending_documentation(parse_description_tag(body, raw, comment.location, :note, "@note"), comment)
|
|
1502
|
+
seen_non_extrbs_annotation = true
|
|
1503
|
+
elsif body.match?(/\A@see(?:\s|\z)/)
|
|
1504
|
+
pending = pending_documentation(parse_description_tag(body, raw, comment.location, :see, "@see"), comment)
|
|
1505
|
+
seen_non_extrbs_annotation = true
|
|
1506
|
+
elsif body.match?(/\A@deprecated(?:\s|\z)/)
|
|
1507
|
+
pending = pending_documentation(parse_description_tag(body, raw, comment.location, :deprecated, "@deprecated"), comment)
|
|
1508
|
+
seen_non_extrbs_annotation = true
|
|
1509
|
+
elsif body.match?(/\A@example(?:\s|\z)/)
|
|
1510
|
+
pending = pending_documentation(parse_description_tag(body, raw, comment.location, :example, "@example"), comment)
|
|
1511
|
+
seen_non_extrbs_annotation = true
|
|
1512
|
+
elsif body.start_with?("@")
|
|
1513
|
+
pending = pending_documentation(raw_tag(body, raw, comment.location), comment)
|
|
1514
|
+
seen_non_extrbs_annotation = true
|
|
1515
|
+
elsif !body.empty?
|
|
1516
|
+
documentation << documentation_tag(:text, nil, nil, body, raw, comment.location)
|
|
1517
|
+
end
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
flush_pending(documentation, pending)
|
|
1521
|
+
|
|
1522
|
+
{documentation: documentation, contracts: contracts}
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
private
|
|
1526
|
+
|
|
1527
|
+
def comment_body(raw)
|
|
1528
|
+
raw.sub(/\A# ?/, "").delete_suffix("\r")
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
def annotation_boundary?(body, raw)
|
|
1532
|
+
raw.start_with?("#:", "# @") || !noncanonical_known_tag(body, raw).nil?
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
def noncanonical_known_tag(body, raw)
|
|
1536
|
+
return if raw.start_with?("# @")
|
|
1537
|
+
|
|
1538
|
+
candidate = body.lstrip[/\A@[^\s]*/]
|
|
1539
|
+
return unless %w[@extrbs @rbs @param @yieldparam @option @raise @return @yieldreturn @note @see @deprecated @example].include?(candidate)
|
|
1540
|
+
|
|
1541
|
+
candidate
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def pending_documentation(tag, comment)
|
|
1545
|
+
lines = tag.description.strip.empty? ? [] : [tag.description]
|
|
1546
|
+
{tag: tag, comments: [comment], lines: lines}
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
def append_continuation(pending, body, comment)
|
|
1550
|
+
pending.fetch(:lines) << (body.strip.empty? ? "" : body)
|
|
1551
|
+
pending.fetch(:comments) << comment
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
def flush_pending(documentation, pending)
|
|
1555
|
+
return unless pending
|
|
1556
|
+
|
|
1557
|
+
lines = pending.fetch(:lines)
|
|
1558
|
+
lines.shift while lines.first == ""
|
|
1559
|
+
lines.pop while lines.last == ""
|
|
1560
|
+
tag = pending.fetch(:tag)
|
|
1561
|
+
comments = pending.fetch(:comments)
|
|
1562
|
+
description = lines.join("\n")
|
|
1563
|
+
location = comments.first.location
|
|
1564
|
+
|
|
1565
|
+
if tag.name != :raw && description.empty?
|
|
1566
|
+
add_diagnostic(
|
|
1567
|
+
"documentation.empty-description",
|
|
1568
|
+
:warning,
|
|
1569
|
+
"Add a description to this ZARD documentation tag.",
|
|
1570
|
+
location
|
|
1571
|
+
)
|
|
1572
|
+
end
|
|
1573
|
+
|
|
1574
|
+
documentation << Model::V1::DocumentationTag.new(
|
|
1575
|
+
name: tag.name,
|
|
1576
|
+
owner: tag.owner,
|
|
1577
|
+
subject: tag.subject,
|
|
1578
|
+
claim: tag.claim,
|
|
1579
|
+
description: description,
|
|
1580
|
+
span: combined_span(comments),
|
|
1581
|
+
raw: combined_raw(comments)
|
|
1582
|
+
)
|
|
1583
|
+
end
|
|
1584
|
+
|
|
1585
|
+
def combined_span(comments)
|
|
1586
|
+
first = comments.first.location
|
|
1587
|
+
last = comments.last.location
|
|
1588
|
+
Model::V1::SourceSpan.new(
|
|
1589
|
+
path: @path,
|
|
1590
|
+
start_line: first.start_line,
|
|
1591
|
+
start_column: first.start_column,
|
|
1592
|
+
end_line: last.end_line,
|
|
1593
|
+
end_column: last.end_column,
|
|
1594
|
+
start_offset: first.start_offset,
|
|
1595
|
+
end_offset: last.end_offset
|
|
1596
|
+
)
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
def combined_raw(comments)
|
|
1600
|
+
first = comments.first.location.start_offset
|
|
1601
|
+
last = comments.last.location.end_offset
|
|
1602
|
+
@source.byteslice(first, last - first)
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def contract_payload(body, tag)
|
|
1606
|
+
match = body.match(/\A#{Regexp.escape(tag)}(?:\s+(.*)|\z)/)
|
|
1607
|
+
match && match[1].to_s
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
def non_utf8_documentation?(body, raw)
|
|
1611
|
+
return false if @encoding == Encoding::UTF_8
|
|
1612
|
+
return false if body.empty?
|
|
1613
|
+
return false if contract_payload(body, "@extrbs") || contract_payload(body, "@rbs")
|
|
1614
|
+
return false if raw.start_with?("#:")
|
|
1615
|
+
|
|
1616
|
+
true
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
def contract(channel, payload, raw, location)
|
|
1620
|
+
contract_payload, marker, note = payload.partition(" -- ")
|
|
1621
|
+
Model::V1::Contract.new(
|
|
1622
|
+
channel: channel,
|
|
1623
|
+
payload: contract_payload,
|
|
1624
|
+
note: marker.empty? ? nil : note,
|
|
1625
|
+
span: span(location),
|
|
1626
|
+
raw: raw
|
|
1627
|
+
)
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
def parse_named_tag(body, raw, location, name, prefix)
|
|
1631
|
+
rest = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
|
|
1632
|
+
head, description = split_named_description(rest)
|
|
1633
|
+
|
|
1634
|
+
unless head
|
|
1635
|
+
yard_like(raw, location)
|
|
1636
|
+
return raw_tag(body, raw, location)
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
subject, claim, valid = parse_named_head(head)
|
|
1640
|
+
unless valid
|
|
1641
|
+
yard_like(raw, location)
|
|
1642
|
+
return raw_tag(body, raw, location)
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
documentation_tag(name, subject, claim, description, raw, location)
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
def parse_option(body, raw, location)
|
|
1649
|
+
rest = body.delete_prefix("@option").sub(/\A[\t ]/, "")
|
|
1650
|
+
head, description = split_named_description(rest)
|
|
1651
|
+
unless head
|
|
1652
|
+
yard_like(raw, location)
|
|
1653
|
+
return raw_tag(body, raw, location)
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1656
|
+
owner, option_head = head.split(/\s+/, 2)
|
|
1657
|
+
subject, claim, valid = parse_named_head(option_head.to_s)
|
|
1658
|
+
|
|
1659
|
+
if !owner || !valid
|
|
1660
|
+
yard_like(raw, location)
|
|
1661
|
+
return raw_tag(body, raw, location)
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
documentation_tag(:option, subject, claim, description, raw, location, owner: owner)
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
def parse_nameless_tag(body, raw, location, name, prefix)
|
|
1668
|
+
rest = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
|
|
1669
|
+
|
|
1670
|
+
if rest == "—" || rest.start_with?("— ")
|
|
1671
|
+
add_diagnostic(
|
|
1672
|
+
"documentation.redundant-marker",
|
|
1673
|
+
:warning,
|
|
1674
|
+
"Remove the redundant em dash from #{prefix} without a type claim.",
|
|
1675
|
+
location
|
|
1676
|
+
)
|
|
1677
|
+
description = rest.delete_prefix("—").sub(/\A /, "")
|
|
1678
|
+
return documentation_tag(name, nil, nil, description, raw, location)
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
if rest.start_with?("[")
|
|
1682
|
+
claim, remainder = bracketed_claim(rest)
|
|
1683
|
+
valid_claim = claim && (remainder == " —" || remainder.start_with?(" — "))
|
|
1684
|
+
if !valid_claim
|
|
1685
|
+
yard_like(raw, location)
|
|
1686
|
+
return raw_tag(body, raw, location)
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
description = remainder.delete_prefix(" —").sub(/\A /, "")
|
|
1690
|
+
return documentation_tag(name, nil, claim, description, raw, location)
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
documentation_tag(name, nil, nil, rest, raw, location)
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
def parse_description_tag(body, raw, location, name, prefix)
|
|
1697
|
+
description = body.delete_prefix(prefix).sub(/\A[\t ]/, "")
|
|
1698
|
+
documentation_tag(name, nil, nil, description, raw, location)
|
|
1699
|
+
end
|
|
1700
|
+
|
|
1701
|
+
def split_named_description(rest)
|
|
1702
|
+
match = rest.match(/\A(.+?) —(?: (.*))?\z/)
|
|
1703
|
+
return [nil, nil] unless match
|
|
1704
|
+
|
|
1705
|
+
[match[1], match[2].to_s]
|
|
1706
|
+
end
|
|
1707
|
+
|
|
1708
|
+
def parse_named_head(head)
|
|
1709
|
+
subject, remainder = head.split(/\s+/, 2)
|
|
1710
|
+
return [nil, nil, false] unless subject
|
|
1711
|
+
return [subject, nil, true] unless remainder
|
|
1712
|
+
|
|
1713
|
+
claim, trailing = bracketed_claim(remainder)
|
|
1714
|
+
return [subject, claim, true] if claim && trailing.empty?
|
|
1715
|
+
|
|
1716
|
+
[subject, nil, false]
|
|
1717
|
+
end
|
|
1718
|
+
|
|
1719
|
+
def bracketed_claim(text)
|
|
1720
|
+
return [nil, text] unless text.start_with?("[")
|
|
1721
|
+
|
|
1722
|
+
depth = 0
|
|
1723
|
+
text.each_char.with_index do |character, index|
|
|
1724
|
+
depth += 1 if character == "["
|
|
1725
|
+
depth -= 1 if character == "]"
|
|
1726
|
+
next unless depth.zero?
|
|
1727
|
+
|
|
1728
|
+
return [text[1...index], text[(index + 1)..]]
|
|
1729
|
+
end
|
|
1730
|
+
|
|
1731
|
+
[nil, text]
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def yard_like(raw, location)
|
|
1735
|
+
add_diagnostic(
|
|
1736
|
+
"documentation.yard-like",
|
|
1737
|
+
:warning,
|
|
1738
|
+
"This tag is preserved as raw text because it does not use the ZARD em dash marker.",
|
|
1739
|
+
location
|
|
1740
|
+
)
|
|
1741
|
+
end
|
|
1742
|
+
|
|
1743
|
+
def raw_tag(body, raw, location)
|
|
1744
|
+
documentation_tag(:raw, nil, nil, body, raw, location)
|
|
1745
|
+
end
|
|
1746
|
+
|
|
1747
|
+
def documentation_tag(name, subject, claim, description, raw, location, owner: nil)
|
|
1748
|
+
Model::V1::DocumentationTag.new(
|
|
1749
|
+
name: name,
|
|
1750
|
+
owner: owner,
|
|
1751
|
+
subject: subject,
|
|
1752
|
+
claim: claim,
|
|
1753
|
+
description: description,
|
|
1754
|
+
span: span(location),
|
|
1755
|
+
raw: raw
|
|
1756
|
+
)
|
|
1757
|
+
end
|
|
1758
|
+
|
|
1759
|
+
def add_diagnostic(code, severity, message, location)
|
|
1760
|
+
@diagnostics << Model::V1::Diagnostic.new(
|
|
1761
|
+
code: code,
|
|
1762
|
+
severity: severity,
|
|
1763
|
+
message: message,
|
|
1764
|
+
span: span(location)
|
|
1765
|
+
)
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
def span(location)
|
|
1769
|
+
Model::V1::SourceSpan.new(
|
|
1770
|
+
path: @path,
|
|
1771
|
+
start_line: location.start_line,
|
|
1772
|
+
start_column: location.start_column,
|
|
1773
|
+
end_line: location.end_line,
|
|
1774
|
+
end_column: location.end_column,
|
|
1775
|
+
start_offset: location.start_offset,
|
|
1776
|
+
end_offset: location.end_offset
|
|
1777
|
+
)
|
|
1778
|
+
end
|
|
1779
|
+
end
|
|
1780
|
+
end
|
|
1781
|
+
end
|