rbs-inline 0.1.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.
@@ -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