Skip to content
Merged
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
13 changes: 10 additions & 3 deletions Jint.Repl/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,17 @@
JsValue str = result;
if (!result.IsPrimitive() && result is not IJsPrimitive)
{
str = serializer.Serialize(result, JsValue.Undefined, " ");
if (str == JsValue.Undefined)
if (result is JsRegExp jsRegExp)
{
str = result;
str = result.ToString();
}
else
{
str = serializer.Serialize(result, JsValue.Undefined, " ");
if (str == JsValue.Undefined)
{
str = result;
}
}
}
else if (result.IsString())
Expand Down
22 changes: 22 additions & 0 deletions Jint.Tests/Runtime/RegExpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ public void ToStringWithRealRegExpInstance()
Assert.Equal("/test/g", result);
}

[Fact]
public void ToStringPreserversOriginalPatternOfLiteral()
{
var engine = new Engine();
var result = engine.Evaluate("/^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$/");

var jsRegExp = Assert.IsType<JsRegExp>(result);
Assert.Equal("^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$", jsRegExp.Source);
Assert.Equal("/^x\\/\\\\r\\n\\u2028\\u2029\\0\0|[x/\\\\r\\n\\u2028\\u2029\\0\0]$/", jsRegExp.ToString());
}

[Fact]
public void ToStringCorrectlyEscapesProblematicCharacters()
{
var engine = new Engine();
var result = engine.Evaluate(@"new RegExp('^x/\\\r\n\u2028\u2029\\0\0|[x/\\\r\n\u2028\u2029\\0\0]$')");

var jsRegExp = Assert.IsType<JsRegExp>(result);
Assert.Equal("^x\\/\\r\\n\\u2028\\u2029\\0\0|[x/\\r\\n\\u2028\\u2029\\0\0]$", jsRegExp.Source);
Assert.Equal("/^x\\/\\r\\n\\u2028\\u2029\\0\0|[x/\\r\\n\\u2028\\u2029\\0\0]$/", jsRegExp.ToString());
}

[Fact]
public void ShouldNotThrowErrorOnIncompatibleRegex()
{
Expand Down
84 changes: 81 additions & 3 deletions Jint/Native/RegExp/RegExpConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,10 +449,13 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT

internal JsRegExp RegExpInitialize(JsRegExp r, JsValue pattern, JsValue flags, bool throwOnLastIndex = false)
{
// This method is called when a RegExp object is created at run-time, e.g.,
// `new RegExp("abc", "i")`, `RegExp("abc", "i")`, `/x/.compile("abc", "i")`, `"x".match("abc")`, etc.

var p = pattern.IsUndefined() ? "" : TypeConverter.ToString(pattern);
if (string.IsNullOrEmpty(p))
{
p = "(?:)";
p = JsRegExp.regExpForMatchingAllCharacters;
}

var f = flags.IsUndefined() ? "" : TypeConverter.ToString(flags);
Expand Down Expand Up @@ -496,8 +499,10 @@ internal JsRegExp RegExpInitialize(JsRegExp r, JsValue pattern, JsValue flags, b
r.ParseResult = regExpParseResult;
}

// The pattern may contain characters that are not valid in a regexp literal ('/' and new line characters),
// so such cases needs to be escaped.
r.Source = EscapeRegExpSource(p);
r.Flags = f;
r.Source = p;

if (throwOnLastIndex)
{
Expand Down Expand Up @@ -884,6 +889,67 @@ private static int HexDigitValue(char c)
return -1;
}

private static string EscapeRegExpSource(string source)
{
StringBuilder? sb = null;
var inClass = false;
var chunkStart = 0;
for (var i = 0; i < source.Length; i++)
{
var ch = source[i];
switch (ch)
{
case '[':
inClass = true;
break;
case ']' when inClass:
inClass = false;
break;
case '\\':
if (++i >= source.Length)
{
break;
}
ch = source[i];
// Backslash is omitted when a new line character follows.
if (ch is '\n' or '\r' or '\u2028' or '\u2029')
{
(sb ??= new()).Append(source, chunkStart, (i - 1) - chunkStart);
goto AppendEscapedNewLine;
}
break;
case '/' when !inClass:
(sb ??= new()).Append(source, chunkStart, i - chunkStart);
sb.Append('\\').Append(ch);
chunkStart = i + 1;
break;
case '\n':
case '\r':
case '\u2028':
case '\u2029':
(sb ??= new()).Append(source, chunkStart, i - chunkStart);
AppendEscapedNewLine:
sb.Append(ch switch
{
'\n' => @"\n",
'\r' => @"\r",
'\u2028' => @"\u2028",
_ => @"\u2029"
});
chunkStart = i + 1;
break;
}
}

if (sb is not null)
{
sb.Append(source, chunkStart, source.Length - chunkStart);
source = sb.ToString();
}

return source;
}

/// <summary>
/// Attempt to compile the regex pattern using the custom Jint regex engine.
/// </summary>
Expand Down Expand Up @@ -955,19 +1021,31 @@ private JsRegExp RegExpAlloc(JsValue newTarget)

internal JsRegExp Construct(RegExpParseResult parseResult, string source, string flags)
{
// This method is called to create a JsRegExp object from a regexp literal, e.g., `/abc/i`.

var r = RegExpAlloc(this);
r.Value = parseResult.Regex ?? DummyRegex;

// Source is already a valid, properly escaped JS regexp pattern.
r.Source = source;
r.Flags = flags;

r.ParseResult = parseResult;
RegExpInitialize(r);
return r;
}

public JsRegExp Construct(Regex regExp, string source, string flags)
public JsRegExp Construct(Regex regExp, string source = "?[native regex]", string flags = "")
{
// This method is called to create a JsRegExp object from a .NET Regex directly.

var r = RegExpAlloc(this);
r.Value = regExp;

// We shouldn't return the .NET pattern as if it were a JS pattern since that would be
// incorrect and misleading. We could try to convert the .NET pattern to a JS one,
// but that would be a huge (if not impossible) task. So let's use an invalid pattern
// giving a hint about the situation.
r.Source = source;
r.Flags = flags;

Expand Down
6 changes: 1 addition & 5 deletions Jint/Native/RegExp/RegExpPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,7 @@ private JsValue Source(JsValue thisObject)
return JsRegExp.regExpForMatchingAllCharacters;
}


return r.Source
.Replace("\\/", "/") // ensure forward-slashes
.Replace("/", "\\/") // then escape again
.Replace("\n", "\\n");
return r.Source;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/Interop/DefaultObjectConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal static class DefaultObjectConverter
{ typeof(ulong), (engine, v) => JsNumber.Create((ulong)v) },
{
typeof(System.Text.RegularExpressions.Regex),
(engine, v) => engine.Realm.Intrinsics.RegExp.Construct((System.Text.RegularExpressions.Regex)v, ((System.Text.RegularExpressions.Regex)v).ToString(), "")
(engine, v) => engine.Realm.Intrinsics.RegExp.Construct((System.Text.RegularExpressions.Regex)v)
}
};

Expand Down
Loading