t-ruby 0.0.38 → 0.0.39

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc0d0297e719aa0061465983334438eeb0ce5d4230e5d1f363cbfe5d3def7b66
4
- data.tar.gz: 898d406d4ce3b0a415312ee2473fff6e7983accd0227aed7030ba5eb396c868f
3
+ metadata.gz: 64bc7ea0ab44e83d6580d07227fee508ae3b5e53bc0e6b19b8164857eaf31376
4
+ data.tar.gz: 01da8585df97030974563634d8212058a3d3325ca9bc05188ae12ccf1cdc6770
5
5
  SHA512:
6
- metadata.gz: '00942ff35f9b6dc3824959cf42e72d35731e41f34156cb063eccc21c18686382fc1faf7b17e3e91caeb8910f2c5f6663fcae30877660e7c09e1e9d90f735ac97'
7
- data.tar.gz: cd5893ac01d1c9e4e969e20739a1e21b2bcf174b5aac6ca54ab0cb52a0f41d411bff883b204b1669de513399e801533e83beba2f46b9e8cc4b1c6cb5aa17ba46
6
+ metadata.gz: b0bf9cd9518368e5ef256dd1851285fbbb78404b16c837346ecb286bfde37d2b8f8f5738782629efb5507fb7419eaea197726ae85f1ccb9481c09abc26d80a7f
7
+ data.tar.gz: 67de07897cec9ef86c5df0798c4f2d44e1d54da0d8bfe799386eb544c28a4c0d5595f17dcc0c0bbc4236a44c17e76f1283fc75a9cd4452cee40f34daa86ef609
data/LICENSE CHANGED
@@ -1,21 +1,25 @@
1
- MIT License
1
+ BSD 2-Clause License
2
2
 
3
- Copyright (c) 2025 type-ruby
3
+ Copyright (c) 2025 Yonghyun Kim and Type-Ruby contributors
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
11
7
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
14
10
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -255,10 +255,9 @@ watch:
255
255
  ## Links
256
256
 
257
257
  **IDE Support**
258
- - [VS Code Extension (and Cursor)](./docs/vscode/en/getting-started.md)
259
- - [JetBrains Plugin](./docs/jetbrains/en/getting-started.md)
260
- - [Vim Setup](./docs/vim/en/getting-started.md)
261
- - [Neovim Setup](./docs/neovim/en/getting-started.md)
258
+ - [VS Code Extension (and Cursor)](https://github.com/type-ruby/t-ruby-vscode)
259
+ - [JetBrains Plugin](https://github.com/type-ruby/t-ruby-jetbrains)
260
+ - [Vim / Neovim](https://github.com/type-ruby/t-ruby-vim)
262
261
 
263
262
  **Guides**
264
263
  - [Syntax Highlighting](./docs/syntax-highlighting/en/guide.md)
@@ -0,0 +1,511 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # ASTTypeInferrer - TypeScript 스타일 정적 타입 추론 엔진
5
+ # IR 노드를 순회하면서 타입을 추론하고 캐싱
6
+ class ASTTypeInferrer
7
+ # 리터럴 타입 매핑
8
+ LITERAL_TYPE_MAP = {
9
+ string: "String",
10
+ integer: "Integer",
11
+ float: "Float",
12
+ boolean: "bool",
13
+ symbol: "Symbol",
14
+ nil: "nil",
15
+ array: "Array[untyped]",
16
+ hash: "Hash[untyped, untyped]",
17
+ }.freeze
18
+
19
+ # 산술 연산자 규칙 (피연산자 타입 → 결과 타입)
20
+ ARITHMETIC_OPS = %w[+ - * / % **].freeze
21
+ COMPARISON_OPS = %w[== != < > <= >= <=>].freeze
22
+ LOGICAL_OPS = %w[&& ||].freeze
23
+
24
+ # 내장 메서드 반환 타입
25
+ BUILTIN_METHODS = {
26
+ # String 메서드
27
+ %w[String upcase] => "String",
28
+ %w[String downcase] => "String",
29
+ %w[String capitalize] => "String",
30
+ %w[String reverse] => "String",
31
+ %w[String strip] => "String",
32
+ %w[String chomp] => "String",
33
+ %w[String chop] => "String",
34
+ %w[String gsub] => "String",
35
+ %w[String sub] => "String",
36
+ %w[String tr] => "String",
37
+ %w[String to_s] => "String",
38
+ %w[String to_str] => "String",
39
+ %w[String to_sym] => "Symbol",
40
+ %w[String to_i] => "Integer",
41
+ %w[String to_f] => "Float",
42
+ %w[String length] => "Integer",
43
+ %w[String size] => "Integer",
44
+ %w[String bytesize] => "Integer",
45
+ %w[String empty?] => "bool",
46
+ %w[String include?] => "bool",
47
+ %w[String start_with?] => "bool",
48
+ %w[String end_with?] => "bool",
49
+ %w[String match?] => "bool",
50
+ %w[String split] => "Array[String]",
51
+ %w[String chars] => "Array[String]",
52
+ %w[String bytes] => "Array[Integer]",
53
+ %w[String lines] => "Array[String]",
54
+
55
+ # Integer 메서드
56
+ %w[Integer to_s] => "String",
57
+ %w[Integer to_i] => "Integer",
58
+ %w[Integer to_f] => "Float",
59
+ %w[Integer abs] => "Integer",
60
+ %w[Integer even?] => "bool",
61
+ %w[Integer odd?] => "bool",
62
+ %w[Integer zero?] => "bool",
63
+ %w[Integer positive?] => "bool",
64
+ %w[Integer negative?] => "bool",
65
+ %w[Integer times] => "Integer",
66
+ %w[Integer upto] => "Enumerator[Integer]",
67
+ %w[Integer downto] => "Enumerator[Integer]",
68
+
69
+ # Float 메서드
70
+ %w[Float to_s] => "String",
71
+ %w[Float to_i] => "Integer",
72
+ %w[Float to_f] => "Float",
73
+ %w[Float abs] => "Float",
74
+ %w[Float ceil] => "Integer",
75
+ %w[Float floor] => "Integer",
76
+ %w[Float round] => "Integer",
77
+ %w[Float truncate] => "Integer",
78
+ %w[Float nan?] => "bool",
79
+ %w[Float infinite?] => "Integer?",
80
+ %w[Float finite?] => "bool",
81
+ %w[Float zero?] => "bool",
82
+ %w[Float positive?] => "bool",
83
+ %w[Float negative?] => "bool",
84
+
85
+ # Array 메서드
86
+ %w[Array length] => "Integer",
87
+ %w[Array size] => "Integer",
88
+ %w[Array count] => "Integer",
89
+ %w[Array empty?] => "bool",
90
+ %w[Array any?] => "bool",
91
+ %w[Array all?] => "bool",
92
+ %w[Array none?] => "bool",
93
+ %w[Array include?] => "bool",
94
+ %w[Array reverse] => "Array[untyped]",
95
+ %w[Array sort] => "Array[untyped]",
96
+ %w[Array uniq] => "Array[untyped]",
97
+ %w[Array compact] => "Array[untyped]",
98
+ %w[Array flatten] => "Array[untyped]",
99
+ %w[Array join] => "String",
100
+ %w[Array to_s] => "String",
101
+ %w[Array to_a] => "Array[untyped]",
102
+
103
+ # Hash 메서드
104
+ %w[Hash length] => "Integer",
105
+ %w[Hash size] => "Integer",
106
+ %w[Hash empty?] => "bool",
107
+ %w[Hash key?] => "bool",
108
+ %w[Hash has_key?] => "bool",
109
+ %w[Hash value?] => "bool",
110
+ %w[Hash has_value?] => "bool",
111
+ %w[Hash include?] => "bool",
112
+ %w[Hash keys] => "Array[untyped]",
113
+ %w[Hash values] => "Array[untyped]",
114
+ %w[Hash to_s] => "String",
115
+ %w[Hash to_a] => "Array[untyped]",
116
+ %w[Hash to_h] => "Hash[untyped, untyped]",
117
+
118
+ # Object 메서드 (모든 타입에 적용)
119
+ %w[Object to_s] => "String",
120
+ %w[Object inspect] => "String",
121
+ %w[Object class] => "Class",
122
+ %w[Object is_a?] => "bool",
123
+ %w[Object kind_of?] => "bool",
124
+ %w[Object instance_of?] => "bool",
125
+ %w[Object respond_to?] => "bool",
126
+ %w[Object nil?] => "bool",
127
+ %w[Object frozen?] => "bool",
128
+ %w[Object dup] => "untyped",
129
+ %w[Object clone] => "untyped",
130
+ %w[Object freeze] => "self",
131
+ %w[Object tap] => "self",
132
+ %w[Object then] => "untyped",
133
+ %w[Object yield_self] => "untyped",
134
+
135
+ # Symbol 메서드
136
+ %w[Symbol to_s] => "String",
137
+ %w[Symbol to_sym] => "Symbol",
138
+ %w[Symbol length] => "Integer",
139
+ %w[Symbol size] => "Integer",
140
+ %w[Symbol empty?] => "bool",
141
+ }.freeze
142
+
143
+ attr_reader :type_cache
144
+
145
+ def initialize
146
+ @type_cache = {} # 노드 → 타입 캐시 (TypeScript의 지연 평가)
147
+ end
148
+
149
+ # 표현식 타입 추론
150
+ # @param node [IR::Node] IR 노드
151
+ # @param env [TypeEnv] 타입 환경
152
+ # @return [String, IR::TypeNode, nil] 추론된 타입
153
+ def infer_expression(node, env)
154
+ # 캐시 확인 (지연 평가)
155
+ cache_key = node.object_id
156
+ return @type_cache[cache_key] if @type_cache.key?(cache_key)
157
+
158
+ type = case node
159
+ when IR::Literal
160
+ infer_literal(node)
161
+ when IR::VariableRef
162
+ infer_variable_ref(node, env)
163
+ when IR::BinaryOp
164
+ infer_binary_op(node, env)
165
+ when IR::UnaryOp
166
+ infer_unary_op(node, env)
167
+ when IR::MethodCall
168
+ infer_method_call(node, env)
169
+ when IR::ArrayLiteral
170
+ infer_array_literal(node, env)
171
+ when IR::HashLiteral
172
+ infer_hash_literal(node, env)
173
+ when IR::Assignment
174
+ infer_assignment(node, env)
175
+ when IR::Conditional
176
+ infer_conditional(node, env)
177
+ when IR::Block
178
+ infer_block(node, env)
179
+ when IR::Return
180
+ infer_return(node, env)
181
+ when IR::RawCode
182
+ "untyped"
183
+ else
184
+ "untyped"
185
+ end
186
+
187
+ @type_cache[cache_key] = type
188
+ type
189
+ end
190
+
191
+ # 메서드 반환 타입 추론
192
+ # @param method_node [IR::MethodDef] 메서드 정의 IR
193
+ # @param class_env [TypeEnv, nil] 클래스 타입 환경
194
+ # @return [String, IR::TypeNode, nil] 추론된 반환 타입
195
+ def infer_method_return_type(method_node, class_env = nil)
196
+ return nil unless method_node.body
197
+
198
+ # 메서드 스코프 생성
199
+ env = TypeEnv.new(class_env)
200
+
201
+ # 파라미터 타입 등록
202
+ method_node.params.each do |param|
203
+ param_type = param.type_annotation&.to_rbs || "untyped"
204
+ env.define(param.name, param_type)
205
+ end
206
+
207
+ # 본문에서 반환 타입 수집
208
+ return_types, terminated = collect_return_types(method_node.body, env)
209
+
210
+ # 암묵적 반환값 추론 (마지막 표현식) - 종료되지 않은 경우만
211
+ unless terminated
212
+ implicit_return = infer_implicit_return(method_node.body, env)
213
+ return_types << implicit_return if implicit_return
214
+ end
215
+
216
+ # 타입 통합
217
+ unify_types(return_types)
218
+ end
219
+
220
+ private
221
+
222
+ # 리터럴 타입 추론
223
+ def infer_literal(node)
224
+ LITERAL_TYPE_MAP[node.literal_type] || "untyped"
225
+ end
226
+
227
+ # 변수 참조 타입 추론
228
+ def infer_variable_ref(node, env)
229
+ # 상수(클래스명)는 그 자체가 타입 (예: MyClass.new 호출 시)
230
+ if node.scope == :constant || node.name.match?(/^[A-Z]/)
231
+ return node.name
232
+ end
233
+
234
+ env.lookup(node.name) || "untyped"
235
+ end
236
+
237
+ # 이항 연산자 타입 추론
238
+ def infer_binary_op(node, env)
239
+ left_type = infer_expression(node.left, env)
240
+ right_type = infer_expression(node.right, env)
241
+ op = node.operator
242
+
243
+ # 비교 연산자는 항상 bool
244
+ return "bool" if COMPARISON_OPS.include?(op)
245
+
246
+ # 논리 연산자
247
+ if op == "&&"
248
+ # && 는 falsy면 왼쪽, truthy면 오른쪽 반환
249
+ return right_type # 단순화: 오른쪽 타입 반환
250
+ end
251
+
252
+ if op == "||"
253
+ # || 는 truthy면 왼쪽, falsy면 오른쪽 반환
254
+ return union_type(left_type, right_type)
255
+ end
256
+
257
+ # 산술 연산자
258
+ if ARITHMETIC_OPS.include?(op)
259
+ return infer_arithmetic_result(left_type, right_type, op)
260
+ end
261
+
262
+ "untyped"
263
+ end
264
+
265
+ # 산술 연산 결과 타입 추론
266
+ def infer_arithmetic_result(left_type, right_type, op)
267
+ left_base = base_type(left_type)
268
+ right_base = base_type(right_type)
269
+
270
+ # 문자열 연결
271
+ if op == "+" && (left_base == "String" || right_base == "String")
272
+ return "String"
273
+ end
274
+
275
+ # 숫자 연산
276
+ if numeric_type?(left_base) && numeric_type?(right_base)
277
+ # Float가 하나라도 있으면 Float
278
+ return "Float" if left_base == "Float" || right_base == "Float"
279
+
280
+ return "Integer"
281
+ end
282
+
283
+ # 배열 연결
284
+ if op == "+" && left_base.start_with?("Array")
285
+ return left_type
286
+ end
287
+
288
+ "untyped"
289
+ end
290
+
291
+ # 단항 연산자 타입 추론
292
+ def infer_unary_op(node, env)
293
+ operand_type = infer_expression(node.operand, env)
294
+
295
+ case node.operator
296
+ when "!"
297
+ "bool"
298
+ when "-"
299
+ operand_type
300
+ else
301
+ "untyped"
302
+ end
303
+ end
304
+
305
+ # 메서드 호출 타입 추론
306
+ def infer_method_call(node, env)
307
+ # receiver 타입 추론
308
+ receiver_type = if node.receiver
309
+ infer_expression(node.receiver, env)
310
+ else
311
+ "Object"
312
+ end
313
+
314
+ receiver_base = base_type(receiver_type)
315
+
316
+ # 내장 메서드 조회
317
+ method_key = [receiver_base, node.method_name]
318
+ if BUILTIN_METHODS.key?(method_key)
319
+ result = BUILTIN_METHODS[method_key]
320
+
321
+ # self 반환인 경우 receiver 타입 반환
322
+ return receiver_type if result == "self"
323
+
324
+ return result
325
+ end
326
+
327
+ # Object 메서드 fallback
328
+ object_key = ["Object", node.method_name]
329
+ if BUILTIN_METHODS.key?(object_key)
330
+ result = BUILTIN_METHODS[object_key]
331
+ return receiver_type if result == "self"
332
+
333
+ return result
334
+ end
335
+
336
+ # new 메서드는 클래스 인스턴스 반환
337
+ if node.method_name == "new" && receiver_base.match?(/^[A-Z]/)
338
+ return receiver_base
339
+ end
340
+
341
+ "untyped"
342
+ end
343
+
344
+ # 배열 리터럴 타입 추론
345
+ def infer_array_literal(node, env)
346
+ return "Array[untyped]" if node.elements.empty?
347
+
348
+ element_types = node.elements.map { |e| infer_expression(e, env) }
349
+ unified = unify_types(element_types)
350
+
351
+ "Array[#{unified}]"
352
+ end
353
+
354
+ # 해시 리터럴 타입 추론
355
+ def infer_hash_literal(node, env)
356
+ return "Hash[untyped, untyped]" if node.pairs.empty?
357
+
358
+ key_types = node.pairs.map { |p| infer_expression(p.key, env) }
359
+ value_types = node.pairs.map { |p| infer_expression(p.value, env) }
360
+
361
+ key_type = unify_types(key_types)
362
+ value_type = unify_types(value_types)
363
+
364
+ "Hash[#{key_type}, #{value_type}]"
365
+ end
366
+
367
+ # 대입 타입 추론 (변수 타입 업데이트 및 우변 타입 반환)
368
+ def infer_assignment(node, env)
369
+ value_type = infer_expression(node.value, env)
370
+
371
+ # 변수 타입 등록
372
+ target = node.target
373
+ if target.start_with?("@") && !target.start_with?("@@")
374
+ env.define_instance_var(target, value_type)
375
+ elsif target.start_with?("@@")
376
+ env.define_class_var(target, value_type)
377
+ else
378
+ env.define(target, value_type)
379
+ end
380
+
381
+ value_type
382
+ end
383
+
384
+ # 조건문 타입 추론 (then/else 브랜치 통합)
385
+ def infer_conditional(node, env)
386
+ then_type = infer_expression(node.then_branch, env) if node.then_branch
387
+ else_type = infer_expression(node.else_branch, env) if node.else_branch
388
+
389
+ types = [then_type, else_type].compact
390
+ return "nil" if types.empty?
391
+
392
+ unify_types(types)
393
+ end
394
+
395
+ # 블록 타입 추론 (마지막 문장의 타입)
396
+ def infer_block(node, env)
397
+ return "nil" if node.statements.empty?
398
+
399
+ # 마지막 문장 타입 반환 (Ruby의 암묵적 반환)
400
+ last_stmt = node.statements.last
401
+ infer_expression(last_stmt, env)
402
+ end
403
+
404
+ # return 문 타입 추론
405
+ def infer_return(node, env)
406
+ return "nil" unless node.value
407
+
408
+ infer_expression(node.value, env)
409
+ end
410
+
411
+ # 본문에서 모든 return 타입 수집
412
+ # @return [Array<(Array<String>, Boolean)>] [수집된 타입들, 종료 여부]
413
+ def collect_return_types(body, env)
414
+ types = []
415
+
416
+ terminated = collect_returns_recursive(body, env, types)
417
+
418
+ [types, terminated]
419
+ end
420
+
421
+ # @return [Boolean] true if this node terminates (contains unconditional return)
422
+ def collect_returns_recursive(node, env, types)
423
+ case node
424
+ when IR::Return
425
+ type = node.value ? infer_expression(node.value, env) : "nil"
426
+ types << type
427
+ true # return은 항상 실행 흐름 종료
428
+ when IR::Block
429
+ node.statements.each do |stmt|
430
+ terminated = collect_returns_recursive(stmt, env, types)
431
+ return true if terminated # return 이후 코드는 unreachable
432
+ end
433
+ false
434
+ when IR::Conditional
435
+ then_terminated = node.then_branch ? collect_returns_recursive(node.then_branch, env, types) : false
436
+ else_terminated = node.else_branch ? collect_returns_recursive(node.else_branch, env, types) : false
437
+ # 모든 분기가 종료되어야 조건문 전체가 종료됨
438
+ then_terminated && else_terminated
439
+ else
440
+ false
441
+ end
442
+ end
443
+
444
+ # 암묵적 반환값 추론 (마지막 표현식)
445
+ def infer_implicit_return(body, env)
446
+ case body
447
+ when IR::Block
448
+ return nil if body.statements.empty?
449
+
450
+ last_stmt = body.statements.last
451
+
452
+ # return 문이면 이미 수집됨
453
+ return nil if last_stmt.is_a?(IR::Return)
454
+
455
+ infer_expression(last_stmt, env)
456
+ else
457
+ infer_expression(body, env)
458
+ end
459
+ end
460
+
461
+ # 타입 통합 (여러 타입을 하나로)
462
+ def unify_types(types)
463
+ types = types.compact.uniq
464
+
465
+ return "nil" if types.empty?
466
+ return types.first if types.length == 1
467
+
468
+ # nil과 다른 타입이 있으면 nullable
469
+ if types.include?("nil") && types.length == 2
470
+ other = types.find { |t| t != "nil" }
471
+ return "#{other}?" if other
472
+ end
473
+
474
+ # 동일 기본 타입은 통합
475
+ base_types = types.map { |t| base_type(t) }.uniq
476
+ return types.first if base_types.length == 1
477
+
478
+ # Union 타입 생성
479
+ types.join(" | ")
480
+ end
481
+
482
+ # Union 타입 생성
483
+ def union_type(type1, type2)
484
+ return type2 if type1 == type2
485
+ return type2 if type1 == "nil"
486
+ return type1 if type2 == "nil"
487
+
488
+ "#{type1} | #{type2}"
489
+ end
490
+
491
+ # 기본 타입 추출 (Generic에서)
492
+ def base_type(type)
493
+ return "untyped" if type.nil?
494
+
495
+ type_str = type.is_a?(String) ? type : type.to_rbs
496
+
497
+ # Array[X] → Array
498
+ return ::Regexp.last_match(1) if type_str =~ /^(\w+)\[/
499
+
500
+ # Nullable X? → X
501
+ return type_str[0..-2] if type_str.end_with?("?")
502
+
503
+ type_str
504
+ end
505
+
506
+ # 숫자 타입인지 확인
507
+ def numeric_type?(type)
508
+ %w[Integer Float Numeric].include?(type)
509
+ end
510
+ end
511
+ end