rbs-inline 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,141 @@
1
+ module RBS
2
+ module Inline
3
+ module AST
4
+ class Tree
5
+ attr_reader :trees
6
+ attr_reader :type
7
+ attr_reader :non_trivia_trees
8
+
9
+ def initialize(type)
10
+ @type = type
11
+ @trees = []
12
+ @non_trivia_trees = []
13
+ end
14
+
15
+ def <<(tok)
16
+ trees << tok
17
+ unless tok.is_a?(Array) && tok[0] == :tWHITESPACE
18
+ non_trivia_trees << tok
19
+ end
20
+ self
21
+ end
22
+
23
+ def to_s
24
+ buf = +""
25
+
26
+ trees.each do |tree|
27
+ case tree
28
+ when Array
29
+ buf << tree[1]
30
+ when Tree
31
+ buf << tree.to_s
32
+ when nil
33
+ else
34
+ loc = tree.location or raise
35
+ buf << loc.source
36
+ end
37
+ end
38
+
39
+ buf
40
+ end
41
+
42
+ def nth_token(index)
43
+ tok = non_trivia_trees[index]
44
+ case tok
45
+ when Array, nil
46
+ tok
47
+ else
48
+ raise
49
+ end
50
+ end
51
+
52
+ def nth_token?(index)
53
+ tok = non_trivia_trees[index]
54
+ case tok
55
+ when Array
56
+ tok
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ def nth_token!(index)
63
+ nth_token(index) || raise
64
+ end
65
+
66
+ def nth_tree(index)
67
+ tok = non_trivia_trees[index]
68
+ case tok
69
+ when Tree, nil
70
+ tok
71
+ else
72
+ raise
73
+ end
74
+ end
75
+
76
+ def nth_tree?(index)
77
+ tok = non_trivia_trees[index]
78
+ case tok
79
+ when Tree
80
+ tok
81
+ else
82
+ nil
83
+ end
84
+ end
85
+
86
+ def nth_tree!(index)
87
+ nth_tree(index) || raise
88
+ end
89
+
90
+
91
+ def nth_type(index)
92
+ tok = non_trivia_trees[index]
93
+ case tok
94
+ when Array, Tree, MethodType
95
+ raise
96
+ else
97
+ tok
98
+ end
99
+ end
100
+
101
+ def nth_type?(index)
102
+ tok = non_trivia_trees[index]
103
+ case tok
104
+ when Array, Tree, nil, MethodType
105
+ nil
106
+ else
107
+ tok
108
+ end
109
+ end
110
+
111
+ def nth_type!(index)
112
+ nth_type(index) || raise
113
+ end
114
+
115
+ def nth_method_type(index)
116
+ tok = non_trivia_trees[index]
117
+ case tok
118
+ when MethodType, nil
119
+ tok
120
+ else
121
+ raise
122
+ end
123
+ end
124
+
125
+ def nth_method_type?(index)
126
+ tok = non_trivia_trees[index]
127
+ case tok
128
+ when MethodType
129
+ tok
130
+ else
131
+ nil
132
+ end
133
+ end
134
+
135
+ def nth_method_type!(index)
136
+ nth_method_type(index) || raise
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,106 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "optparse"
4
+
5
+ module RBS
6
+ module Inline
7
+ class CLI
8
+ attr_reader :stdout, :stderr #:: IO
9
+ attr_reader :logger #:: Logger
10
+
11
+ # @rbs stdout: IO
12
+ # @rbs stderr: IO
13
+ def initialize(stdout: STDOUT, stderr: STDERR) #:: void
14
+ @stdout = stdout
15
+ @stderr = stderr
16
+ @logger = Logger.new(stderr)
17
+ logger.level = :ERROR
18
+ end
19
+
20
+ # @rbs args: Array[String]
21
+ # @rbs returns Integer
22
+ def run(args)
23
+ base_path = Pathname("lib")
24
+ output_path = nil #: Pathname?
25
+
26
+ OptionParser.new do |opts|
27
+ opts.on("--base=[BASE]", "The path to calculate relative path of files (defaults to #{base_path})") do
28
+ base_path = Pathname(_1)
29
+ end
30
+
31
+ opts.on("--output=[BASE]", "The directory where the RBS files are saved at (defaults to STDOUT if not specified)") do
32
+ output_path = Pathname(_1)
33
+ end
34
+
35
+ opts.on("--verbose") do
36
+ logger.level = :DEBUG
37
+ end
38
+ end.parse!(args)
39
+
40
+ base_path = Pathname.pwd + base_path
41
+
42
+ logger.debug { "base_path = #{base_path}, output_path = #{output_path}" }
43
+
44
+ targets = args.flat_map do
45
+ path = Pathname(_1)
46
+
47
+ if path.directory?
48
+ pattern = path + "**/*.rb"
49
+ Pathname.glob(pattern.to_s)
50
+ else
51
+ path
52
+ end
53
+ end
54
+
55
+ targets.sort!
56
+ targets.uniq!
57
+
58
+ count = 0
59
+
60
+ targets.each do |target|
61
+ relative_path = (Pathname.pwd + target).relative_path_from(base_path)
62
+ if output_path
63
+ output = output_path + relative_path.sub_ext(".rbs")
64
+
65
+ unless output.to_s.start_with?(output_path.to_s)
66
+ raise "Cannot calculate the output file path for #{target} in #{output_path}"
67
+ end
68
+
69
+ logger.debug { "Generating #{output} from #{target} ..." }
70
+ else
71
+ logger.debug { "Generating RBS declaration from #{target} ..." }
72
+ end
73
+
74
+ logger.debug { "Parsing ruby file #{target}..." }
75
+
76
+ if (uses, decls = Parser.parse(Prism.parse_file(target.to_s)))
77
+ writer = Writer.new()
78
+ writer.header("Generated from #{target.relative? ? target : target.relative_path_from(Pathname.pwd)} with RBS::Inline")
79
+ writer.write(uses, decls)
80
+
81
+ if output
82
+ unless output.parent.directory?
83
+ logger.debug { "Making directory #{output.parent}..." }
84
+ output.parent.mkpath
85
+ end
86
+
87
+ logger.debug { "Writing RBS file to #{output}..." }
88
+ output.write(writer.output)
89
+ else
90
+ stdout.puts writer.output
91
+ stdout.puts
92
+ end
93
+
94
+ count += 1
95
+ else
96
+ logger.debug { "Skipping #{target} because `# rbs_inline: enabled` comment not found" }
97
+ end
98
+ end
99
+
100
+ stderr.puts "🎉 Generated #{count} RBS files under #{output_path}"
101
+
102
+ 0
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,12 @@
1
+ module RBS
2
+ module Inline
3
+ module NodeUtils
4
+ def type_name(node)
5
+ case node
6
+ when Prism::ConstantReadNode
7
+ TypeName(node.name.to_s)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,360 @@
1
+ # rbs_inline: enabled
2
+
3
+ # @rbs use Prism::*
4
+
5
+ module RBS
6
+ module Inline
7
+ class Parser < Prism::Visitor
8
+ # The top level declarations
9
+ #
10
+ attr_reader :decls #:: Array[AST::Declarations::t]
11
+
12
+ # The surrounding declarations
13
+ #
14
+ attr_reader :surrounding_decls #:: Array[AST::Declarations::ModuleDecl | AST::Declarations::ClassDecl]
15
+
16
+ # ParsingResult associated with the line number at the end
17
+ #
18
+ # ```rb
19
+ # # Hello
20
+ # # world <= The comments hash includes `2` (line 2) to the two lines
21
+ # ```
22
+ #
23
+ # > [!IMPORTANT]
24
+ # > The values will be removed during parsing.
25
+ #
26
+ attr_reader :comments #:: Hash[Integer, AnnotationParser::ParsingResult]
27
+
28
+ # The current visibility applied to single `def` node
29
+ #
30
+ # Assuming it's directly inside `private` or `public` calls.
31
+ # `nil` when the `def` node is not inside `private` or `public` calls.
32
+ #
33
+ attr_reader :current_visibility #:: RBS::AST::Members::visibility?
34
+
35
+ def initialize() #:: void
36
+ @decls = []
37
+ @surrounding_decls = []
38
+ @comments = {}
39
+ end
40
+
41
+ # @rbs result: ParseResult[ProgramNode]
42
+ # @rbs returns [Array[AST::Annotations::Use], Array[AST::Declarations::t]]?
43
+ def self.parse(result)
44
+ instance = Parser.new()
45
+
46
+ # pp result
47
+
48
+ annots = AnnotationParser.parse(result.comments)
49
+ annots.each do |result|
50
+ instance.comments[result.line_range.end] = result
51
+ end
52
+
53
+ if result.comments.none? {|comment| comment.location.slice =~ /\A# rbs_inline: enabled\Z/}
54
+ return
55
+ end
56
+
57
+ uses = [] #: Array[AST::Annotations::Use]
58
+ annots.each do |annot|
59
+ annot.annotations.each do |annotation|
60
+ if annotation.is_a?(AST::Annotations::Use)
61
+ uses << annotation
62
+ end
63
+ end
64
+ end
65
+
66
+ instance.visit(result.value)
67
+
68
+ [
69
+ uses,
70
+ instance.decls
71
+ ]
72
+ end
73
+
74
+ # @rbs rturns AST::Declarations::ModuleDecl | AST::Declarations::ClassDecl | nil
75
+ def current_class_module_decl
76
+ surrounding_decls.last
77
+ end
78
+
79
+ # @rbs returns AST::Declarations::ModuleDecl | AST::Declarations::ClassDecl
80
+ def current_class_module_decl!
81
+ current_class_module_decl or raise
82
+ end
83
+
84
+ #:: (AST::Declarations::ModuleDecl | AST::Declarations::ClassDecl) { () -> void } -> void
85
+ #:: (AST::Declarations::ConstantDecl) -> void
86
+ def push_class_module_decl(decl)
87
+ if current = current_class_module_decl
88
+ current.members << decl
89
+ else
90
+ decls << decl
91
+ end
92
+
93
+ if block_given?
94
+ surrounding_decls.push(_ = decl)
95
+ begin
96
+ yield
97
+ ensure
98
+ surrounding_decls.pop()
99
+ end
100
+ end
101
+ end
102
+
103
+ # Load inner declarations and delete them from `#comments` hash
104
+ #
105
+ # It also sorts the `members` by `#start_line`` ascending.
106
+ #
107
+ # @rbs start_line: Integer
108
+ # @rbs end_line: Integer
109
+ # @rbs members: Array[AST::Members::t | AST::Declarations::t] --
110
+ # The destination.
111
+ # The method doesn't insert declarations, but have it to please type checker.
112
+ def load_inner_annotations(start_line, end_line, members) #:: void
113
+ comments = inner_annotations(start_line, end_line)
114
+
115
+ comments.each do |comment|
116
+ comment.annotations.each do |annotation|
117
+ case annotation
118
+ when AST::Annotations::IvarType
119
+ members << AST::Members::RBSIvar.new(comment, annotation)
120
+ when AST::Annotations::Embedded
121
+ members << AST::Members::RBSEmbedded.new(comment, annotation)
122
+ end
123
+ end
124
+ end
125
+
126
+ members.sort_by! { _1.start_line }
127
+ end
128
+
129
+ # @rbs override
130
+ def visit_class_node(node)
131
+ return if ignored_node?(node)
132
+
133
+ visit node.constant_path
134
+ visit node.superclass
135
+
136
+ associated_comment = comments.delete(node.location.start_line - 1)
137
+ if node.superclass
138
+ app_comment = application_annotation(node.superclass)
139
+ end
140
+
141
+ class_decl = AST::Declarations::ClassDecl.new(node, associated_comment, app_comment)
142
+
143
+ push_class_module_decl(class_decl) do
144
+ visit node.body
145
+ end
146
+
147
+ load_inner_annotations(node.location.start_line, node.location.end_line, class_decl.members)
148
+ end
149
+
150
+ # @rbs override
151
+ def visit_module_node(node)
152
+ return if ignored_node?(node)
153
+
154
+ visit node.constant_path
155
+
156
+ associated_comment = comments.delete(node.location.start_line - 1)
157
+
158
+ module_decl = AST::Declarations::ModuleDecl.new(node, associated_comment)
159
+ push_class_module_decl(module_decl) do
160
+ visit node.body
161
+ end
162
+
163
+ load_inner_annotations(node.location.start_line, node.location.end_line, module_decl.members)
164
+ end
165
+
166
+ # Returns an array of annotations from comments that is located between start_line and end_line
167
+ #
168
+ # ```rb
169
+ # module Foo # line 1 (start_line)
170
+ # # foo
171
+ # # bar
172
+ # end # line 4 (end_line)
173
+ # ```
174
+ #
175
+ # @rbs start_line: Integer
176
+ # @rbs end_line: Integer
177
+ def inner_annotations(start_line, end_line) #:: Array[AnnotationParser::ParsingResult]
178
+ annotations = comments.each_value.select do |annotation|
179
+ range = annotation.line_range
180
+ start_line < range.begin && range.end < end_line
181
+ end
182
+
183
+ annotations.each do |annot|
184
+ comments.delete(annot.line_range.end)
185
+ end
186
+ end
187
+
188
+ # @rbs override
189
+ def visit_def_node(node)
190
+ return if ignored_node?(node)
191
+ return unless current_class_module_decl
192
+
193
+ current_decl = current_class_module_decl!
194
+
195
+ if node.location
196
+ associated_comment = comments.delete(node.location.start_line - 1)
197
+ end
198
+
199
+ assertion = assertion_annotation(node.rparen_loc || node&.parameters&.location || node.name_loc)
200
+
201
+ current_decl.members << AST::Members::RubyDef.new(node, associated_comment, current_visibility, assertion)
202
+
203
+ super
204
+ end
205
+
206
+ # @rbs override
207
+ def visit_alias_method_node(node)
208
+ return if ignored_node?(node)
209
+
210
+ if node.location
211
+ comment = comments.delete(node.location.start_line - 1)
212
+ end
213
+ current_class_module_decl!.members << AST::Members::RubyAlias.new(node, comment)
214
+ super
215
+ end
216
+
217
+ # @rbs override
218
+ def visit_call_node(node)
219
+ return if ignored_node?(node)
220
+
221
+ case node.name
222
+ when :include, :prepend, :extend
223
+ case node.receiver
224
+ when nil, Prism::SelfNode
225
+ comment = comments.delete(node.location.start_line - 1)
226
+ app = application_annotation(node)
227
+
228
+ current_class_module_decl!.members << AST::Members::RubyMixin.new(node, comment, app)
229
+
230
+ return
231
+ end
232
+ when :attr_reader, :attr_accessor, :attr_writer
233
+ case node.receiver
234
+ when nil, Prism::SelfNode
235
+ comment = comments.delete(node.location.start_line - 1)
236
+
237
+ comment_line, assertion_comment = comments.find do |_, comment|
238
+ comment.line_range.begin == node.location.end_line
239
+ end
240
+ if assertion_comment && comment_line
241
+ comments.delete(comment_line)
242
+ assertion = assertion_comment.annotations.find do |annotation|
243
+ annotation.is_a?(AST::Annotations::Assertion)
244
+ end #: AST::Annotations::Assertion?
245
+ end
246
+
247
+ current_class_module_decl!.members << AST::Members::RubyAttr.new(node, comment, assertion)
248
+
249
+ return
250
+ end
251
+ when :public, :private
252
+ case node.receiver
253
+ when nil, Prism::SelfNode
254
+ if node.arguments && node.arguments.arguments.size > 0
255
+ if node.name == :public
256
+ push_visibility(:public) { super }
257
+ end
258
+
259
+ if node.name == :private
260
+ push_visibility(:private) { super }
261
+ end
262
+
263
+ return
264
+ else
265
+ if node.name == :public
266
+ current_class_module_decl!.members << AST::Members::RubyPublic.new(node)
267
+ return
268
+ end
269
+
270
+ if node.name == :private
271
+ current_class_module_decl!.members << AST::Members::RubyPrivate.new(node)
272
+ return
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ super
279
+ end
280
+
281
+ # @rbs new_visibility: RBS::AST::Members::visibility?
282
+ # @rbs block: ^() -> void
283
+ # @rbs returns void
284
+ def push_visibility(new_visibility, &block)
285
+ old_visibility = current_visibility
286
+
287
+ begin
288
+ @current_visibility = new_visibility
289
+ yield
290
+ ensure
291
+ @current_visibility = old_visibility
292
+ end
293
+ end
294
+
295
+ # @rbs node: Node
296
+ # @rbs returns bool
297
+ def ignored_node?(node)
298
+ if comment = comments.fetch(node.location.start_line - 1, nil)
299
+ comment.annotations.any? { _1.is_a?(AST::Annotations::Skip) }
300
+ else
301
+ false
302
+ end
303
+ end
304
+
305
+ # Fetch Application annotation which is associated to `node`
306
+ #
307
+ # The application annotation is removed from `comments`.
308
+ #
309
+ # @rbs node: Node
310
+ # @rbs returns AST::Annotations::Application?
311
+ def application_annotation(node)
312
+ comment_line, app_comment = comments.find do |_, comment|
313
+ comment.line_range.begin == node.location.end_line
314
+ end
315
+
316
+ if app_comment && comment_line
317
+ comments.delete(comment_line)
318
+ app_comment.annotations.find do |annotation|
319
+ annotation.is_a?(AST::Annotations::Application)
320
+ end #: AST::Annotations::Application?
321
+ end
322
+ end
323
+
324
+ # Fetch Assertion annotation which is associated to `node`
325
+ #
326
+ # The assertion annotation is removed from `comments`.
327
+ #
328
+ # @rbs node: Node | Location
329
+ # @rbs returns AST::Annotations::Assertion?
330
+ def assertion_annotation(node)
331
+ if node.is_a?(Prism::Location)
332
+ location = node
333
+ else
334
+ location = node.location
335
+ end
336
+ comment_line, app_comment = comments.find do |_, comment|
337
+ comment.line_range.begin == location.end_line
338
+ end
339
+
340
+ if app_comment && comment_line
341
+ comments.delete(comment_line)
342
+ app_comment.annotations.find do |annotation|
343
+ annotation.is_a?(AST::Annotations::Assertion)
344
+ end #: AST::Annotations::Assertion?
345
+ end
346
+ end
347
+
348
+ # @rbs override
349
+ def visit_constant_write_node(node)
350
+ return if ignored_node?(node)
351
+
352
+ comment = comments.delete(node.location.start_line - 1)
353
+ assertion = assertion_annotation(node)
354
+
355
+ decl = AST::Declarations::ConstantDecl.new(node, comment, assertion)
356
+ push_class_module_decl(decl)
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBS
4
+ module Inline
5
+ VERSION = "0.1.0"
6
+ end
7
+ end