Skip to content

Commit

Permalink
Fix Regex#inspect with non-literal-compatible options (#14575)
Browse files Browse the repository at this point in the history
  • Loading branch information
straight-shoota committed May 15, 2024
1 parent 920a2da commit 453a159
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 11 deletions.
28 changes: 21 additions & 7 deletions spec/std/regex_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,29 @@ describe "Regex" do
end

describe "#inspect" do
it "with options" do
/foo/.inspect.should eq("/foo/")
/foo/im.inspect.should eq("/foo/im")
/foo/imx.inspect.should eq("/foo/imx")
context "with literal-compatible options" do
it "prints flags" do
/foo/.inspect.should eq("/foo/")
/foo/im.inspect.should eq("/foo/im")
/foo/imx.inspect.should eq("/foo/imx")
end

it "escapes" do
%r(/).inspect.should eq("/\\//")
%r(\/).inspect.should eq("/\\//")
end
end

it "escapes" do
%r(/).inspect.should eq("/\\//")
%r(\/).inspect.should eq("/\\//")
context "with non-literal-compatible options" do
it "prints flags" do
Regex.new("foo", :anchored).inspect.should eq %(Regex.new("foo", Regex::Options::ANCHORED))
Regex.new("foo", :no_utf_check).inspect.should eq %(Regex.new("foo", Regex::Options::NO_UTF8_CHECK))
Regex.new("foo", Regex::CompileOptions[IGNORE_CASE, ANCHORED]).inspect.should eq %(Regex.new("foo", Regex::Options[IGNORE_CASE, ANCHORED]))
end

it "escapes" do
Regex.new(%("), :anchored).inspect.should eq %(Regex.new("\\"", Regex::Options::ANCHORED))
end
end
end

Expand Down
46 changes: 42 additions & 4 deletions src/regex.cr
Original file line number Diff line number Diff line change
Expand Up @@ -590,14 +590,36 @@ class Regex
nil
end

# Convert to `String` in literal format. Returns the source as a `String` in
# Regex literal format, delimited in forward slashes (`/`), with any
# optional flags included.
# Prints to *io* an unambiguous string representation of this regular expression object.
#
# Uses the regex literal syntax with basic option flags if sufficient (i.e. no
# other options than `IGNORE_CASE`, `MULTILINE`, or `EXTENDED` are set).
# Otherwise the syntax presents a `Regex.new` call.
# ```
# /ab+c/ix.inspect # => "/ab+c/ix"
# /ab+c/ix.inspect # => "/ab+c/ix"
# Regex.new("ab+c", :anchored).inspect # => Regex.new("ab+c", Regex::Options::ANCHORED)
# ```
def inspect(io : IO) : Nil
if (options & ~CompileOptions[IGNORE_CASE, MULTILINE, EXTENDED]).none?
inspect_literal(io)
else
inspect_extensive(io)
end
end

# Convert to `String` in literal format. Returns the source as a `String` in
# Regex literal format, delimited in forward slashes (`/`), with option flags
# included.
#
# Only `IGNORE_CASE`, `MULTILINE`, and `EXTENDED` options can be represented.
# Any other option value is ignored. Use `#inspect` instead for an unambiguous
# and correct representation.
#
# ```
# /ab+c/ix.inspect_literal # => "/ab+c/ix"
# Regex.new("ab+c", :anchored).inspect_literal # => "/ab+c/"
# ```
private def inspect_literal(io : IO) : Nil
io << '/'
Regex.append_source(source, io)
io << '/'
Expand All @@ -606,6 +628,22 @@ class Regex
io << 'x' if options.extended?
end

# Prints to *io* an extensive string representation of this regular expression object.
# The result is unambiguous and mirrors a Crystal expression to recreate an equivalent
# instance.
#
# ```
# /ab+c/ix.inspect_literal # => Regex.new("ab+c", Regex::Options[IGNORE_CASE, EXTENDED])
# Regex.new("ab+c", :anchored).inspect_literal # => Regex.new("ab+c", Regex::Options::ANCHORED)
# ```
private def inspect_extensive(io : IO) : Nil
io << "Regex.new("
source.inspect(io)
io << ", "
options.inspect(io)
io << ")"
end

# Match at character index. Matches a regular expression against `String`
# *str*. Starts at the character index given by *pos* if given, otherwise at
# the start of *str*. Returns a `Regex::MatchData` if *str* matched, otherwise
Expand Down

0 comments on commit 453a159

Please sign in to comment.