t-ruby 0.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.
@@ -0,0 +1,474 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module TRuby
7
+ # API Documentation Generator
8
+ class DocGenerator
9
+ attr_reader :docs, :config
10
+
11
+ def initialize(config = nil)
12
+ @config = config || Config.new
13
+ @docs = {
14
+ types: {},
15
+ interfaces: {},
16
+ functions: {},
17
+ modules: {}
18
+ }
19
+ @parser = nil
20
+ end
21
+
22
+ # Generate documentation from source files
23
+ def generate(paths, output_dir: "docs")
24
+ puts "Generating T-Ruby API Documentation..."
25
+
26
+ # Parse all files
27
+ files = collect_files(paths)
28
+ files.each { |file| parse_file(file) }
29
+
30
+ # Generate output
31
+ FileUtils.mkdir_p(output_dir)
32
+
33
+ generate_index(output_dir)
34
+ generate_type_docs(output_dir)
35
+ generate_interface_docs(output_dir)
36
+ generate_function_docs(output_dir)
37
+ generate_module_docs(output_dir)
38
+ generate_search_index(output_dir)
39
+
40
+ puts "Documentation generated in #{output_dir}/"
41
+ @docs
42
+ end
43
+
44
+ # Generate single-file markdown documentation
45
+ def generate_markdown(paths, output_path: "API.md")
46
+ files = collect_files(paths)
47
+ files.each { |file| parse_file(file) }
48
+
49
+ md = []
50
+ md << "# T-Ruby API Documentation"
51
+ md << ""
52
+ md << "**Generated:** #{Time.now}"
53
+ md << ""
54
+ md << "## Table of Contents"
55
+ md << ""
56
+ md << "- [Types](#types)"
57
+ md << "- [Interfaces](#interfaces)"
58
+ md << "- [Functions](#functions)"
59
+ md << ""
60
+
61
+ # Types section
62
+ md << "## Types"
63
+ md << ""
64
+ @docs[:types].each do |name, info|
65
+ md << "### `#{name}`"
66
+ md << ""
67
+ md << "```typescript"
68
+ md << "type #{name} = #{info[:definition]}"
69
+ md << "```"
70
+ md << ""
71
+ md << info[:description] if info[:description]
72
+ md << ""
73
+ md << "**Source:** `#{info[:source]}`" if info[:source]
74
+ md << ""
75
+ end
76
+
77
+ # Interfaces section
78
+ md << "## Interfaces"
79
+ md << ""
80
+ @docs[:interfaces].each do |name, info|
81
+ md << "### `#{name}`"
82
+ md << ""
83
+ md << info[:description] if info[:description]
84
+ md << ""
85
+
86
+ if info[:type_params]&.any?
87
+ md << "**Type Parameters:** `<#{info[:type_params].join(', ')}>`"
88
+ md << ""
89
+ end
90
+
91
+ if info[:members]&.any?
92
+ md << "#### Members"
93
+ md << ""
94
+ md << "| Name | Type | Description |"
95
+ md << "|------|------|-------------|"
96
+ info[:members].each do |member|
97
+ md << "| `#{member[:name]}` | `#{member[:type]}` | #{member[:description] || '-'} |"
98
+ end
99
+ md << ""
100
+ end
101
+
102
+ md << "**Source:** `#{info[:source]}`" if info[:source]
103
+ md << ""
104
+ end
105
+
106
+ # Functions section
107
+ md << "## Functions"
108
+ md << ""
109
+ @docs[:functions].each do |name, info|
110
+ md << "### `#{name}`"
111
+ md << ""
112
+ md << info[:description] if info[:description]
113
+ md << ""
114
+
115
+ # Signature
116
+ params = info[:params]&.map { |p| "#{p[:name]}: #{p[:type]}" }&.join(", ") || ""
117
+ type_params = info[:type_params]&.any? ? "<#{info[:type_params].join(', ')}>" : ""
118
+ md << "```ruby"
119
+ md << "def #{name}#{type_params}(#{params}): #{info[:return_type] || 'void'}"
120
+ md << "```"
121
+ md << ""
122
+
123
+ if info[:params]&.any?
124
+ md << "#### Parameters"
125
+ md << ""
126
+ md << "| Name | Type | Description |"
127
+ md << "|------|------|-------------|"
128
+ info[:params].each do |param|
129
+ md << "| `#{param[:name]}` | `#{param[:type]}` | #{param[:description] || '-'} |"
130
+ end
131
+ md << ""
132
+ end
133
+
134
+ md << "**Returns:** `#{info[:return_type]}`" if info[:return_type]
135
+ md << ""
136
+ md << "**Source:** `#{info[:source]}`" if info[:source]
137
+ md << ""
138
+ end
139
+
140
+ File.write(output_path, md.join("\n"))
141
+ puts "Documentation generated: #{output_path}"
142
+ end
143
+
144
+ # Generate JSON documentation
145
+ def generate_json(paths, output_path: "api.json")
146
+ files = collect_files(paths)
147
+ files.each { |file| parse_file(file) }
148
+
149
+ File.write(output_path, JSON.pretty_generate({
150
+ generated_at: Time.now.iso8601,
151
+ version: TRuby::VERSION,
152
+ types: @docs[:types],
153
+ interfaces: @docs[:interfaces],
154
+ functions: @docs[:functions],
155
+ modules: @docs[:modules]
156
+ }))
157
+
158
+ puts "JSON documentation generated: #{output_path}"
159
+ end
160
+
161
+ private
162
+
163
+ def parser
164
+ @parser ||= Parser.new("")
165
+ end
166
+
167
+ def collect_files(paths)
168
+ files = []
169
+ paths.each do |path|
170
+ if File.directory?(path)
171
+ files.concat(Dir.glob(File.join(path, "**", "*.trb")))
172
+ files.concat(Dir.glob(File.join(path, "**", "*.d.trb")))
173
+ elsif File.file?(path)
174
+ files << path
175
+ end
176
+ end
177
+ files.uniq
178
+ end
179
+
180
+ def parse_file(file_path)
181
+ content = File.read(file_path)
182
+ relative_path = file_path.sub("#{Dir.pwd}/", "")
183
+
184
+ # Extract documentation comments
185
+ doc_comments = extract_doc_comments(content)
186
+
187
+ # Parse type aliases
188
+ content.scan(/^type\s+(\w+)(?:<([^>]+)>)?\s*=\s*(.+)$/) do |name, type_params, definition|
189
+ @docs[:types][name] = {
190
+ name: name,
191
+ type_params: type_params&.split(/\s*,\s*/),
192
+ definition: definition.strip,
193
+ description: doc_comments["type:#{name}"],
194
+ source: relative_path
195
+ }
196
+ end
197
+
198
+ # Parse interfaces
199
+ parse_interfaces(content, relative_path, doc_comments)
200
+
201
+ # Parse functions
202
+ parse_functions(content, relative_path, doc_comments)
203
+ end
204
+
205
+ def extract_doc_comments(content)
206
+ comments = {}
207
+ current_comment = []
208
+ current_target = nil
209
+
210
+ content.each_line do |line|
211
+ if line =~ /^\s*#\s*@doc\s+(\w+):(\w+)/
212
+ current_target = "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
213
+ current_comment = []
214
+ elsif line =~ /^\s*#\s*(.+)/ && current_target
215
+ current_comment << Regexp.last_match(1).strip
216
+ elsif line !~ /^\s*#/ && current_target
217
+ comments[current_target] = current_comment.join(" ")
218
+ current_target = nil
219
+ current_comment = []
220
+ end
221
+
222
+ # Also check inline comments before definitions
223
+ if line =~ /^\s*#\s*(.+)$/
224
+ current_comment << Regexp.last_match(1).strip
225
+ elsif line =~ /^(type|interface|def)\s+(\w+)/
226
+ type = Regexp.last_match(1)
227
+ name = Regexp.last_match(2)
228
+ unless current_comment.empty?
229
+ comments["#{type}:#{name}"] = current_comment.join(" ")
230
+ end
231
+ current_comment = []
232
+ end
233
+ end
234
+
235
+ comments
236
+ end
237
+
238
+ def parse_interfaces(content, source, doc_comments)
239
+ # Match interface blocks
240
+ content.scan(/interface\s+(\w+)(?:<([^>]+)>)?\s*\n((?:(?!^end).)*?)^end/m) do |name, type_params, body|
241
+ members = []
242
+
243
+ body.scan(/^\s*(\w+[\?\!]?)\s*:\s*(.+)$/) do |member_name, member_type|
244
+ members << {
245
+ name: member_name,
246
+ type: member_type.strip,
247
+ description: doc_comments["member:#{name}.#{member_name}"]
248
+ }
249
+ end
250
+
251
+ @docs[:interfaces][name] = {
252
+ name: name,
253
+ type_params: type_params&.split(/\s*,\s*/),
254
+ members: members,
255
+ description: doc_comments["interface:#{name}"],
256
+ source: source
257
+ }
258
+ end
259
+ end
260
+
261
+ def parse_functions(content, source, doc_comments)
262
+ # Match function definitions
263
+ content.scan(/def\s+(\w+[\?\!]?)(?:<([^>]+)>)?\s*\(([^)]*)\)(?:\s*:\s*(.+?))?(?:\n|$)/) do |name, type_params, params_str, return_type|
264
+ params = []
265
+
266
+ params_str.scan(/(\w+)\s*:\s*([^,]+)/) do |param_name, param_type|
267
+ params << {
268
+ name: param_name,
269
+ type: param_type.strip,
270
+ description: doc_comments["param:#{name}.#{param_name}"]
271
+ }
272
+ end
273
+
274
+ @docs[:functions][name] = {
275
+ name: name,
276
+ type_params: type_params&.split(/\s*,\s*/),
277
+ params: params,
278
+ return_type: return_type&.strip,
279
+ description: doc_comments["def:#{name}"],
280
+ source: source
281
+ }
282
+ end
283
+ end
284
+
285
+ def generate_index(output_dir)
286
+ html = <<~HTML
287
+ <!DOCTYPE html>
288
+ <html>
289
+ <head>
290
+ <title>T-Ruby API Documentation</title>
291
+ <style>
292
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
293
+ h1 { color: #cc342d; }
294
+ .section { margin: 20px 0; }
295
+ a { color: #0366d6; text-decoration: none; }
296
+ a:hover { text-decoration: underline; }
297
+ .item { padding: 5px 0; }
298
+ code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; }
299
+ </style>
300
+ </head>
301
+ <body>
302
+ <h1>T-Ruby API Documentation</h1>
303
+ <p>Generated: #{Time.now}</p>
304
+
305
+ <div class="section">
306
+ <h2>Types (#{@docs[:types].size})</h2>
307
+ #{@docs[:types].keys.sort.map { |name| "<div class='item'><a href='types/#{name}.html'><code>#{name}</code></a></div>" }.join}
308
+ </div>
309
+
310
+ <div class="section">
311
+ <h2>Interfaces (#{@docs[:interfaces].size})</h2>
312
+ #{@docs[:interfaces].keys.sort.map { |name| "<div class='item'><a href='interfaces/#{name}.html'><code>#{name}</code></a></div>" }.join}
313
+ </div>
314
+
315
+ <div class="section">
316
+ <h2>Functions (#{@docs[:functions].size})</h2>
317
+ #{@docs[:functions].keys.sort.map { |name| "<div class='item'><a href='functions/#{name}.html'><code>#{name}</code></a></div>" }.join}
318
+ </div>
319
+ </body>
320
+ </html>
321
+ HTML
322
+
323
+ File.write(File.join(output_dir, "index.html"), html)
324
+ end
325
+
326
+ def generate_type_docs(output_dir)
327
+ types_dir = File.join(output_dir, "types")
328
+ FileUtils.mkdir_p(types_dir)
329
+
330
+ @docs[:types].each do |name, info|
331
+ html = generate_type_html(name, info)
332
+ File.write(File.join(types_dir, "#{name}.html"), html)
333
+ end
334
+ end
335
+
336
+ def generate_interface_docs(output_dir)
337
+ interfaces_dir = File.join(output_dir, "interfaces")
338
+ FileUtils.mkdir_p(interfaces_dir)
339
+
340
+ @docs[:interfaces].each do |name, info|
341
+ html = generate_interface_html(name, info)
342
+ File.write(File.join(interfaces_dir, "#{name}.html"), html)
343
+ end
344
+ end
345
+
346
+ def generate_function_docs(output_dir)
347
+ functions_dir = File.join(output_dir, "functions")
348
+ FileUtils.mkdir_p(functions_dir)
349
+
350
+ @docs[:functions].each do |name, info|
351
+ html = generate_function_html(name, info)
352
+ File.write(File.join(functions_dir, "#{name}.html"), html)
353
+ end
354
+ end
355
+
356
+ def generate_module_docs(output_dir)
357
+ # Placeholder for module documentation
358
+ end
359
+
360
+ def generate_search_index(output_dir)
361
+ search_data = []
362
+
363
+ @docs[:types].each do |name, info|
364
+ search_data << { type: "type", name: name, url: "types/#{name}.html" }
365
+ end
366
+
367
+ @docs[:interfaces].each do |name, info|
368
+ search_data << { type: "interface", name: name, url: "interfaces/#{name}.html" }
369
+ end
370
+
371
+ @docs[:functions].each do |name, info|
372
+ search_data << { type: "function", name: name, url: "functions/#{name}.html" }
373
+ end
374
+
375
+ File.write(File.join(output_dir, "search-index.json"), JSON.generate(search_data))
376
+ end
377
+
378
+ def generate_type_html(name, info)
379
+ <<~HTML
380
+ <!DOCTYPE html>
381
+ <html>
382
+ <head>
383
+ <title>#{name} - T-Ruby API</title>
384
+ <style>
385
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
386
+ h1 { color: #cc342d; }
387
+ pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }
388
+ .meta { color: #6a737d; font-size: 14px; }
389
+ a { color: #0366d6; }
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <a href="../index.html">← Back to Index</a>
394
+ <h1>type #{name}</h1>
395
+ #{info[:description] ? "<p>#{info[:description]}</p>" : ""}
396
+ <pre>type #{name}#{info[:type_params] ? "<#{info[:type_params].join(', ')}>" : ""} = #{info[:definition]}</pre>
397
+ <p class="meta">Source: <code>#{info[:source]}</code></p>
398
+ </body>
399
+ </html>
400
+ HTML
401
+ end
402
+
403
+ def generate_interface_html(name, info)
404
+ members_html = info[:members]&.map do |m|
405
+ "<tr><td><code>#{m[:name]}</code></td><td><code>#{m[:type]}</code></td><td>#{m[:description] || '-'}</td></tr>"
406
+ end&.join || ""
407
+
408
+ <<~HTML
409
+ <!DOCTYPE html>
410
+ <html>
411
+ <head>
412
+ <title>#{name} - T-Ruby API</title>
413
+ <style>
414
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
415
+ h1 { color: #cc342d; }
416
+ table { border-collapse: collapse; width: 100%; }
417
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
418
+ th { background: #f6f8fa; }
419
+ pre { background: #f6f8fa; padding: 16px; border-radius: 6px; }
420
+ .meta { color: #6a737d; font-size: 14px; }
421
+ a { color: #0366d6; }
422
+ </style>
423
+ </head>
424
+ <body>
425
+ <a href="../index.html">← Back to Index</a>
426
+ <h1>interface #{name}#{info[:type_params] ? "&lt;#{info[:type_params].join(', ')}&gt;" : ""}</h1>
427
+ #{info[:description] ? "<p>#{info[:description]}</p>" : ""}
428
+ #{"<h2>Members</h2><table><tr><th>Name</th><th>Type</th><th>Description</th></tr>#{members_html}</table>" unless members_html.empty?}
429
+ <p class="meta">Source: <code>#{info[:source]}</code></p>
430
+ </body>
431
+ </html>
432
+ HTML
433
+ end
434
+
435
+ def generate_function_html(name, info)
436
+ params_html = info[:params]&.map do |p|
437
+ "<tr><td><code>#{p[:name]}</code></td><td><code>#{p[:type]}</code></td><td>#{p[:description] || '-'}</td></tr>"
438
+ end&.join || ""
439
+
440
+ params_sig = info[:params]&.map { |p| "#{p[:name]}: #{p[:type]}" }&.join(", ") || ""
441
+ type_params = info[:type_params]&.any? ? "<#{info[:type_params].join(', ')}>" : ""
442
+
443
+ <<~HTML
444
+ <!DOCTYPE html>
445
+ <html>
446
+ <head>
447
+ <title>#{name} - T-Ruby API</title>
448
+ <style>
449
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
450
+ h1 { color: #cc342d; }
451
+ table { border-collapse: collapse; width: 100%; }
452
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
453
+ th { background: #f6f8fa; }
454
+ pre { background: #f6f8fa; padding: 16px; border-radius: 6px; }
455
+ .meta { color: #6a737d; font-size: 14px; }
456
+ a { color: #0366d6; }
457
+ </style>
458
+ </head>
459
+ <body>
460
+ <a href="../index.html">← Back to Index</a>
461
+ <h1>#{name}</h1>
462
+ #{info[:description] ? "<p>#{info[:description]}</p>" : ""}
463
+ <h2>Signature</h2>
464
+ <pre>def #{name}#{type_params}(#{params_sig}): #{info[:return_type] || 'void'}</pre>
465
+ #{"<h2>Parameters</h2><table><tr><th>Name</th><th>Type</th><th>Description</th></tr>#{params_html}</table>" unless params_html.empty?}
466
+ <h2>Returns</h2>
467
+ <p><code>#{info[:return_type] || 'void'}</code></p>
468
+ <p class="meta">Source: <code>#{info[:source]}</code></p>
469
+ </body>
470
+ </html>
471
+ HTML
472
+ end
473
+ end
474
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ class ErrorHandler
5
+ VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
6
+
7
+ def initialize(source)
8
+ @source = source
9
+ @lines = source.split("\n")
10
+ @errors = []
11
+ @functions = {}
12
+ end
13
+
14
+ def check
15
+ @errors = []
16
+ @functions = {}
17
+ @type_aliases = {}
18
+ @interfaces = {}
19
+
20
+ check_type_alias_errors
21
+ check_interface_errors
22
+ check_syntax_errors
23
+ check_type_validation
24
+ check_duplicate_definitions
25
+
26
+ @errors
27
+ end
28
+
29
+ private
30
+
31
+ def check_interface_errors
32
+ @lines.each_with_index do |line, idx|
33
+ next unless line.match?(/^\s*interface\s+\w+/)
34
+
35
+ match = line.match(/^\s*interface\s+(\w+)/)
36
+ next unless match
37
+
38
+ interface_name = match[1]
39
+
40
+ if @interfaces[interface_name]
41
+ @errors << "Line #{idx + 1}: Interface '#{interface_name}' is already defined at line #{@interfaces[interface_name]}"
42
+ else
43
+ @interfaces[interface_name] = idx + 1
44
+ end
45
+ end
46
+ end
47
+
48
+ def check_type_alias_errors
49
+ @lines.each_with_index do |line, idx|
50
+ next unless line.match?(/^\s*type\s+\w+/)
51
+
52
+ match = line.match(/^\s*type\s+(\w+)\s*=\s*(.+)$/)
53
+ next unless match
54
+
55
+ alias_name = match[1]
56
+
57
+ if @type_aliases[alias_name]
58
+ @errors << "Line #{idx + 1}: Type alias '#{alias_name}' is already defined at line #{@type_aliases[alias_name]}"
59
+ else
60
+ @type_aliases[alias_name] = idx + 1
61
+ end
62
+ end
63
+ end
64
+
65
+ def check_syntax_errors
66
+ @lines.each_with_index do |line, idx|
67
+ next unless line.match?(/^\s*def\s+/)
68
+
69
+ # Check for unclosed parenthesis
70
+ if line.match?(/def\s+\w+\([^)]*$/) && !@lines[idx + 1..].any? { |l| l.match?(/\)/) }
71
+ @errors << "Line #{idx + 1}: Potential unclosed parenthesis in function definition"
72
+ end
73
+
74
+ # Check for invalid parameter syntax (e.g., "def test(: String)")
75
+ if line.match?(/def\s+\w+\(\s*:\s*\w+/)
76
+ @errors << "Line #{idx + 1}: Invalid parameter syntax - parameter name missing"
77
+ end
78
+ end
79
+ end
80
+
81
+ def check_type_validation
82
+ @lines.each_with_index do |line, idx|
83
+ next unless line.match?(/^\s*def\s+/)
84
+
85
+ # Extract types from function definition
86
+ match = line.match(/def\s+\w+\s*\((.*?)\)\s*(?::\s*(\w+))?/)
87
+ next unless match
88
+
89
+ params_str = match[1]
90
+ return_type = match[2]
91
+
92
+ # Check return type
93
+ if return_type && !VALID_TYPES.include?(return_type) && !@type_aliases.key?(return_type)
94
+ @errors << "Line #{idx + 1}: Unknown return type '#{return_type}'"
95
+ end
96
+
97
+ # Check parameter types
98
+ check_parameter_types(params_str, idx)
99
+ end
100
+ end
101
+
102
+ def check_parameter_types(params_str, line_idx)
103
+ return if params_str.empty?
104
+
105
+ param_list = params_str.split(",").map(&:strip)
106
+ param_list.each do |param|
107
+ match = param.match(/^(\w+)(?::\s*(\w+))?$/)
108
+ next unless match
109
+
110
+ param_type = match[2]
111
+ next unless param_type
112
+ next if VALID_TYPES.include?(param_type) || @type_aliases.key?(param_type)
113
+
114
+ @errors << "Line #{line_idx + 1}: Unknown parameter type '#{param_type}'"
115
+ end
116
+ end
117
+
118
+ def check_duplicate_definitions
119
+ @lines.each_with_index do |line, idx|
120
+ next unless line.match?(/^\s*def\s+(\w+)/)
121
+
122
+ func_name = line.match(/def\s+(\w+)/)[1]
123
+
124
+ if @functions[func_name]
125
+ @errors << "Line #{idx + 1}: Function '#{func_name}' is already defined at line #{@functions[func_name]}"
126
+ else
127
+ @functions[func_name] = idx + 1
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ class GenericTypeParser
5
+ def initialize(type_string)
6
+ @type_string = type_string.strip
7
+ end
8
+
9
+ def parse
10
+ if @type_string.include?("<") && @type_string.include?(">")
11
+ parse_generic
12
+ else
13
+ { type: :simple, value: @type_string }
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def parse_generic
20
+ # Match: BaseName<Params>
21
+ match = @type_string.match(/^(\w+)<(.+)>$/)
22
+ return { type: :simple, value: @type_string } unless match
23
+
24
+ base_name = match[1]
25
+ params_str = match[2]
26
+
27
+ # Parse parameters, handling nested generics
28
+ params = parse_params(params_str)
29
+
30
+ {
31
+ type: :generic,
32
+ base: base_name,
33
+ params: params
34
+ }
35
+ end
36
+
37
+ def parse_params(params_str)
38
+ # Simple comma-based splitting (doesn't handle nested generics fully)
39
+ # For nested generics like Array<Array<String>>, we need careful parsing
40
+ params = []
41
+ current = ""
42
+ depth = 0
43
+
44
+ params_str.each_char do |char|
45
+ case char
46
+ when "<"
47
+ depth += 1
48
+ current += char
49
+ when ">"
50
+ depth -= 1
51
+ current += char
52
+ when ","
53
+ if depth == 0
54
+ params << current.strip
55
+ current = ""
56
+ else
57
+ current += char
58
+ end
59
+ else
60
+ current += char
61
+ end
62
+ end
63
+
64
+ params << current.strip if current.length > 0
65
+ params
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ class IntersectionTypeParser
5
+ def initialize(type_string)
6
+ @type_string = type_string.strip
7
+ end
8
+
9
+ def parse
10
+ if @type_string.include?("&")
11
+ parse_intersection
12
+ else
13
+ { type: :simple, value: @type_string }
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def parse_intersection
20
+ members = @type_string.split("&").map { |m| m.strip }.compact
21
+
22
+ {
23
+ type: :intersection,
24
+ members: members,
25
+ has_duplicates: members.length != members.uniq.length,
26
+ unique_members: members.uniq
27
+ }
28
+ end
29
+ end
30
+ end