Skip to content

Commit c6a520f

Browse files
authored
feat: add TypeScript-style array shorthand syntax T[] (#36)
* feat: add TypeScript-style array shorthand syntax `T[]` - Add `String[]` as alternative to `Array<String>` - Support nested arrays: `Integer[][]` → `Array[Array[Integer]]` - Support nullable arrays: `String[]?` → `(Array[String] | nil)` - Support arrays of nullable elements: `String?[]` → `Array[String?]` - Support union type arrays: `(String | Integer)[]` - Both syntaxes produce identical IR and RBS output - Add parse_postfix_type_operators for `[]` and `?` suffix handling - Add comprehensive unit tests (15 cases) and E2E tests (14 cases) - Update README examples to show new syntax * Update Gemfile.lock * fix: restore Gemfile.lock with listen gem for CI compatibility Ruby 4.0 skips listen gem installation, but CI runs on Ruby 3.x which requires it. Restore the original Gemfile.lock from main branch. * refactor: remove unused legacy type parsing code from IR::Builder TypeParser now handles all type parsing including: - Simple types, generics, array shorthand, union, intersection, function types Remove legacy fallback code that was never executed, improving code coverage.
1 parent 2df43ca commit c6a520f

File tree

8 files changed

+919
-82
lines changed

8 files changed

+919
-82
lines changed

README.ja.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ watch:
238238

239239
- **型アノテーション** — パラメータと戻り値の型、コンパイル時に削除
240240
- **ユニオン型**`String | Integer | nil`
241-
- **ジェネリクス**`Array<User>`, `Hash<String, Integer>`
241+
- **ジェネリクス**`User[]`, `Array<User>`, `Hash<String, Integer>`
242242
- **インターフェース** — オブジェクト間の契約を定義
243243
- **型エイリアス**`type UserID = Integer`
244244
- **RBS 生成** — Steep、Ruby LSP、Sorbet と連携

README.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ watch:
238238

239239
- **타입 어노테이션** — 파라미터와 리턴 타입, 컴파일 시 제거됨
240240
- **유니온 타입**`String | Integer | nil`
241-
- **제네릭**`Array<User>`, `Hash<String, Integer>`
241+
- **제네릭**`User[]`, `Array<User>`, `Hash<String, Integer>`
242242
- **인터페이스** — 객체 간 계약 정의
243243
- **타입 별칭**`type UserID = Integer`
244244
- **RBS 생성** — Steep, Ruby LSP, Sorbet과 연동

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ watch:
243243

244244
- **Type annotations** — Parameter and return types, erased at compile time
245245
- **Union types**`String | Integer | nil`
246-
- **Generics**`Array<User>`, `Hash<String, Integer>`
246+
- **Generics**`User[]`, `Array<User>`, `Hash<String, Integer>`
247247
- **Interfaces** — Define contracts between objects
248248
- **Type aliases**`type UserID = Integer`
249249
- **RBS generation** — Works with Steep, Ruby LSP, Sorbet

lib/t_ruby/ir.rb

Lines changed: 14 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,13 @@ def initialize(inner_type:, **opts)
646646
end
647647

648648
def to_rbs
649-
"#{@inner_type.to_rbs}?"
649+
inner_rbs = @inner_type.to_rbs
650+
# Simple types can use ? suffix, complex types need (Type | nil) form
651+
if @inner_type.is_a?(SimpleType)
652+
"#{inner_rbs}?"
653+
else
654+
"(#{inner_rbs} | nil)"
655+
end
650656
end
651657

652658
def to_trb
@@ -829,77 +835,17 @@ def parse_type(type_str)
829835
return nil unless type_str
830836

831837
type_str = type_str.strip
838+
return nil if type_str.empty?
832839

833-
# Union type
834-
if type_str.include?("|")
835-
types = type_str.split("|").map { |t| parse_type(t.strip) }
836-
return UnionType.new(types: types)
837-
end
838-
839-
# Intersection type
840-
if type_str.include?("&")
841-
types = type_str.split("&").map { |t| parse_type(t.strip) }
842-
return IntersectionType.new(types: types)
843-
end
840+
# Use ParserCombinator::TypeParser for all type parsing
841+
# Supports: simple types, generics, array shorthand, union, intersection, function types
842+
@type_parser ||= TRuby::ParserCombinator::TypeParser.new
843+
result = @type_parser.parse(type_str)
844+
return result[:type] if result[:success]
844845

845-
# Nullable type
846-
if type_str.end_with?("?")
847-
inner = parse_type(type_str[0..-2])
848-
return NullableType.new(inner_type: inner)
849-
end
850-
851-
# Generic type
852-
if type_str.include?("<") && type_str.include?(">")
853-
match = type_str.match(/^(\w+)<(.+)>$/)
854-
if match
855-
base = match[1]
856-
args = parse_generic_args(match[2])
857-
return GenericType.new(base: base, type_args: args)
858-
end
859-
end
860-
861-
# Function type
862-
if type_str.include?("->")
863-
match = type_str.match(/^\((.*)?\)\s*->\s*(.+)$/)
864-
if match
865-
param_types = match[1] ? match[1].split(",").map { |t| parse_type(t.strip) } : []
866-
return_type = parse_type(match[2])
867-
return FunctionType.new(param_types: param_types, return_type: return_type)
868-
end
869-
end
870-
871-
# Simple type
846+
# Fallback for unparseable types - return as SimpleType
872847
SimpleType.new(name: type_str)
873848
end
874-
875-
def parse_generic_args(args_str)
876-
args = []
877-
current = ""
878-
depth = 0
879-
880-
args_str.each_char do |char|
881-
case char
882-
when "<"
883-
depth += 1
884-
current += char
885-
when ">"
886-
depth -= 1
887-
current += char
888-
when ","
889-
if depth.zero?
890-
args << parse_type(current.strip)
891-
current = ""
892-
else
893-
current += char
894-
end
895-
else
896-
current += char
897-
end
898-
end
899-
900-
args << parse_type(current.strip) unless current.empty?
901-
args
902-
end
903849
end
904850

905851
#==========================================================================

lib/t_ruby/parser_combinator/token/token_declaration_parser.rb

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,7 @@ def parse_primary_type(tokens, position)
781781
end
782782

783783
# Check for tuple type: (Type, Type) -> ReturnType
784+
# or parenthesized type: (String | Integer)[]
784785
if tokens[position].type == :lparen
785786
position += 1
786787
param_types = []
@@ -811,6 +812,11 @@ def parse_primary_type(tokens, position)
811812

812813
node = IR::FunctionType.new(param_types: param_types, return_type: return_result.value)
813814
return TokenParseResult.success(node, tokens, return_result.position)
815+
elsif param_types.length == 1
816+
# Single type in parentheses: (String | Integer)
817+
# Check for postfix operators like [] or ?
818+
type = param_types[0]
819+
return parse_postfix_type_operators(tokens, position, type)
814820
else
815821
node = IR::TupleType.new(element_types: param_types)
816822
return TokenParseResult.success(node, tokens, position)
@@ -826,6 +832,13 @@ def parse_primary_type(tokens, position)
826832
type_name = tokens[position].value
827833
position += 1
828834

835+
# Handle type names ending with ? (e.g., "String?" scanned as single token)
836+
# This happens because Ruby allows ? in method/identifier names
837+
is_nullable_from_name = type_name.end_with?("?")
838+
if is_nullable_from_name
839+
type_name = type_name.chomp("?")
840+
end
841+
829842
# Check for generic arguments: Type<Args>
830843
if position < tokens.length && tokens[position].type == :lt
831844
position += 1
@@ -848,17 +861,46 @@ def parse_primary_type(tokens, position)
848861
position += 1
849862

850863
node = IR::GenericType.new(base: type_name, type_args: type_args)
851-
TokenParseResult.success(node, tokens, position)
852-
elsif position < tokens.length && tokens[position].type == :question
853-
# Check for nullable: Type?
854-
position += 1
855-
inner = IR::SimpleType.new(name: type_name)
856-
node = IR::NullableType.new(inner_type: inner)
857-
TokenParseResult.success(node, tokens, position)
864+
# Wrap in NullableType if the original type name ended with ?
865+
node = IR::NullableType.new(inner_type: node) if is_nullable_from_name
866+
# Apply postfix operators ([] or ?) to generic types too
867+
parse_postfix_type_operators(tokens, position, node)
858868
else
869+
# Simple type - apply postfix operators
859870
node = IR::SimpleType.new(name: type_name)
860-
TokenParseResult.success(node, tokens, position)
871+
# Wrap in NullableType if the original type name ended with ?
872+
node = IR::NullableType.new(inner_type: node) if is_nullable_from_name
873+
parse_postfix_type_operators(tokens, position, node)
874+
end
875+
end
876+
877+
# Parse postfix type operators: [] (array shorthand) and ? (nullable)
878+
# Handles patterns like:
879+
# String[] => Array<String>
880+
# Integer[][] => Array<Array<Integer>>
881+
# String[]? => NullableType(Array<String>)
882+
# String?[] => Array<NullableType(String)>
883+
# (A | B)[] => Array<UnionType(A, B)>
884+
def parse_postfix_type_operators(tokens, position, type)
885+
loop do
886+
break if position >= tokens.length
887+
888+
case tokens[position].type
889+
when :lbracket
890+
# Check for [] (empty brackets for array shorthand)
891+
break unless tokens[position + 1]&.type == :rbracket
892+
893+
position += 2
894+
type = IR::GenericType.new(base: "Array", type_args: [type])
895+
when :question
896+
position += 1
897+
type = IR::NullableType.new(inner_type: type)
898+
else
899+
break
900+
end
861901
end
902+
903+
TokenParseResult.success(type, tokens, position)
862904
end
863905

864906
# Parse hash literal type: { key: Type, key2: Type }

lib/t_ruby/parser_combinator/type_parser.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,24 @@ def build_parsers
8181
generic_type
8282
)
8383

84-
# With optional nullable suffix
85-
base_type = (primary_type >> nullable_suffix.optional).map do |(type, nullable)|
86-
nullable ? IR::NullableType.new(inner_type: type) : type
84+
# Array shorthand suffix: [] (can be repeated for nested arrays)
85+
array_suffix = string("[]")
86+
87+
# Postfix operators: ([] | ?)*
88+
# Handles: String[], Integer[][], String[]?, String?[], etc.
89+
postfix_op = array_suffix | nullable_suffix
90+
91+
base_type = (primary_type >> postfix_op.many).map do |(initial_type, ops)|
92+
ops.reduce(initial_type) do |type, op|
93+
case op
94+
when "[]"
95+
IR::GenericType.new(base: "Array", type_args: [type])
96+
when "?"
97+
IR::NullableType.new(inner_type: type)
98+
else
99+
type
100+
end
101+
end
87102
end
88103

89104
# Union type: Type | Type | ...

0 commit comments

Comments
 (0)