From ce24bf49c5ef0d6b656e2bb1199a288fec7ed7bc Mon Sep 17 00:00:00 2001 From: Alexey Yurchenko Date: Sat, 1 Feb 2025 20:07:30 +0300 Subject: [PATCH] Implement `StringLiteral#scan` --- spec/compiler/macro/macro_methods_spec.cr | 6 +++ src/compiler/crystal/macros.cr | 6 +++ src/compiler/crystal/macros/methods.cr | 54 +++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/spec/compiler/macro/macro_methods_spec.cr b/spec/compiler/macro/macro_methods_spec.cr index a0884b8331e9..508ae594a7d2 100644 --- a/spec/compiler/macro/macro_methods_spec.cr +++ b/spec/compiler/macro/macro_methods_spec.cr @@ -571,6 +571,12 @@ module Crystal assert_macro %({{"hello".gsub(/e|o/, "a")}}), %("halla") end + it "executes scan" do + assert_macro %({{"Crystal".scan(/(Cr)(?y)(st)(?al)/)}}), %([{0 => "Crystal", 1 => "Cr", "name1" => "y", 3 => "st", "name2" => "al"} of ::Int32 | ::String => ::String | ::Nil] of ::Hash(::Int32 | ::String, ::String | ::Nil)) + assert_macro %({{"Crystal".scan(/(Cr)?(stal)/)}}), %([{0 => "stal", 1 => nil, 2 => "stal"} of ::Int32 | ::String => ::String | ::Nil] of ::Hash(::Int32 | ::String, ::String | ::Nil)) + assert_macro %({{"Ruby".scan(/Crystal/)}}), %([] of ::Hash(::Int32 | ::String, ::String | ::Nil)) + end + it "executes camelcase" do assert_macro %({{"foo_bar".camelcase}}), %("FooBar") end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index ae6634e83a6f..0048bf635dcc 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -62,6 +62,12 @@ private macro def_string_methods(klass) def includes?(search : StringLiteral | CharLiteral) : BoolLiteral end + # Returns an array of capture hashes for each match of *regex* in this string. + # + # Capture hashes have the same form as `Regex::MatchData#to_h`. + def scan(regex : RegexLiteral) : ArrayLiteral(HashLiteral(NumberLiteral | StringLiteral), StringLiteral | NilLiteral) + end + # Similar to `String#size`. def size : NumberLiteral end diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index ede6ebb28a65..24992816c8f6 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -758,6 +758,60 @@ module Crystal end BoolLiteral.new(@value.includes?(piece)) end + when "scan" + interpret_check_args do |arg| + unless arg.is_a?(RegexLiteral) + raise "StringLiteral#scan expects a regex, not #{arg.class_desc}" + end + + regex_value = arg.value + if regex_value.is_a?(StringLiteral) + regex = Regex.new(regex_value.value, arg.options) + else + raise "regex interpolations not yet allowed in macros" + end + + matches = ArrayLiteral.new( + of: Generic.new( + Path.global("Hash"), + [ + Union.new([Path.global("Int32"), Path.global("String")] of ASTNode), + Union.new([Path.global("String"), Path.global("Nil")] of ASTNode), + ] of ASTNode + ) + ) + + @value.scan(regex) do |match_data| + captures = HashLiteral.new( + of: HashLiteral::Entry.new( + Union.new([Path.global("Int32"), Path.global("String")] of ASTNode), + Union.new([Path.global("String"), Path.global("Nil")] of ASTNode), + ) + ) + + match_data.to_h.each do |capture, substr| + case capture + in Int32 + key = NumberLiteral.new(capture) + in String + key = StringLiteral.new(capture) + end + + case substr + in String + value = StringLiteral.new(substr) + in Nil + value = NilLiteral.new + end + + captures.entries << HashLiteral::Entry.new(key, value) + end + + matches.elements << captures + end + + matches + end when "size" interpret_check_args { NumberLiteral.new(@value.size) } when "lines"