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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -0
- data/CHANGELOG.md +25 -0
- data/README.md +28 -1
- data/lib/parlour.rb +5 -0
- data/lib/parlour/conflict_resolver.rb +73 -11
- data/lib/parlour/detached_rbi_generator.rb +30 -0
- data/lib/parlour/kernel_hack.rb +2 -0
- data/lib/parlour/parse_error.rb +19 -0
- data/lib/parlour/rbi_generator.rb +13 -7
- data/lib/parlour/rbi_generator/class_namespace.rb +8 -5
- data/lib/parlour/rbi_generator/module_namespace.rb +6 -4
- data/lib/parlour/rbi_generator/namespace.rb +11 -6
- data/lib/parlour/rbi_generator/options.rb +15 -2
- data/lib/parlour/rbi_generator/parameter.rb +1 -1
- data/lib/parlour/type_loader.rb +84 -0
- data/lib/parlour/type_parser.rb +609 -0
- data/lib/parlour/version.rb +1 -1
- data/parlour.gemspec +5 -4
- metadata +28 -10
@@ -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}.
|
@@ -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
|