parlour 1.0.0 → 4.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  3. data/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  4. data/.gitignore +1 -0
  5. data/.parlour +5 -0
  6. data/.rspec +0 -0
  7. data/.travis.yml +4 -3
  8. data/CHANGELOG.md +65 -0
  9. data/CODE_OF_CONDUCT.md +0 -0
  10. data/Gemfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +54 -1
  13. data/Rakefile +0 -0
  14. data/exe/parlour +68 -2
  15. data/lib/parlour.rb +7 -0
  16. data/lib/parlour/conflict_resolver.rb +129 -19
  17. data/lib/parlour/debugging.rb +0 -0
  18. data/lib/parlour/detached_rbi_generator.rb +25 -0
  19. data/lib/parlour/kernel_hack.rb +2 -0
  20. data/lib/parlour/parse_error.rb +19 -0
  21. data/lib/parlour/plugin.rb +1 -1
  22. data/lib/parlour/rbi_generator.rb +13 -7
  23. data/lib/parlour/rbi_generator/arbitrary.rb +0 -0
  24. data/lib/parlour/rbi_generator/attribute.rb +0 -0
  25. data/lib/parlour/rbi_generator/class_namespace.rb +8 -5
  26. data/lib/parlour/rbi_generator/constant.rb +11 -2
  27. data/lib/parlour/rbi_generator/enum_class_namespace.rb +24 -2
  28. data/lib/parlour/rbi_generator/extend.rb +0 -0
  29. data/lib/parlour/rbi_generator/include.rb +0 -0
  30. data/lib/parlour/rbi_generator/method.rb +0 -0
  31. data/lib/parlour/rbi_generator/module_namespace.rb +6 -4
  32. data/lib/parlour/rbi_generator/namespace.rb +81 -15
  33. data/lib/parlour/rbi_generator/options.rb +15 -2
  34. data/lib/parlour/rbi_generator/parameter.rb +5 -5
  35. data/lib/parlour/rbi_generator/rbi_object.rb +0 -0
  36. data/lib/parlour/rbi_generator/struct_class_namespace.rb +103 -0
  37. data/lib/parlour/rbi_generator/struct_prop.rb +136 -0
  38. data/lib/parlour/type_loader.rb +104 -0
  39. data/lib/parlour/type_parser.rb +854 -0
  40. data/lib/parlour/version.rb +1 -1
  41. data/parlour.gemspec +6 -5
  42. data/plugin_examples/foobar_plugin.rb +0 -0
  43. data/rbi/parlour.rbi +893 -0
  44. metadata +40 -18
@@ -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
@@ -42,7 +42,7 @@ module Parlour
42
42
 
43
43
  @kind = :keyword if kind == :normal && name.end_with?(':')
44
44
 
45
- @type = type
45
+ @type = type || 'T.untyped'
46
46
  @default = default
47
47
  end
48
48
 
@@ -80,10 +80,10 @@ module Parlour
80
80
  T.must(name[prefix.length..-1])
81
81
  end
82
82
 
83
- sig { returns(T.nilable(String)) }
83
+ sig { returns(String) }
84
84
  # A Sorbet string of this parameter's type, such as +"String"+ or
85
85
  # +"T.untyped"+.
86
- # @return [String, nil]
86
+ # @return [String]
87
87
  attr_reader :type
88
88
 
89
89
  sig { returns(T.nilable(String)) }
@@ -118,7 +118,7 @@ module Parlour
118
118
  #
119
119
  # @return [String]
120
120
  def to_sig_param
121
- "#{name_without_kind}: #{type || 'T.untyped'}"
121
+ "#{name_without_kind}: #{type}"
122
122
  end#
123
123
 
124
124
  # A mapping of {kind} values to the characteristic prefixes each kind has.
File without changes
@@ -0,0 +1,103 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbiGenerator
4
+ # Represents an struct definition; that is, a class which subclasses
5
+ # +T::Struct+ and declares `prop` members.
6
+ class StructClassNamespace < ClassNamespace
7
+ extend T::Sig
8
+
9
+ sig do
10
+ params(
11
+ generator: RbiGenerator,
12
+ name: String,
13
+ final: T::Boolean,
14
+ props: T::Array[StructProp],
15
+ abstract: T::Boolean,
16
+ block: T.nilable(T.proc.params(x: StructClassNamespace).void)
17
+ ).void
18
+ end
19
+ # Creates a new struct class definition.
20
+ # @note You should use {Namespace#create_struct_class} rather than this directly.
21
+ #
22
+ # @param generator [RbiGenerator] The current RbiGenerator.
23
+ # @param name [String] The name of this class.
24
+ # @param final [Boolean] Whether this namespace is final.
25
+ # @param props [Array<StructProp>] The props of the struct.
26
+ # @param abstract [Boolean] A boolean indicating whether this class is abstract.
27
+ # @param block A block which the new instance yields itself to.
28
+ # @return [void]
29
+ def initialize(generator, name, final, props, abstract, &block)
30
+ super(generator, name, final, 'T::Struct', abstract, &block)
31
+ @props = props
32
+ end
33
+
34
+ sig { returns(T::Array[StructProp]) }
35
+ # The props of the struct.
36
+ # @return [Array<StructProp>]
37
+ attr_reader :props
38
+
39
+ sig do
40
+ override.params(
41
+ indent_level: Integer,
42
+ options: Options
43
+ ).returns(T::Array[String])
44
+ end
45
+ # Generates the RBI lines for the body of this struct. This consists of
46
+ # {props}, {includes}, {extends} and {children}.
47
+ #
48
+ # @param indent_level [Integer] The indentation level to generate the lines at.
49
+ # @param options [Options] The formatting options to use.
50
+ # @return [Array<String>] The RBI lines for the body, formatted as specified.
51
+ def generate_body(indent_level, options)
52
+ result = []
53
+ props.each do |prop|
54
+ result << options.indented(indent_level, prop.to_prop_call)
55
+ end
56
+ result << ''
57
+
58
+ result + super
59
+ end
60
+
61
+ sig do
62
+ override.params(
63
+ others: T::Array[RbiGenerator::RbiObject]
64
+ ).returns(T::Boolean)
65
+ end
66
+ # Given an array of {StructClassNamespace} instances, returns true if they may
67
+ # be merged into this instance using {merge_into_self}. For instances to
68
+ # be mergeable, they must either all be abstract or all not be abstract,
69
+ # and they must define the same superclass (or none at all).
70
+ #
71
+ # @param others [Array<RbiGenerator::RbiObject>] An array of other {StructClassNamespace} instances.
72
+ # @return [Boolean] Whether this instance may be merged with them.
73
+ def mergeable?(others)
74
+ others = T.cast(others, T::Array[Namespace]) rescue (return false)
75
+ all = others + [self]
76
+ all_structs = T.cast(all.select { |x| StructClassNamespace === x }, T::Array[StructClassNamespace])
77
+
78
+ T.must(super && all_structs.map { |s| s.props.map(&:to_prop_call).sort }.reject(&:empty?).uniq.length <= 1)
79
+ end
80
+
81
+ sig do
82
+ override.params(
83
+ others: T::Array[RbiGenerator::RbiObject]
84
+ ).void
85
+ end
86
+ # Given an array of {StructClassNamespace} instances, merges them into this one.
87
+ # You MUST ensure that {mergeable?} is true for those instances.
88
+ #
89
+ # @param others [Array<RbiGenerator::RbiObject>] An array of other {StructClassNamespace} instances.
90
+ # @return [void]
91
+ def merge_into_self(others)
92
+ super
93
+
94
+ others.each do |other|
95
+ next unless StructClassNamespace === other
96
+ other = T.cast(other, StructClassNamespace)
97
+
98
+ @props = other.props if props.empty?
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,136 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbiGenerator
4
+ # Represents a +T::Struct+ property.
5
+ class StructProp
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ name: String,
11
+ type: String,
12
+ optional: T.nilable(T.any(T::Boolean, Symbol)),
13
+ enum: T.nilable(String),
14
+ dont_store: T.nilable(T::Boolean),
15
+ foreign: T.nilable(String),
16
+ default: T.nilable(String),
17
+ factory: T.nilable(String),
18
+ immutable: T.nilable(T::Boolean),
19
+ array: T.nilable(String),
20
+ override: T.nilable(T::Boolean),
21
+ redaction: T.nilable(String),
22
+ ).void
23
+ end
24
+ # Create a new struct property.
25
+ #
26
+ # For documentation on all optional properties, please refer to the
27
+ # documentation for T::Struct within the sorbet-runtime gem:
28
+ # https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/props/_props.rb#L31-L106
29
+ #
30
+ # @param name [String] The name of this property.
31
+ # @param type [String] A Sorbet string of this property's type, such as
32
+ # +"String"+.
33
+ # @return [void]
34
+ def initialize(name, type, optional: nil, enum: nil, dont_store: nil,
35
+ foreign: nil, default: nil, factory: nil, immutable: nil, array: nil,
36
+ override: nil, redaction: nil)
37
+
38
+ @name = name
39
+ @type = type
40
+ @optional = optional
41
+ @enum = enum
42
+ @dont_store = dont_store
43
+ @foreign = foreign
44
+ @default = default
45
+ @factory = factory
46
+ @immutable = immutable
47
+ @array = array
48
+ @override = override
49
+ @redaction = redaction
50
+ end
51
+
52
+ sig { params(other: Object).returns(T::Boolean) }
53
+ # Returns true if this instance is equal to another instance.
54
+ #
55
+ # @param other [Object] The other instance. If this is not a {StructProp} (or a
56
+ # subclass of it), this will always return false.
57
+ # @return [Boolean]
58
+ def ==(other)
59
+ StructProp === other &&
60
+ name == other.name &&
61
+ type == other.type &&
62
+ optional == other.optional &&
63
+ enum == other.enum &&
64
+ dont_store == other.dont_store &&
65
+ foreign == other.foreign &&
66
+ default == other.default &&
67
+ factory == other.factory &&
68
+ immutable == other.immutable &&
69
+ array == other.array &&
70
+ override == other.override &&
71
+ redaction == other.redaction
72
+ end
73
+
74
+ sig { returns(String) }
75
+ # The name of this parameter, including any prefixes or suffixes such as
76
+ # +*+.
77
+ # @return [String]
78
+ attr_reader :name
79
+
80
+ sig { returns(T.nilable(String)) }
81
+ # A Sorbet string of this parameter's type, such as +"String"+ or
82
+ # +"T.untyped"+.
83
+ # @return [String, nil]
84
+ attr_reader :type
85
+
86
+ sig { returns(T.nilable(T.any(T::Boolean, Symbol))) }
87
+ attr_reader :optional
88
+
89
+ sig { returns(T.nilable(String)) }
90
+ attr_reader :enum
91
+
92
+ sig { returns(T.nilable(T::Boolean)) }
93
+ attr_reader :dont_store
94
+
95
+ sig { returns(T.nilable(String)) }
96
+ attr_reader :foreign
97
+
98
+ sig { returns(T.nilable(String)) }
99
+ attr_reader :default
100
+
101
+ sig { returns(T.nilable(String)) }
102
+ attr_reader :factory
103
+
104
+ sig { returns(T.nilable(T::Boolean)) }
105
+ attr_reader :immutable
106
+
107
+ sig { returns(T.nilable(String)) }
108
+ attr_reader :array
109
+
110
+ sig { returns(T.nilable(T::Boolean)) }
111
+ attr_reader :override
112
+
113
+ sig { returns(T.nilable(String)) }
114
+ attr_reader :redaction
115
+
116
+ # The optional properties available on instances of this class.
117
+ EXTRA_PROPERTIES = %i{
118
+ optional enum dont_store foreign default factory immutable array override redaction
119
+ }
120
+
121
+ sig { returns(String) }
122
+ # Returns the +prop+ call required to create this property.
123
+ # @return [String]
124
+ def to_prop_call
125
+ call = "prop :#{name}, #{type}"
126
+
127
+ EXTRA_PROPERTIES.each do |extra_property|
128
+ value = send extra_property
129
+ call += ", #{extra_property}: #{value}" unless value.nil?
130
+ end
131
+
132
+ call
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,104 @@
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), generator: T.nilable(RbiGenerator)).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, generator: nil)
21
+ TypeParser.from_source(filename || '(source)', source, generator: generator).parse_all
22
+ end
23
+
24
+ sig { params(filename: String, generator: T.nilable(RbiGenerator)).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, generator: nil)
30
+ load_source(File.read(filename), filename, generator: generator)
31
+ end
32
+
33
+ sig do
34
+ params(
35
+ root: String,
36
+ inclusions: T::Array[String],
37
+ exclusions: T::Array[String],
38
+ generator: T.nilable(RbiGenerator),
39
+ ).returns(RbiGenerator::Namespace)
40
+ end
41
+ # Loads an entire Sorbet project using Sorbet's file table, obeying any
42
+ # "typed: ignore" sigils, into a tree of objects.
43
+ #
44
+ # Files within sorbet/rbi/hidden-definitions are excluded, as they cause
45
+ # merging issues with abstract classes due to sorbet/sorbet#1653.
46
+ #
47
+ # @param [String] root The root of the project; where the "sorbet" directory
48
+ # and "Gemfile" are located.
49
+ # @param [Array<String>] inclusions A list of files to include when loading
50
+ # the project, relative to the given root.
51
+ # @param [Array<String>] exclusions A list of files to exclude when loading
52
+ # the project, relative to the given root.
53
+ # @return [RbiGenerator::Namespace] The root of the object tree.
54
+ def self.load_project(root, inclusions: ['.'], exclusions: [], generator: nil)
55
+ expanded_inclusions = inclusions.map { |i| File.expand_path(i, root) }
56
+ expanded_exclusions = exclusions.map { |e| File.expand_path(e, root) }
57
+
58
+ stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
59
+ 'bundle exec srb tc -p file-table-json',
60
+ chdir: root
61
+ )
62
+
63
+ file_table_hash = JSON.parse(T.must(stdout.read))
64
+ file_table_entries = file_table_hash['files']
65
+
66
+ namespaces = T.let([], T::Array[Parlour::RbiGenerator::Namespace])
67
+ file_table_entries.each do |file_table_entry|
68
+ next if file_table_entry['sigil'] == 'Ignore' ||
69
+ file_table_entry['strict'] == 'Ignore'
70
+
71
+ rel_path = file_table_entry['path']
72
+ next if rel_path.start_with?('./sorbet/rbi/hidden-definitions/')
73
+ path = File.expand_path(rel_path, root)
74
+
75
+ # Skip this file if it was excluded
76
+ next if !expanded_inclusions.any? { |i| path.start_with?(i) } \
77
+ || expanded_exclusions.any? { |e| path.start_with?(e) }
78
+
79
+ # There are some entries which are URLs to stdlib
80
+ next unless File.exist?(path)
81
+
82
+ namespaces << load_file(path, generator: generator)
83
+ end
84
+
85
+ namespaces.uniq!
86
+
87
+ raise 'project is empty' if namespaces.empty?
88
+
89
+ first_namespace, *other_namespaces = namespaces
90
+ first_namespace = T.must(first_namespace)
91
+ other_namespaces = T.must(other_namespaces)
92
+
93
+ raise 'cannot merge namespaces loaded from a project' \
94
+ unless first_namespace.mergeable?(other_namespaces)
95
+ first_namespace.merge_into_self(other_namespaces)
96
+
97
+ ConflictResolver.new.resolve_conflicts(first_namespace) do |n, o|
98
+ raise "conflict of #{o.length} objects: #{n}"
99
+ end
100
+
101
+ first_namespace
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,854 @@
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, generator: T.nilable(RbiGenerator)).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, generator: nil)
96
+ @ast = ast
97
+ @unknown_node_errors = unknown_node_errors
98
+ @generator = generator || DetachedRbiGenerator.new
99
+ end
100
+
101
+ sig { params(filename: String, source: String, generator: T.nilable(RbiGenerator)).returns(TypeParser) }
102
+ # Creates a new {TypeParser} from a source file and its filename.
103
+ #
104
+ # @param [String] filename A filename. This does not need to be an actual
105
+ # file; it merely identifies this source.
106
+ # @param [String] source The Ruby source code.
107
+ # @return [TypeParser]
108
+ def self.from_source(filename, source, generator: nil)
109
+ buffer = Parser::Source::Buffer.new(filename)
110
+ buffer.source = source
111
+
112
+ # || special case handles parser returning nil on an empty file
113
+ parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body)
114
+ TypeParser.new(parsed, generator: generator)
115
+ end
116
+
117
+ sig { returns(Parser::AST::Node) }
118
+ # @return [Parser::AST::Node] The AST which this type parser should use.
119
+ attr_accessor :ast
120
+
121
+ sig { returns(T::Boolean) }
122
+ # @return [Boolean] Whether to raise an error if a node of an unknown kind
123
+ # is encountered.
124
+ attr_reader :unknown_node_errors
125
+
126
+ sig { returns(RbiGenerator) }
127
+ # @return [RbiGenerator] The {RbiGenerator} to load the source into.
128
+ attr_accessor :generator
129
+
130
+ # Parses the entire source file and returns the resulting root namespace.
131
+ #
132
+ # @return [RbiGenerator::Namespace] The root namespace of the parsed source.
133
+ sig { returns(RbiGenerator::Namespace) }
134
+ def parse_all
135
+ root = generator.root
136
+ root.children.concat(parse_path_to_object(NodePath.new([])))
137
+ root
138
+ end
139
+
140
+ # Given a path to a node in the AST, parses the object definitions it
141
+ # represents and returns it, recursing to any child namespaces and parsing
142
+ # any methods within.
143
+ #
144
+ # If the node directly represents several nodes, such as being a
145
+ # (begin ...) node, they are all returned.
146
+ #
147
+ # @param [NodePath] path The path to the namespace definition. Do not pass
148
+ # any of the other parameters to this method in an external call.
149
+ # @return [Array<RbiGenerator::RbiObject>] The objects the node at the path
150
+ # represents, parsed into an RBI generator object.
151
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::RbiObject]) }
152
+ def parse_path_to_object(path, is_within_eigenclass: false)
153
+ node = path.traverse(ast)
154
+
155
+ case node.type
156
+ when :class
157
+ parse_err 'cannot declare classes in an eigenclass', node if is_within_eigenclass
158
+
159
+ name, superclass, body = *node
160
+ final = body_has_modifier?(body, :final!)
161
+ abstract = body_has_modifier?(body, :abstract!)
162
+ includes, extends = body ? body_includes_and_extends(body) : [[], []]
163
+
164
+ # Create all classes, if we're given a definition like "class A::B"
165
+ *parent_names, this_name = constant_names(name)
166
+ target = T.let(nil, T.nilable(RbiGenerator::Namespace))
167
+ top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
168
+ parent_names.each do |n|
169
+ new_obj = RbiGenerator::Namespace.new(
170
+ generator,
171
+ n.to_s,
172
+ false,
173
+ )
174
+ target.children << new_obj if target
175
+ target = new_obj
176
+ top_level ||= new_obj
177
+ end if parent_names
178
+
179
+ # Instantiate the correct kind of class
180
+ if ['T::Struct', '::T::Struct'].include?(node_to_s(superclass))
181
+ # Find all of this struct's props and consts
182
+ # The body is typically a `begin` element but when there's only
183
+ # one node there's no wrapping block and instead it would directly
184
+ # be the node.
185
+ prop_nodes = body.nil? ? [] :
186
+ (body.type == :begin ? body.to_a : [body]).select { |x| x.type == :send && [:prop, :const].include?(x.to_a[1]) }
187
+
188
+ props = prop_nodes.map do |prop_node|
189
+ _, prop_type, name_node, type_node, extras_hash_node = *prop_node
190
+
191
+ # "const" is just "prop ..., immutable: true"
192
+ extras_hash = extras_hash_node.to_a.map do |pair_node|
193
+ key_node, value_node = *pair_node
194
+ parse_err 'prop/const key must be a symbol', prop_node unless key_node.type == :sym
195
+ key = key_node.to_a.first
196
+
197
+ value =
198
+ if key == :default
199
+ T.must(node_to_s(value_node))
200
+ else
201
+ case value_node.type
202
+ when :true
203
+ true
204
+ when :false
205
+ false
206
+ when :sym
207
+ value_node.to_a.first
208
+ else
209
+ T.must(node_to_s(value_node))
210
+ end
211
+ end
212
+
213
+ [key, value]
214
+ end.to_h
215
+
216
+ if prop_type == :const
217
+ parse_err 'const cannot use immutable key', prop_node unless extras_hash[:immutable].nil?
218
+ extras_hash[:immutable] = true
219
+ end
220
+
221
+ # Get prop/const name
222
+ parse_err 'prop/const name must be a symbol or string', prop_node unless [:sym, :str].include?(name_node.type)
223
+ name = name_node.to_a.first.to_s
224
+
225
+ RbiGenerator::StructProp.new(
226
+ name,
227
+ T.must(node_to_s(type_node)),
228
+ **T.unsafe(extras_hash)
229
+ )
230
+ end
231
+
232
+ final_obj = RbiGenerator::StructClassNamespace.new(
233
+ generator,
234
+ this_name.to_s,
235
+ final,
236
+ props,
237
+ abstract,
238
+ )
239
+ elsif ['T::Enum', '::T::Enum'].include?(node_to_s(superclass))
240
+ # Look for (block (send nil :enums) ...) structure
241
+ enums_node = body.nil? ? nil :
242
+ (body.type == :begin ? body.to_a : [body]).find { |x| x.type == :block && x.to_a[0].type == :send && x.to_a[0].to_a[1] == :enums }
243
+
244
+ # Find the constant assigments within this block
245
+ constant_nodes = enums_node.to_a[2].to_a
246
+
247
+ # Convert this to an array to enums as EnumClassNamespace expects
248
+ enums = constant_nodes.map do |constant_node|
249
+ _, name, new_node = *constant_node
250
+ serialize_value = node_to_s(new_node.to_a[2])
251
+
252
+ serialize_value ? [name.to_s, serialize_value] : name.to_s
253
+ end
254
+
255
+ final_obj = RbiGenerator::EnumClassNamespace.new(
256
+ generator,
257
+ this_name.to_s,
258
+ final,
259
+ enums,
260
+ abstract,
261
+ )
262
+ else
263
+ final_obj = RbiGenerator::ClassNamespace.new(
264
+ generator,
265
+ this_name.to_s,
266
+ final,
267
+ node_to_s(superclass),
268
+ abstract,
269
+ )
270
+ end
271
+
272
+ final_obj.children.concat(parse_path_to_object(path.child(2))) if body
273
+ final_obj.create_includes(includes)
274
+ final_obj.create_extends(extends)
275
+
276
+ if target
277
+ target.children << final_obj
278
+ [top_level]
279
+ else
280
+ [final_obj]
281
+ end
282
+ when :module
283
+ parse_err 'cannot declare modules in an eigenclass', node if is_within_eigenclass
284
+
285
+ name, body = *node
286
+ final = body_has_modifier?(body, :final!)
287
+ interface = body_has_modifier?(body, :interface!)
288
+ includes, extends = body ? body_includes_and_extends(body) : [[], []]
289
+
290
+ # Create all modules, if we're given a definition like "module A::B"
291
+ *parent_names, this_name = constant_names(name)
292
+ target = T.let(nil, T.nilable(RbiGenerator::Namespace))
293
+ top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
294
+ parent_names.each do |n|
295
+ new_obj = RbiGenerator::Namespace.new(
296
+ generator,
297
+ n.to_s,
298
+ false,
299
+ )
300
+ target.children << new_obj if target
301
+ target = new_obj
302
+ top_level ||= new_obj
303
+ end if parent_names
304
+
305
+ final_obj = RbiGenerator::ModuleNamespace.new(
306
+ generator,
307
+ this_name.to_s,
308
+ final,
309
+ interface,
310
+ ) do |m|
311
+ m.children.concat(parse_path_to_object(path.child(1))) if body
312
+ m.create_includes(includes)
313
+ m.create_extends(extends)
314
+ end
315
+
316
+ if target
317
+ target.children << final_obj
318
+ [top_level]
319
+ else
320
+ [final_obj]
321
+ end
322
+ when :send, :block
323
+ if sig_node?(node)
324
+ parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass)
325
+ elsif node.type == :send &&
326
+ [:attr_reader, :attr_writer, :attr_accessor].include?(node.to_a[1]) &&
327
+ !previous_sibling_sig_node?(path)
328
+ parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
329
+ else
330
+ []
331
+ end
332
+ when :def, :defs
333
+ if previous_sibling_sig_node?(path)
334
+ []
335
+ else
336
+ parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
337
+ end
338
+ when :sclass
339
+ parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self
340
+ parse_path_to_object(path.child(1), is_within_eigenclass: true)
341
+ when :begin
342
+ # Just map over all the things
343
+ node.to_a.length.times.map do |c|
344
+ parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
345
+ end.flatten
346
+ when :casgn
347
+ _, name, body = *node
348
+ [Parlour::RbiGenerator::Constant.new(
349
+ generator,
350
+ name: T.must(name).to_s,
351
+ value: T.must(node_to_s(body)),
352
+ )]
353
+ else
354
+ if unknown_node_errors
355
+ parse_err "don't understand node type #{node.type}", node
356
+ else
357
+ []
358
+ end
359
+ end
360
+ end
361
+
362
+ # A parsed sig, not associated with a method.
363
+ class IntermediateSig < T::Struct
364
+ prop :type_parameters, T.nilable(T::Array[Symbol])
365
+ prop :overridable, T::Boolean
366
+ prop :override, T::Boolean
367
+ prop :abstract, T::Boolean
368
+ prop :final, T::Boolean
369
+ prop :return_type, T.nilable(String)
370
+ prop :params, T.nilable(T::Array[Parser::AST::Node])
371
+ end
372
+
373
+ sig { params(path: NodePath).returns(IntermediateSig) }
374
+ # Given a path to a sig in the AST, parses that sig into an intermediate
375
+ # sig object.
376
+ # This will raise an exception if the sig is invalid.
377
+ # This is intended to be called by {#parse_sig_into_methods}, and shouldn't
378
+ # be called manually unless you're doing something hacky.
379
+ #
380
+ # @param [NodePath] path The sig to parse.
381
+ # @return [IntermediateSig] The parsed sig.
382
+ def parse_sig_into_sig(path)
383
+ sig_block_node = path.traverse(ast)
384
+
385
+ # A sig's AST uses lots of nested nodes due to a deep call chain, so let's
386
+ # flatten it out to make it easier to work with
387
+ sig_chain = []
388
+ current_sig_chain_node = sig_block_node.to_a[2]
389
+ while current_sig_chain_node
390
+ name = current_sig_chain_node.to_a[1]
391
+ arguments = current_sig_chain_node.to_a[2..-1]
392
+
393
+ sig_chain << [name, arguments]
394
+ current_sig_chain_node = current_sig_chain_node.to_a[0]
395
+ end
396
+
397
+ # Get basic boolean flags
398
+ override = !!sig_chain.find { |(n, a)| n == :override && a.empty? }
399
+ overridable = !!sig_chain.find { |(n, a)| n == :overridable && a.empty? }
400
+ abstract = !!sig_chain.find { |(n, a)| n == :abstract && a.empty? }
401
+
402
+ # Determine whether this method is final (i.e. sig(:final))
403
+ _, _, *sig_arguments = *sig_block_node.to_a[0]
404
+ final = sig_arguments.any? { |a| a.type == :sym && a.to_a[0] == :final }
405
+
406
+ # Find the return type by looking for a "returns" call
407
+ return_type = sig_chain
408
+ .find { |(n, _)| n == :returns }
409
+ &.then do |(_, a)|
410
+ parse_err 'wrong number of arguments in "returns" for sig', sig_block_node if a.length != 1
411
+ node_to_s(a[0])
412
+ end
413
+
414
+ # Find the arguments specified in the "params" call in the sig
415
+ sig_args = sig_chain
416
+ .find { |(n, _)| n == :params }
417
+ &.then do |(_, a)|
418
+ parse_err 'wrong number of arguments in "params" for sig', sig_block_node if a.length != 1
419
+ arg = a[0]
420
+ parse_err 'argument to "params" should be a hash', arg unless arg.type == :hash
421
+ arg.to_a
422
+ end
423
+
424
+ # Find type parameters if they were used
425
+ type_parameters = sig_chain
426
+ .find { |(n, _)| n == :type_parameters }
427
+ &.then do |(_, a)|
428
+ a.map do |arg|
429
+ parse_err 'type parameter must be a symbol', arg if arg.type != :sym
430
+ arg.to_a[0]
431
+ end
432
+ end
433
+
434
+ IntermediateSig.new(
435
+ type_parameters: type_parameters,
436
+ overridable: overridable,
437
+ override: override,
438
+ abstract: abstract,
439
+ final: final,
440
+ params: sig_args,
441
+ return_type: return_type
442
+ )
443
+ end
444
+
445
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
446
+ # Given a path to a sig in the AST, finds the associated definition and
447
+ # parses them into methods.
448
+ # This will raise an exception if the sig is invalid.
449
+ # Usually this will return one method; the only exception currently is for
450
+ # attributes, where multiple can be declared in one call, e.g.
451
+ # +attr_reader :x, :y, :z+.
452
+ #
453
+ # @param [NodePath] path The sig to parse.
454
+ # @param [Boolean] is_within_eigenclass Whether the method definition this sig is
455
+ # associated with appears inside an eigenclass definition. If true, the
456
+ # returned method is made a class method. If the method definition
457
+ # is already a class method, an exception is thrown as the method will be
458
+ # a class method of the eigenclass, which Parlour can't represent.
459
+ # @return [<RbiGenerator::Method>] The parsed methods.
460
+ def parse_sig_into_methods(path, is_within_eigenclass: false)
461
+ sig_block_node = path.traverse(ast)
462
+
463
+ # A :def node represents a definition like "def x; end"
464
+ # A :defs node represents a definition like "def self.x; end"
465
+ def_node = path.sibling(1).traverse(ast)
466
+ case def_node.type
467
+ when :def
468
+ class_method = false
469
+ def_names = [def_node.to_a[0].to_s]
470
+ def_params = def_node.to_a[1].to_a
471
+ kind = :def
472
+ when :defs
473
+ parse_err 'targeted definitions on a non-self target are not supported', def_node \
474
+ unless def_node.to_a[0].type == :self
475
+ class_method = true
476
+ def_names = [def_node.to_a[1].to_s]
477
+ def_params = def_node.to_a[2].to_a
478
+ kind = :def
479
+ when :send
480
+ target, method_name, *parameters = *def_node
481
+
482
+ parse_err 'node after a sig must be a method definition', def_node \
483
+ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
484
+ || target != nil
485
+
486
+ parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
487
+
488
+ kind = :attr
489
+ attr_direction = method_name.to_s.gsub('attr_', '').to_sym
490
+ def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
491
+ class_method = false
492
+ else
493
+ parse_err 'node after a sig must be a method definition', def_node
494
+ end
495
+
496
+ if is_within_eigenclass
497
+ parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
498
+ class_method = true
499
+ end
500
+
501
+ this_sig = parse_sig_into_sig(path)
502
+ params = this_sig.params
503
+ return_type = this_sig.return_type
504
+
505
+ if kind == :def
506
+ # Sorbet allows a trailing blockarg that's not in the sig
507
+ if params &&
508
+ def_params.length == params.length + 1 &&
509
+ def_params[-1].type == :blockarg
510
+ def_params = def_params[0...-1]
511
+ end
512
+
513
+ parse_err 'mismatching number of arguments in sig and def', sig_block_node \
514
+ if params && def_params.length != params.length
515
+
516
+ # sig_args will look like:
517
+ # [(pair (sym :x) <type>), (pair (sym :y) <type>), ...]
518
+ # def_params will look like:
519
+ # [(arg :x), (arg :y), ...]
520
+ parameters = params \
521
+ ? zip_by(params, ->x{ x.to_a[0].to_a[0] }, def_params, ->x{ x.to_a[0] })
522
+ .map do |sig_arg, def_param|
523
+
524
+ arg_name = def_param.to_a[0]
525
+
526
+ # TODO: anonymous restarg
527
+ full_name = arg_name.to_s
528
+ full_name = "*#{arg_name}" if def_param.type == :restarg
529
+ full_name = "**#{arg_name}" if def_param.type == :kwrestarg
530
+ full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
531
+ full_name = "&#{arg_name}" if def_param.type == :blockarg
532
+
533
+ default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
534
+ type = node_to_s(sig_arg.to_a[1])
535
+
536
+ RbiGenerator::Parameter.new(full_name, type: type, default: default)
537
+ end
538
+ : []
539
+
540
+ # There should only be one ever here, but future-proofing anyway
541
+ def_names.map do |def_name|
542
+ RbiGenerator::Method.new(
543
+ generator,
544
+ def_name,
545
+ parameters,
546
+ return_type,
547
+ type_parameters: this_sig.type_parameters,
548
+ override: this_sig.override,
549
+ overridable: this_sig.overridable,
550
+ abstract: this_sig.abstract,
551
+ final: this_sig.final,
552
+ class_method: class_method
553
+ )
554
+ end
555
+ elsif kind == :attr
556
+ case attr_direction
557
+ when :reader, :accessor
558
+ parse_err "attr_#{attr_direction} sig should have no parameters", sig_block_node \
559
+ if params && params.length > 0
560
+
561
+ parse_err "attr_#{attr_direction} sig should have non-void return", sig_block_node \
562
+ unless return_type
563
+
564
+ attr_type = return_type
565
+ when :writer
566
+ # These are special and can only have one name
567
+ raise 'typed attr_writer can only have one name' if def_names.length > 1
568
+
569
+ def_name = def_names[0]
570
+ parse_err "attr_writer sig should take one argument with the property's name", sig_block_node \
571
+ if !params || params.length != 1 || params[0].to_a[0].to_a[0].to_s != def_name
572
+
573
+ parse_err "attr_writer sig should have non-void return", sig_block_node \
574
+ if return_type.nil?
575
+
576
+ attr_type = T.must(node_to_s(params[0].to_a[1]))
577
+ else
578
+ raise "unknown attribute direction #{attr_direction}"
579
+ end
580
+
581
+ def_names.map do |def_name|
582
+ RbiGenerator::Attribute.new(
583
+ generator,
584
+ def_name,
585
+ attr_direction,
586
+ attr_type,
587
+ class_attribute: class_method
588
+ )
589
+ end
590
+ else
591
+ raise "unknown definition kind #{kind}"
592
+ end
593
+ end
594
+
595
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
596
+ # Given a path to a method in the AST, finds the associated definition and
597
+ # parses them into methods.
598
+ # Usually this will return one method; the only exception currently is for
599
+ # attributes, where multiple can be declared in one call, e.g.
600
+ # +attr_reader :x, :y, :z+.
601
+ #
602
+ # @param [NodePath] path The sig to parse.
603
+ # @param [Boolean] is_within_eigenclass Whether the method definition this sig is
604
+ # associated with appears inside an eigenclass definition. If true, the
605
+ # returned method is made a class method. If the method definition
606
+ # is already a class method, an exception is thrown as the method will be
607
+ # a class method of the eigenclass, which Parlour can't represent.
608
+ # @return [<RbiGenerator::Method>] The parsed methods.
609
+ def parse_method_into_methods(path, is_within_eigenclass: false)
610
+ # A :def node represents a definition like "def x; end"
611
+ # A :defs node represents a definition like "def self.x; end"
612
+ def_node = path.traverse(ast)
613
+ case def_node.type
614
+ when :def
615
+ class_method = false
616
+ def_names = [def_node.to_a[0].to_s]
617
+ def_params = def_node.to_a[1].to_a
618
+ kind = :def
619
+ when :defs
620
+ parse_err 'targeted definitions on a non-self target are not supported', def_node \
621
+ unless def_node.to_a[0].type == :self
622
+ class_method = true
623
+ def_names = [def_node.to_a[1].to_s]
624
+ def_params = def_node.to_a[2].to_a
625
+ kind = :def
626
+ when :send
627
+ target, method_name, *parameters = *def_node
628
+
629
+ parse_err 'node after a sig must be a method definition', def_node \
630
+ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
631
+ || target != nil
632
+
633
+ parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
634
+
635
+ kind = :attr
636
+ attr_direction = method_name.to_s.gsub('attr_', '').to_sym
637
+ def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
638
+ class_method = false
639
+ else
640
+ parse_err 'node after a sig must be a method definition', def_node
641
+ end
642
+
643
+ if is_within_eigenclass
644
+ parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
645
+ class_method = true
646
+ end
647
+
648
+ return_type = "T.untyped"
649
+
650
+ if kind == :def
651
+ parameters = def_params.map do |def_param|
652
+ arg_name = def_param.to_a[0]
653
+
654
+ # TODO: anonymous restarg
655
+ full_name = arg_name.to_s
656
+ full_name = "*#{arg_name}" if def_param.type == :restarg
657
+ full_name = "**#{arg_name}" if def_param.type == :kwrestarg
658
+ full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
659
+ full_name = "&#{arg_name}" if def_param.type == :blockarg
660
+
661
+ default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
662
+ type = nil
663
+
664
+ RbiGenerator::Parameter.new(full_name, type: type, default: default)
665
+ end
666
+
667
+ # There should only be one ever here, but future-proofing anyway
668
+ def_names.map do |def_name|
669
+ RbiGenerator::Method.new(
670
+ generator,
671
+ def_name,
672
+ parameters,
673
+ return_type,
674
+ class_method: class_method
675
+ )
676
+ end
677
+ elsif kind == :attr
678
+ case attr_direction
679
+ when :reader, :accessor, :writer
680
+ attr_type = return_type
681
+ else
682
+ raise "unknown attribute direction #{attr_direction}"
683
+ end
684
+
685
+ def_names.map do |def_name|
686
+ RbiGenerator::Attribute.new(
687
+ generator,
688
+ def_name,
689
+ attr_direction,
690
+ attr_type,
691
+ class_attribute: class_method
692
+ )
693
+ end
694
+ else
695
+ raise "unknown definition kind #{kind}"
696
+ end
697
+ end
698
+
699
+ protected
700
+
701
+ sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
702
+ # Given a node representing a simple chain of constants (such as A or
703
+ # A::B::C), converts that node into an array of the constant names which
704
+ # are accessed. For example, A::B::C would become [:A, :B, :C].
705
+ #
706
+ # @param [Parser::AST::Node, nil] node The node to convert. This must
707
+ # consist only of nested (:const) nodes.
708
+ # @return [Array<Symbol>] The chain of constant names.
709
+ def constant_names(node)
710
+ node ? constant_names(node.to_a[0]) + [node.to_a[1]] : []
711
+ end
712
+
713
+ sig { params(node: Parser::AST::Node).returns(T::Boolean) }
714
+ # Given a node, returns a boolean indicating whether that node represents a
715
+ # a call to "sig" with a block. No further semantic checking, such as
716
+ # whether it preceeds a method call, is done.
717
+ #
718
+ # @param [Parser::AST::Node] node The node to check.
719
+ # @return [Boolean] True if that node represents a "sig" call, false
720
+ # otherwise.
721
+ def sig_node?(node)
722
+ node.type == :block &&
723
+ node.to_a[0].type == :send &&
724
+ node.to_a[0].to_a[1] == :sig
725
+ end
726
+
727
+ sig { params(path: NodePath).returns(T::Boolean) }
728
+ # Given a path, returns a boolean indicating whether the previous sibling
729
+ # represents a call to "sig" with a block.
730
+ #
731
+ # @param [NodePath] path The path to the namespace definition.
732
+ # @return [Boolean] True if that node represents a "sig" call, false
733
+ # otherwise.
734
+ def previous_sibling_sig_node?(path)
735
+ previous_sibling = path.sibling(-1)
736
+ previous_node = previous_sibling.traverse(ast)
737
+ sig_node?(previous_node)
738
+ rescue IndexError, ArgumentError, TypeError
739
+ # `sibling` call could raise IndexError or ArgumentError if reaching into negative indices
740
+ # `traverse` call could raise TypeError if path doesn't return Parser::AST::Node
741
+
742
+ false
743
+ end
744
+
745
+ sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
746
+ # Given an AST node, returns the source code from which it was constructed.
747
+ # If the given AST node is nil, this returns nil.
748
+ #
749
+ # @param [Parser::AST::Node, nil] node The AST node, or nil.
750
+ # @return [String] The source code string it represents, or nil.
751
+ def node_to_s(node)
752
+ return nil unless node
753
+
754
+ exp = node.loc.expression
755
+ exp.source_buffer.source[exp.begin_pos...exp.end_pos]
756
+ end
757
+
758
+ sig { params(node: T.nilable(Parser::AST::Node), modifier: Symbol).returns(T::Boolean) }
759
+ # Given an AST node and a symbol, determines if that node is a call (or a
760
+ # body containing a call at the top level) to the method represented by the
761
+ # symbol, without any arguments or a block.
762
+ #
763
+ # This is designed to be used to determine if a namespace body uses a Sorbet
764
+ # modifier such as "abstract!".
765
+ #
766
+ # @param [Parser::AST::Node, nil] node The AST node to search in.
767
+ # @param [Symbol] modifier The method name to search for.
768
+ # @return [T::Boolean] True if the call is found, or false otherwise.
769
+ def body_has_modifier?(node, modifier)
770
+ return false unless node
771
+
772
+ (node.type == :send && node.to_a == [nil, modifier]) ||
773
+ (node.type == :begin &&
774
+ node.to_a.any? { |c| c.type == :send && c.to_a == [nil, modifier] })
775
+ end
776
+
777
+ sig { params(node: Parser::AST::Node).returns([T::Array[String], T::Array[String]]) }
778
+ # Given an AST node representing the body of a class or module, returns two
779
+ # arrays of the includes and extends contained within the body.
780
+ #
781
+ # @param [Parser::AST::Node] node The body of the namespace.
782
+ # @return [(Array<String>, Array<String>)] An array of the includes and an
783
+ # array of the extends.
784
+ def body_includes_and_extends(node)
785
+ result = [[], []]
786
+
787
+ nodes_to_search = node.type == :begin ? node.to_a : [node]
788
+ nodes_to_search.each do |this_node|
789
+ next unless this_node.type == :send
790
+ target, name, *args = *this_node
791
+ next unless target.nil? && args.length == 1
792
+
793
+ if name == :include
794
+ result[0] << node_to_s(args.first)
795
+ elsif name == :extend
796
+ result[1] << node_to_s(args.first)
797
+ end
798
+ end
799
+
800
+ result
801
+ end
802
+
803
+ sig { params(desc: String, node: T.any(Parser::AST::Node, NodePath)).returns(T.noreturn) }
804
+ # Raises a parse error on a node.
805
+ # @param [String] desc A description of the error.
806
+ # @param [Parser::AST::Node, NodePath] A node, passed as either a path or a
807
+ # raw parser node.
808
+ def parse_err(desc, node)
809
+ node = node.traverse(ast) if node.is_a?(NodePath)
810
+ range = node.loc.expression
811
+ buffer = range.source_buffer
812
+
813
+ raise ParseError.new(buffer, range), desc
814
+ end
815
+
816
+ sig do
817
+ type_parameters(:A, :B)
818
+ .params(
819
+ a: T::Array[T.type_parameter(:A)],
820
+ fa: T.proc.params(item: T.type_parameter(:A)).returns(T.untyped),
821
+ b: T::Array[T.type_parameter(:B)],
822
+ fb: T.proc.params(item: T.type_parameter(:B)).returns(T.untyped)
823
+ )
824
+ .returns(T::Array[[T.type_parameter(:A), T.type_parameter(:B)]])
825
+ end
826
+ # Given two arrays and functions to get a key for each item in the two
827
+ # arrays, joins the two arrays into one array of pairs by that key.
828
+ #
829
+ # The arrays should both be the same length, and the key functions should
830
+ # never return duplicate keys for two different items.
831
+ #
832
+ # @param [Array<A>] a The first array.
833
+ # @param [A -> Any] fa A function to obtain a key for any element in the
834
+ # first array.
835
+ # @param [Array<B>] b The second array.
836
+ # @param [B -> Any] fb A function to obtain a key for any element in the
837
+ # second array.
838
+ # @return [Array<(A, B)>] An array of pairs, where the left of the pair is
839
+ # an element from A and the right is the element from B with the
840
+ # corresponding key.
841
+ def zip_by(a, fa, b, fb)
842
+ raise ArgumentError, "arrays are not the same length" if a.length != b.length
843
+
844
+ a.map do |a_item|
845
+ a_key = fa.(a_item)
846
+ b_items = b.select { |b_item| fb.(b_item) == a_key }
847
+ raise "multiple items for key #{a_key}" if b_items.length > 1
848
+ raise "no item in second list corresponding to key #{a_key}" if b_items.length == 0
849
+
850
+ [a_item, T.must(b_items[0])]
851
+ end
852
+ end
853
+ end
854
+ end