Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions spec/compiler/macro/macro_methods_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,15 @@ module Crystal
assert_macro %({{"hello".gsub(/e|o/, "a")}}), %("halla")
end

it "executes gsub with a block" do
assert_macro %q({{ "foo bar baz".gsub(/ba./) { "biz" } }}), %("foo biz biz") # No block args
assert_macro %q({{ "foo bar baz".gsub(/ba./) { |match| match.upcase } }}), %("foo BAR BAZ") # full matched string
assert_macro %q({{ "Name: Alice, Name: Bob".gsub(/Name: (\w+)/) { |full, matches| "User(#{matches[1].id})" } }}), %("User(Alice), User(Bob)") # single capture group
assert_macro %q({{ "5x10, 3x7".gsub(/(\d+)x(\d+)/) { |full, matches| "#{matches[1].to_i * matches[2].to_i}" } }}), %("50, 21") # multiple capture groups
assert_macro %q({{ "bar baz".gsub /bar (foo)?/ { |_, matches| matches[1].nil? ? "" : "BUG" } }}), %("baz") # Capture group no match
assert_macro %q({{ "bar".gsub /(foo)/ { "STR" } }}), %("bar") # No match at all
end

it "executes scan" do
assert_macro %({{"Crystal".scan(/(Cr)(?<name1>y)(st)(?<name2>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))
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/crystal/macros.cr
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ private macro def_string_methods(klass)
def ends_with?(other : StringLiteral | CharLiteral) : BoolLiteral
end

# Similar to `String#gsub(pattern, options, &)`.
#
# NOTE: The special variables `$~` and `$1`, `$2`, ... are not supported.
def gsub(regex : RegexLiteral, & : StringLiteral, ArrayLiteral(StringLiteral | NilLiteral) -> _) : {{klass}}
end

# Similar to `String#gsub`.
def gsub(regex : RegexLiteral, replacement : StringLiteral) : {{klass}}
end
Expand Down
33 changes: 28 additions & 5 deletions src/compiler/crystal/macros/methods.cr
Original file line number Diff line number Diff line change
Expand Up @@ -746,13 +746,36 @@ module Crystal
BoolLiteral.new(@value.ends_with?(piece))
end
when "gsub"
interpret_check_args do |first, second|
raise "first argument to StringLiteral#gsub must be a regex, not #{first.class_desc}" unless first.is_a?(RegexLiteral)
raise "second argument to StringLiteral#gsub must be a string, not #{second.class_desc}" unless second.is_a?(StringLiteral)
if block
interpret_check_args(uses_block: true) do |first|
raise "first argument to StringLiteral#gsub(&) must be a regex, not #{first.class_desc}" unless first.is_a?(RegexLiteral)

regex = regex_value first

new_value = value.gsub regex do |string, matches|
string_match_arg = block.args[0]?
matches_array_arg = block.args[1]?
matches_array_literal = ArrayLiteral.map matches.to_a do |item|
item.nil? ? NilLiteral.new : StringLiteral.new item
end

interpreter.define_var(string_match_arg.name, StringLiteral.new string) if string_match_arg
interpreter.define_var(matches_array_arg.name, matches_array_literal) if matches_array_arg

regex = regex_value(first)
interpreter.accept(block.body).to_macro_id
end

StringLiteral.new(value.gsub(regex, second.value))
StringLiteral.new new_value
end
else
interpret_check_args do |first, second|
raise "first argument to StringLiteral#gsub must be a regex, not #{first.class_desc}" unless first.is_a?(RegexLiteral)
raise "second argument to StringLiteral#gsub must be a string, not #{second.class_desc}" unless second.is_a?(StringLiteral)

regex = regex_value(first)

StringLiteral.new(value.gsub(regex, second.value))
end
end
when "identify"
interpret_check_args { StringLiteral.new(@value.tr(":", "_")) }
Expand Down
5 changes: 4 additions & 1 deletion src/string.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2707,7 +2707,10 @@ class String
# by the block value's value.
#
# ```
# "hello".gsub(/./) { |s| s[0].ord.to_s + ' ' } # => "104 101 108 108 111 "
# "hello".gsub(/./) { |s| s[0].ord.to_s + ' ' } # => "104 101 108 108 111 "
# "foo bar baz".gsub(/ba./) { |match| match.upcase } # => "foo BAR BAZ"
# "Name: Alice, Name: Bob".gsub(/Name: (\w+)/) { |full, matches| "User(#{matches[1]})" } # => "User(Alice), User(Bob)"
# "5x10, 3x7".gsub(/(\d+)x(\d+)/) { |full, matches| "#{matches[1].to_i * matches[2].to_i}" } # => "50, 21"
# ```
def gsub(pattern : Regex, *, options : Regex::MatchOptions = Regex::MatchOptions::None, &) : String
gsub_append(pattern, options) do |string, match, buffer|
Expand Down