diff --git a/Jint.Tests/Runtime/RegExpTests.cs b/Jint.Tests/Runtime/RegExpTests.cs index 0efbd69011..1d923f2e7b 100644 --- a/Jint.Tests/Runtime/RegExpTests.cs +++ b/Jint.Tests/Runtime/RegExpTests.cs @@ -34,6 +34,50 @@ public void MatchGlobalUnicodeEmptyMatchesAdvanceByCodePoint() Assert.Equal(3, result); } + [Theory] + [InlineData("gy")] + [InlineData("guy")] + public void MatchStickyGlobalCollectsAllAdjacentMatches(string flags) + { + // without 'u' exercises the .NET sticky fast path, with 'u' the generic exec loop + var engine = new Engine(); + var result = engine.Evaluate($"JSON.stringify(['aaa'.match(/a/{flags}), 'ababab'.match(/ab/{flags})])").AsString(); + + Assert.Equal("[[\"a\",\"a\",\"a\"],[\"ab\",\"ab\",\"ab\"]]", result); + } + + [Theory] + [InlineData("gy")] + [InlineData("guy")] + public void MatchStickyGlobalStopsAtFirstGap(string flags) + { + // matches after the gap exist but are not adjacent, so sticky matching must not include them + var engine = new Engine(); + var result = engine.Evaluate($"JSON.stringify(['aabaa'.match(/a/{flags}), 'baa'.match(/a/{flags})])").AsString(); + + Assert.Equal("[[\"a\",\"a\"],null]", result); + } + + [Theory] + [InlineData("gy")] + [InlineData("guy")] + public void MatchStickyGlobalAdvancesOverEmptyMatches(string flags) + { + var engine = new Engine(); + var result = engine.Evaluate($"JSON.stringify('aab'.match(/a*/{flags}))").AsString(); + + Assert.Equal("[\"aa\",\"\",\"\"]", result); + } + + [Fact] + public void MatchStickyGlobalResetsLastIndex() + { + var engine = new Engine(); + var result = engine.Evaluate("var r = /a/gy; r.lastIndex = 2; JSON.stringify(['aaa'.match(r), r.lastIndex])").AsString(); + + Assert.Equal("[[\"a\",\"a\",\"a\"],0]", result); + } + [Theory] [InlineData("")] [InlineData("u")] diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 1f8aff847d..50a6475564 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/Jint/Native/RegExp/RegExpPrototype.cs @@ -930,21 +930,28 @@ private JsValue Match(JsValue thisObject, JsValue stringArg) return Null; } - a.SetIndexValue(0, match.Value, updateLength: false); - uint li = 0; + uint matchCount = 0; while (true) { - if (li > 0 && li % ConstraintCheckInterval == 0) + a.SetIndexValue(matchCount, match.Value, updateLength: false); + matchCount++; + + if (matchCount % ConstraintCheckInterval == 0) { _engine.Constraints.Check(); } + // sticky continuation: the next match must start exactly where the previous one + // ended; an empty match advances by one (AdvanceStringIndex, never unicode here) + var expectedIndex = match.Length == 0 ? match.Index + 1 : match.Index + match.Length; match = match.NextMatch(); - if (!match.Success || match.Index != ++li) + if (!match.Success || match.Index != expectedIndex) + { break; - a.SetIndexValue(li, match.Value, updateLength: false); + } } - a.SetLength(li); + + a.SetLength(matchCount); return a; } else