parlour 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ module Parlour
6
6
  class Options
7
7
  extend T::Sig
8
8
 
9
- sig { params(break_params: Integer, tab_size: Integer).void }
9
+ sig { params(break_params: Integer, tab_size: Integer, sort_namespaces: T::Boolean).void }
10
10
  # Creates a new set of formatting options.
11
11
  #
12
12
  # @example Create Options with +break_params+ of +4+ and +tab_size+ of +2+.
@@ -15,10 +15,13 @@ module Parlour
15
15
  # @param break_params [Integer] If there are at least this many parameters in a
16
16
  # Sorbet +sig+, then it is broken onto separate lines.
17
17
  # @param tab_size [Integer] The number of spaces to use per indent.
18
+ # @param sort_namespaces [Boolean] Whether to sort all items within a
19
+ # namespace alphabetically.
18
20
  # @return [void]
19
- def initialize(break_params:, tab_size:)
21
+ def initialize(break_params:, tab_size:, sort_namespaces:)
20
22
  @break_params = break_params
21
23
  @tab_size = tab_size
24
+ @sort_namespaces = sort_namespaces
22
25
  end
23
26
 
24
27
  sig { returns(Integer) }
@@ -46,6 +49,16 @@ module Parlour
46
49
  # @return [Integer]
47
50
  attr_reader :tab_size
48
51
 
52
+ sig { returns(T::Boolean) }
53
+ # Whether to sort all items within a namespace alphabetically.
54
+ # Items which are typically grouped together, such as "include" or
55
+ # "extend" calls, will remain grouped together when sorted.
56
+ # If true, items are sorted by their name when the RBI is generated.
57
+ # If false, items are generated in the order they are added to the
58
+ # namespace.
59
+ # @return [Boolean]
60
+ attr_reader :sort_namespaces
61
+
49
62
  sig { params(level: Integer, str: String).returns(String) }
50
63
  # Returns a string indented to the given indent level, according to the
51
64
  # set {tab_size}.
@@ -7,7 +7,7 @@ module Parlour
7
7
 
8
8
  sig do
9
9
  params(
10
- name: T.nilable(String),
10
+ name: String,
11
11
  type: T.nilable(String),
12
12
  default: T.nilable(String)
13
13
  ).void
@@ -0,0 +1,84 @@
1
+ # typed: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+
6
+ module Parlour
7
+ module TypeLoader
8
+ extend T::Sig
9
+
10
+ # TODO: make this into a class which stores configuration and passes it to
11
+ # all typeparsers
12
+
13
+ sig { params(source: String, filename: T.nilable(String)).returns(RbiGenerator::Namespace) }
14
+ # Converts Ruby source code into a tree of objects.
15
+ #
16
+ # @param [String] source The Ruby source code.
17
+ # @param [String, nil] filename The filename to use when parsing this code.
18
+ # This may be used in error messages, but is optional.
19
+ # @return [RbiGenerator::Namespace] The root of the object tree.
20
+ def self.load_source(source, filename = nil)
21
+ TypeParser.from_source(filename || '(source)', source).parse_all
22
+ end
23
+
24
+ sig { params(filename: String).returns(RbiGenerator::Namespace) }
25
+ # Converts Ruby source code into a tree of objects from a file.
26
+ #
27
+ # @param [String] filename The name of the file to load code from.
28
+ # @return [RbiGenerator::Namespace] The root of the object tree.
29
+ def self.load_file(filename)
30
+ load_source(File.read(filename), filename)
31
+ end
32
+
33
+ sig { params(root: String).returns(RbiGenerator::Namespace) }
34
+ # Loads an entire Sorbet project using Sorbet's file table, obeying any
35
+ # "typed: ignore" sigils, into a tree of objects.
36
+ #
37
+ # Files within sorbet/rbi/hidden-definitions are excluded, as they cause
38
+ # merging issues with abstract classes due to sorbet/sorbet#1653.
39
+ #
40
+ # @param [String] root The root of the project; where the "sorbet" directory
41
+ # and "Gemfile" are located.
42
+ # @return [RbiGenerator::Namespace] The root of the object tree.
43
+ def self.load_project(root)
44
+ stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
45
+ 'bundle exec srb tc -p file-table-json',
46
+ chdir: root
47
+ )
48
+
49
+ file_table_hash = JSON.parse(T.must(stdout.read))
50
+ file_table_entries = file_table_hash['files']
51
+
52
+ namespaces = T.let([], T::Array[Parlour::RbiGenerator::Namespace])
53
+ file_table_entries.each do |file_table_entry|
54
+ next if file_table_entry['sigil'] == 'Ignore' ||
55
+ file_table_entry['strict'] == 'Ignore'
56
+
57
+ rel_path = file_table_entry['path']
58
+ next if rel_path.start_with?('./sorbet/rbi/hidden-definitions/')
59
+ path = File.expand_path(rel_path, root)
60
+
61
+ # There are some entries which are URLs to stdlib
62
+ next unless File.exist?(path)
63
+
64
+ namespaces << load_file(path)
65
+ end
66
+
67
+ raise 'project is empty' if namespaces.empty?
68
+
69
+ first_namespace, *other_namespaces = namespaces
70
+ first_namespace = T.must(first_namespace)
71
+ other_namespaces = T.must(other_namespaces)
72
+
73
+ raise 'cannot merge namespaces loaded from a project' \
74
+ unless first_namespace.mergeable?(other_namespaces)
75
+ first_namespace.merge_into_self(other_namespaces)
76
+
77
+ ConflictResolver.new.resolve_conflicts(first_namespace) do |n, o|
78
+ raise "conflict of #{o.length} objects: #{n}"
79
+ end
80
+
81
+ first_namespace
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,609 @@
1
+ # typed: true
2
+
3
+ # TODO: support sig without runtime
4
+
5
+ # Suppress versioning warnings - the majority of users will not actually be
6
+ # using this, so we don't want to pollute their console
7
+ old_verbose = $VERBOSE
8
+ begin
9
+ $VERBOSE = nil
10
+ require 'parser/current'
11
+ ensure
12
+ $VERBOSE = old_verbose
13
+ end
14
+
15
+ module Parlour
16
+ # Parses Ruby source to find Sorbet type signatures.
17
+ class TypeParser
18
+ # Represents a path of indices which can be traversed to reach a specific
19
+ # node in an AST.
20
+ class NodePath
21
+ extend T::Sig
22
+
23
+ sig { returns(T::Array[Integer]) }
24
+ # @return [Array<Integer>] The path of indices.
25
+ attr_reader :indices
26
+
27
+ sig { params(indices: T::Array[Integer]).void }
28
+ # Creates a new {NodePath}.
29
+ #
30
+ # @param [Array<Integer>] indices The path of indices.
31
+ def initialize(indices)
32
+ @indices = indices
33
+ end
34
+
35
+ sig { returns(NodePath) }
36
+ # @return [NodePath] The parent path for the node at this path.
37
+ def parent
38
+ if indices.empty?
39
+ raise IndexError, 'cannot get parent of an empty path'
40
+ else
41
+ NodePath.new(T.must(indices[0...-1]))
42
+ end
43
+ end
44
+
45
+ sig { params(index: Integer).returns(NodePath) }
46
+ # @param [Integer] index The index of the child whose path to return.
47
+ # @return [NodePath] The path to the child at the given index.
48
+ def child(index)
49
+ NodePath.new(indices + [index])
50
+ end
51
+
52
+ sig { params(offset: Integer).returns(NodePath) }
53
+ # @param [Integer] offset The sibling offset to use. 0 is the current
54
+ # node, -1 is the previous node, or 3 is is the node three nodes after
55
+ # this one.
56
+ # @return [NodePath] The path to the sibling with the given context.
57
+ def sibling(offset)
58
+ if indices.empty?
59
+ raise IndexError, 'cannot get sibling of an empty path'
60
+ else
61
+ *xs, x = indices
62
+ x = T.must(x)
63
+ raise ArgumentError, "sibling offset of #{offset} results in " \
64
+ "negative index of #{x + offset}" if x + offset < 0
65
+ NodePath.new(T.must(xs) + [x + offset])
66
+ end
67
+ end
68
+
69
+ sig { params(start: Parser::AST::Node).returns(Parser::AST::Node) }
70
+ # Follows this path of indices from an AST node.
71
+ #
72
+ # @param [Parser::AST::Node] start The AST node to start from.
73
+ # @return [Parser::AST::Node] The resulting AST node.
74
+ def traverse(start)
75
+ current = T.unsafe(start)
76
+ indices.each do |index|
77
+ raise IndexError, 'path does not exist' if index >= current.to_a.length
78
+ current = current.to_a[index]
79
+ end
80
+ current
81
+ end
82
+ end
83
+
84
+ extend T::Sig
85
+
86
+ sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean).void }
87
+ # Creates a new {TypeParser} from whitequark/parser AST.
88
+ #
89
+ # @param [Parser::AST::Node] The AST.
90
+ # @param [Boolean] unknown_node_errors Whether to raise an error if a node
91
+ # of an unknown kind is encountered. If false, the node is simply ignored;
92
+ # if true, a parse error is raised. Setting this to true is likely to
93
+ # raise errors for lots of non-RBI Ruby code, but setting it to false
94
+ # could miss genuine typed objects if Parlour or your code contains a bug.
95
+ def initialize(ast, unknown_node_errors: false)
96
+ @ast = ast
97
+ @unknown_node_errors = unknown_node_errors
98
+ end
99
+
100
+ sig { params(filename: String, source: String).returns(TypeParser) }
101
+ # Creates a new {TypeParser} from a source file and its filename.
102
+ #
103
+ # @param [String] filename A filename. This does not need to be an actual
104
+ # file; it merely identifies this source.
105
+ # @param [String] source The Ruby source code.
106
+ # @return [TypeParser]
107
+ def self.from_source(filename, source)
108
+ buffer = Parser::Source::Buffer.new(filename)
109
+ buffer.source = source
110
+
111
+ TypeParser.new(Parser::CurrentRuby.new.parse(buffer))
112
+ end
113
+
114
+ sig { returns(Parser::AST::Node) }
115
+ # @return [Parser::AST::Node] The AST which this type parser should use.
116
+ attr_accessor :ast
117
+
118
+ sig { returns(T::Boolean) }
119
+ # @return [Boolean] Whether to raise an error if a node of an unknown kind
120
+ # is encountered.
121
+ attr_reader :unknown_node_errors
122
+
123
+ # Parses the entire source file and returns the resulting root namespace.
124
+ #
125
+ # @return [RbiGenerator::Namespace] The root namespace of the parsed source.
126
+ sig { returns(RbiGenerator::Namespace) }
127
+ def parse_all
128
+ root = RbiGenerator::Namespace.new(DetachedRbiGenerator.new)
129
+ root.children.concat(parse_path_to_object(NodePath.new([])))
130
+ root
131
+ end
132
+
133
+ # Given a path to a node in the AST, parses the object definitions it
134
+ # represents and returns it, recursing to any child namespaces and parsing
135
+ # any methods within.
136
+ #
137
+ # If the node directly represents several nodes, such as being a
138
+ # (begin ...) node, they are all returned.
139
+ #
140
+ # @param [NodePath] path The path to the namespace definition. Do not pass
141
+ # any of the other parameters to this method in an external call.
142
+ # @return [Array<RbiGenerator::RbiObject>] The objects the node at the path
143
+ # represents, parsed into an RBI generator object.
144
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::RbiObject]) }
145
+ def parse_path_to_object(path, is_within_eigenclass: false)
146
+ node = path.traverse(ast)
147
+
148
+ case node.type
149
+ when :class
150
+ parse_err 'cannot declare classes in an eigenclass', node if is_within_eigenclass
151
+
152
+ name, superclass, body = *node
153
+ final = body_has_modifier?(body, :final!)
154
+ abstract = body_has_modifier?(body, :abstract!)
155
+ includes, extends = body ? body_includes_and_extends(body) : [[], []]
156
+
157
+ # Create all classes, if we're given a definition like "class A::B"
158
+ *parent_names, this_name = constant_names(name)
159
+ target = T.let(nil, T.nilable(RbiGenerator::Namespace))
160
+ top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
161
+ parent_names.each do |n|
162
+ new_obj = RbiGenerator::Namespace.new(
163
+ DetachedRbiGenerator.new,
164
+ n.to_s,
165
+ false,
166
+ )
167
+ target.children << new_obj if target
168
+ target = new_obj
169
+ top_level ||= new_obj
170
+ end if parent_names
171
+
172
+ final_obj = RbiGenerator::ClassNamespace.new(
173
+ DetachedRbiGenerator.new,
174
+ this_name.to_s,
175
+ final,
176
+ node_to_s(superclass),
177
+ abstract,
178
+ ) do |c|
179
+ c.children.concat(parse_path_to_object(path.child(2))) if body
180
+ c.create_includes(includes)
181
+ c.create_extends(extends)
182
+ end
183
+
184
+ if target
185
+ target.children << final_obj
186
+ [top_level]
187
+ else
188
+ [final_obj]
189
+ end
190
+ when :module
191
+ parse_err 'cannot declare modules in an eigenclass', node if is_within_eigenclass
192
+
193
+ name, body = *node
194
+ final = body_has_modifier?(body, :final!)
195
+ interface = body_has_modifier?(body, :interface!)
196
+ includes, extends = body ? body_includes_and_extends(body) : [[], []]
197
+
198
+ # Create all modules, if we're given a definition like "module A::B"
199
+ *parent_names, this_name = constant_names(name)
200
+ target = T.let(nil, T.nilable(RbiGenerator::Namespace))
201
+ top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
202
+ parent_names.each do |n|
203
+ new_obj = RbiGenerator::Namespace.new(
204
+ DetachedRbiGenerator.new,
205
+ n.to_s,
206
+ false,
207
+ )
208
+ target.children << new_obj if target
209
+ target = new_obj
210
+ top_level ||= new_obj
211
+ end if parent_names
212
+
213
+ final_obj = RbiGenerator::ModuleNamespace.new(
214
+ DetachedRbiGenerator.new,
215
+ this_name.to_s,
216
+ final,
217
+ interface,
218
+ ) do |m|
219
+ m.children.concat(parse_path_to_object(path.child(1))) if body
220
+ m.create_includes(includes)
221
+ m.create_extends(extends)
222
+ end
223
+
224
+ if target
225
+ target.children << final_obj
226
+ [top_level]
227
+ else
228
+ [final_obj]
229
+ end
230
+ when :send, :block
231
+ if sig_node?(node)
232
+ parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass)
233
+ else
234
+ []
235
+ end
236
+ when :def, :defs
237
+ # TODO: Support for defs without sigs
238
+ # If so, we need some kind of state machine to determine whether
239
+ # they've already been dealt with by the "when :send" clause and
240
+ # #parse_sig_into_methods.
241
+ # If not, just ignore this.
242
+ []
243
+ when :sclass
244
+ parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self
245
+ parse_path_to_object(path.child(1), is_within_eigenclass: true)
246
+ when :begin
247
+ # Just map over all the things
248
+ node.to_a.length.times.map do |c|
249
+ parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
250
+ end.flatten
251
+ else
252
+ if unknown_node_errors
253
+ parse_err "don't understand node type #{node.type}", node
254
+ else
255
+ []
256
+ end
257
+ end
258
+ end
259
+
260
+ # A parsed sig, not associated with a method.
261
+ class IntermediateSig < T::Struct
262
+ prop :overridable, T::Boolean
263
+ prop :override, T::Boolean
264
+ prop :abstract, T::Boolean
265
+ prop :final, T::Boolean
266
+ prop :return_type, T.nilable(String)
267
+ prop :params, T.nilable(T::Array[Parser::AST::Node])
268
+ end
269
+
270
+ sig { params(path: NodePath).returns(IntermediateSig) }
271
+ # Given a path to a sig in the AST, parses that sig into an intermediate
272
+ # sig object.
273
+ # This will raise an exception if the sig is invalid.
274
+ # This is intended to be called by {#parse_sig_into_methods}, and shouldn't
275
+ # be called manually unless you're doing something hacky.
276
+ #
277
+ # @param [NodePath] path The sig to parse.
278
+ # @return [IntermediateSig] The parsed sig.
279
+ def parse_sig_into_sig(path)
280
+ sig_block_node = path.traverse(ast)
281
+
282
+ # A sig's AST uses lots of nested nodes due to a deep call chain, so let's
283
+ # flatten it out to make it easier to work with
284
+ sig_chain = []
285
+ current_sig_chain_node = sig_block_node.to_a[2]
286
+ while current_sig_chain_node
287
+ name = current_sig_chain_node.to_a[1]
288
+ arguments = current_sig_chain_node.to_a[2..-1]
289
+
290
+ sig_chain << [name, arguments]
291
+ current_sig_chain_node = current_sig_chain_node.to_a[0]
292
+ end
293
+
294
+ # Get basic boolean flags
295
+ override = !!sig_chain.find { |(n, a)| n == :override && a.empty? }
296
+ overridable = !!sig_chain.find { |(n, a)| n == :overridable && a.empty? }
297
+ abstract = !!sig_chain.find { |(n, a)| n == :abstract && a.empty? }
298
+
299
+ # Determine whether this method is final (i.e. sig(:final))
300
+ _, _, *sig_arguments = *sig_block_node.to_a[0]
301
+ final = sig_arguments.any? { |a| a.type == :sym && a.to_a[0] == :final }
302
+
303
+ # Find the return type by looking for a "returns" call
304
+ return_type = sig_chain
305
+ .find { |(n, _)| n == :returns }
306
+ &.then do |(_, a)|
307
+ parse_err 'wrong number of arguments in "returns" for sig', sig_block_node if a.length != 1
308
+ node_to_s(a[0])
309
+ end
310
+
311
+ # Find the arguments specified in the "params" call in the sig
312
+ sig_args = sig_chain
313
+ .find { |(n, _)| n == :params }
314
+ &.then do |(_, a)|
315
+ parse_err 'wrong number of arguments in "params" for sig', sig_block_node if a.length != 1
316
+ arg = a[0]
317
+ parse_err 'argument to "params" should be a hash', arg unless arg.type == :hash
318
+ arg.to_a
319
+ end
320
+
321
+ IntermediateSig.new(
322
+ overridable: overridable,
323
+ override: override,
324
+ abstract: abstract,
325
+ final: final,
326
+ params: sig_args,
327
+ return_type: return_type
328
+ )
329
+ end
330
+
331
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
332
+ # Given a path to a sig in the AST, finds the associated definition and
333
+ # parses them into methods.
334
+ # This will raise an exception if the sig is invalid.
335
+ # Usually this will return one method; the only exception currently is for
336
+ # attributes, where multiple can be declared in one call, e.g.
337
+ # +attr_reader :x, :y, :z+.
338
+ #
339
+ # @param [NodePath] path The sig to parse.
340
+ # @param [Boolean] is_within_eigenclass Whether the method definition this sig is
341
+ # associated with appears inside an eigenclass definition. If true, the
342
+ # returned method is made a class method. If the method definition
343
+ # is already a class method, an exception is thrown as the method will be
344
+ # a class method of the eigenclass, which Parlour can't represent.
345
+ # @return [<RbiGenerator::Method>] The parsed methods.
346
+ def parse_sig_into_methods(path, is_within_eigenclass: false)
347
+ sig_block_node = path.traverse(ast)
348
+
349
+ # A :def node represents a definition like "def x; end"
350
+ # A :defs node represents a definition like "def self.x; end"
351
+ def_node = path.sibling(1).traverse(ast)
352
+ case def_node.type
353
+ when :def
354
+ class_method = false
355
+ def_names = [def_node.to_a[0].to_s]
356
+ def_params = def_node.to_a[1].to_a
357
+ kind = :def
358
+ when :defs
359
+ parse_err 'targeted definitions on a non-self target are not supported', def_node \
360
+ unless def_node.to_a[0].type == :self
361
+ class_method = true
362
+ def_names = [def_node.to_a[1].to_s]
363
+ def_params = def_node.to_a[2].to_a
364
+ kind = :def
365
+ when :send
366
+ target, method_name, *parameters = *def_node
367
+
368
+ parse_err 'node after a sig must be a method definition', def_node \
369
+ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
370
+ || target != nil
371
+
372
+ parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
373
+
374
+ kind = :attr
375
+ attr_direction = method_name.to_s.gsub('attr_', '').to_sym
376
+ def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
377
+ class_method = false
378
+ else
379
+ parse_err 'node after a sig must be a method definition', def_node
380
+ end
381
+
382
+ if is_within_eigenclass
383
+ parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
384
+ class_method = true
385
+ end
386
+
387
+ this_sig = parse_sig_into_sig(path)
388
+ params = this_sig.params
389
+ return_type = this_sig.return_type
390
+
391
+ if kind == :def
392
+ parse_err 'mismatching number of arguments in sig and def', sig_block_node \
393
+ if params && def_params.length != params.length
394
+
395
+ # sig_args will look like:
396
+ # [(pair (sym :x) <type>), (pair (sym :y) <type>), ...]
397
+ # def_params will look like:
398
+ # [(arg :x), (arg :y), ...]
399
+ parameters = params \
400
+ ? zip_by(params, ->x{ x.to_a[0].to_a[0] }, def_params, ->x{ x.to_a[0] })
401
+ .map do |sig_arg, def_param|
402
+ arg_name = def_param.to_a[0]
403
+
404
+ # TODO: anonymous restarg
405
+ full_name = arg_name.to_s
406
+ full_name = "*#{arg_name}" if def_param.type == :restarg
407
+ full_name = "**#{arg_name}" if def_param.type == :kwrestarg
408
+ full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
409
+ full_name = "&#{arg_name}" if def_param.type == :blockarg
410
+
411
+ default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
412
+ type = node_to_s(sig_arg.to_a[1])
413
+
414
+ RbiGenerator::Parameter.new(full_name, type: type, default: default)
415
+ end
416
+ : []
417
+
418
+ # There should only be one ever here, but future-proofing anyway
419
+ def_names.map do |def_name|
420
+ RbiGenerator::Method.new(
421
+ DetachedRbiGenerator.new,
422
+ def_name,
423
+ parameters,
424
+ return_type,
425
+ override: this_sig.override,
426
+ overridable: this_sig.overridable,
427
+ abstract: this_sig.abstract,
428
+ final: this_sig.final,
429
+ class_method: class_method
430
+ )
431
+ end
432
+ elsif kind == :attr
433
+ case attr_direction
434
+ when :reader, :accessor
435
+ parse_err "attr_#{attr_direction} sig should have no parameters", sig_block_node \
436
+ if params && params.length > 0
437
+
438
+ parse_err "attr_#{attr_direction} sig should have non-void return", sig_block_node \
439
+ unless return_type
440
+
441
+ attr_type = return_type
442
+ when :writer
443
+ # These are special and can only have one name
444
+ raise 'typed attr_writer can only have one name' if def_names.length > 1
445
+
446
+ def_name = def_names[0]
447
+ parse_err "attr_writer sig should take one argument with the property's name", sig_block_node \
448
+ if !params || params.length != 1 || params[0].to_a[0].to_a[0].to_s != def_name
449
+
450
+ parse_err "attr_writer sig should have non-void return", sig_block_node \
451
+ if return_type.nil?
452
+
453
+ attr_type = T.must(node_to_s(params[0].to_a[1]))
454
+ else
455
+ raise "unknown attribute direction #{attr_direction}"
456
+ end
457
+
458
+ def_names.map do |def_name|
459
+ RbiGenerator::Attribute.new(
460
+ DetachedRbiGenerator.new,
461
+ def_name,
462
+ attr_direction,
463
+ attr_type,
464
+ class_attribute: class_method
465
+ )
466
+ end
467
+ else
468
+ raise "unknown definition kind #{kind}"
469
+ end
470
+ end
471
+
472
+ protected
473
+
474
+ sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
475
+ # Given a node representing a simple chain of constants (such as A or
476
+ # A::B::C), converts that node into an array of the constant names which
477
+ # are accessed. For example, A::B::C would become [:A, :B, :C].
478
+ #
479
+ # @param [Parser::AST::Node, nil] node The node to convert. This must
480
+ # consist only of nested (:const) nodes.
481
+ # @return [Array<Symbol>] The chain of constant names.
482
+ def constant_names(node)
483
+ node ? constant_names(node.to_a[0]) + [node.to_a[1]] : []
484
+ end
485
+
486
+ sig { params(node: Parser::AST::Node).returns(T::Boolean) }
487
+ # Given a node, returns a boolean indicating whether that node represents a
488
+ # a call to "sig" with a block. No further semantic checking, such as
489
+ # whether it preceeds a method call, is done.
490
+ #
491
+ # @param [Parser::AST::Node] node The node to check.
492
+ # @return [Boolean] True if that node represents a "sig" call, false
493
+ # otherwise.
494
+ def sig_node?(node)
495
+ node.type == :block &&
496
+ node.to_a[0].type == :send &&
497
+ node.to_a[0].to_a[1] == :sig
498
+ end
499
+
500
+ sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
501
+ # Given an AST node, returns the source code from which it was constructed.
502
+ # If the given AST node is nil, this returns nil.
503
+ #
504
+ # @param [Parser::AST::Node, nil] node The AST node, or nil.
505
+ # @return [String] The source code string it represents, or nil.
506
+ def node_to_s(node)
507
+ return nil unless node
508
+
509
+ exp = node.loc.expression
510
+ exp.source_buffer.source[exp.begin_pos...exp.end_pos]
511
+ end
512
+
513
+ sig { params(node: T.nilable(Parser::AST::Node), modifier: Symbol).returns(T::Boolean) }
514
+ # Given an AST node and a symbol, determines if that node is a call (or a
515
+ # body containing a call at the top level) to the method represented by the
516
+ # symbol, without any arguments or a block.
517
+ #
518
+ # This is designed to be used to determine if a namespace body uses a Sorbet
519
+ # modifier such as "abstract!".
520
+ #
521
+ # @param [Parser::AST::Node, nil] node The AST node to search in.
522
+ # @param [Symbol] modifier The method name to search for.
523
+ # @return [T::Boolean] True if the call is found, or false otherwise.
524
+ def body_has_modifier?(node, modifier)
525
+ return false unless node
526
+
527
+ (node.type == :send && node.to_a == [nil, modifier]) ||
528
+ (node.type == :begin &&
529
+ node.to_a.any? { |c| c.type == :send && c.to_a == [nil, modifier] })
530
+ end
531
+
532
+ sig { params(node: Parser::AST::Node).returns([T::Array[String], T::Array[String]]) }
533
+ # Given an AST node representing the body of a class or module, returns two
534
+ # arrays of the includes and extends contained within the body.
535
+ #
536
+ # @param [Parser::AST::Node] node The body of the namespace.
537
+ # @return [(Array<String>, Array<String>)] An array of the includes and an
538
+ # array of the extends.
539
+ def body_includes_and_extends(node)
540
+ result = [[], []]
541
+
542
+ nodes_to_search = node.type == :begin ? node.to_a : [node]
543
+ nodes_to_search.each do |this_node|
544
+ next unless this_node.type == :send
545
+ target, name, *args = *this_node
546
+ next unless target.nil? && args.length == 1
547
+
548
+ if name == :include
549
+ result[0] << node_to_s(args.first)
550
+ elsif name == :extend
551
+ result[1] << node_to_s(args.first)
552
+ end
553
+ end
554
+
555
+ result
556
+ end
557
+
558
+ sig { params(desc: String, node: T.any(Parser::AST::Node, NodePath)).returns(T.noreturn) }
559
+ # Raises a parse error on a node.
560
+ # @param [String] desc A description of the error.
561
+ # @param [Parser::AST::Node, NodePath] A node, passed as either a path or a
562
+ # raw parser node.
563
+ def parse_err(desc, node)
564
+ node = node.traverse(ast) if node.is_a?(NodePath)
565
+ range = node.loc.expression
566
+ buffer = range.source_buffer
567
+
568
+ raise ParseError.new(buffer, range), desc
569
+ end
570
+
571
+ sig do
572
+ type_parameters(:A, :B)
573
+ .params(
574
+ a: T::Array[T.type_parameter(:A)],
575
+ fa: T.proc.params(item: T.type_parameter(:A)).returns(T.untyped),
576
+ b: T::Array[T.type_parameter(:B)],
577
+ fb: T.proc.params(item: T.type_parameter(:B)).returns(T.untyped)
578
+ )
579
+ .returns(T::Array[[T.type_parameter(:A), T.type_parameter(:B)]])
580
+ end
581
+ # Given two arrays and functions to get a key for each item in the two
582
+ # arrays, joins the two arrays into one array of pairs by that key.
583
+ #
584
+ # The arrays should both be the same length, and the key functions should
585
+ # never return duplicate keys for two different items.
586
+ #
587
+ # @param [Array<A>] a The first array.
588
+ # @param [A -> Any] fa A function to obtain a key for any element in the
589
+ # first array.
590
+ # @param [Array<B>] b The second array.
591
+ # @param [B -> Any] fb A function to obtain a key for any element in the
592
+ # second array.
593
+ # @return [Array<(A, B)>] An array of pairs, where the left of the pair is
594
+ # an element from A and the right is the element from B with the
595
+ # corresponding key.
596
+ def zip_by(a, fa, b, fb)
597
+ raise ArgumentError, "arrays are not the same length" if a.length != b.length
598
+
599
+ a.map do |a_item|
600
+ a_key = fa.(a_item)
601
+ b_items = b.select { |b_item| fb.(b_item) == a_key }
602
+ raise "multiple items for key #{a_key}" if b_items.length > 1
603
+ raise "no item in second list corresponding to key #{a_key}" if b_items.length == 0
604
+
605
+ [a_item, T.must(b_items[0])]
606
+ end
607
+ end
608
+ end
609
+ end