diff --git a/source/units/Goccia.Builtins.GlobalRegExp.pas b/source/units/Goccia.Builtins.GlobalRegExp.pas index dd423594..9699c697 100644 --- a/source/units/Goccia.Builtins.GlobalRegExp.pas +++ b/source/units/Goccia.Builtins.GlobalRegExp.pas @@ -397,7 +397,7 @@ constructor TGocciaGlobalRegExp.Create(const AName: string; function RequireRegExpThis(const AThisValue: TGocciaValue; const AMethodName: string): TGocciaObjectValue; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(AMethodName + ' requires a RegExp object'); Result := TGocciaObjectValue(AThisValue); end; @@ -685,9 +685,9 @@ function TGocciaGlobalRegExp.RegExpConstructorFn( const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; var - PatternArg: TGocciaValue; + PatternArg, PropVal: TGocciaValue; Pattern, Flags: string; - IsConstructCall: Boolean; + IsConstructCall, PatternIsRegExp: Boolean; begin Pattern := ''; Flags := ''; @@ -696,21 +696,31 @@ function TGocciaGlobalRegExp.RegExpConstructorFn( if AArgs.Length > 0 then begin PatternArg := AArgs.GetElement(0); - if IsRegExpValue(PatternArg) then + PatternIsRegExp := IsRegExp(PatternArg); + + // §22.2.3.1 step 2b: non-construct, regexp-like, no flags, same constructor + if PatternIsRegExp and not IsConstructCall and + ((AArgs.Length <= 1) or + (AArgs.GetElement(1) is TGocciaUndefinedLiteralValue)) and + (TGocciaObjectValue(PatternArg).GetProperty(PROP_CONSTRUCTOR) = + FRegExpConstructor) then + Exit(PatternArg); + + // §22.2.3.1 steps 3–4: read source/flags when regexp-like + if PatternIsRegExp then begin - if not IsConstructCall and - ((AArgs.Length <= 1) or - (AArgs.GetElement(1) is TGocciaUndefinedLiteralValue)) then - Exit(PatternArg); - - Pattern := TGocciaObjectValue(PatternArg).GetProperty(PROP_SOURCE) - .ToStringLiteral.Value; + PropVal := TGocciaObjectValue(PatternArg).GetProperty(PROP_SOURCE); + if not (PropVal is TGocciaUndefinedLiteralValue) then + Pattern := PropVal.ToStringLiteral.Value; if (AArgs.Length > 1) and not (AArgs.GetElement(1) is TGocciaUndefinedLiteralValue) then Flags := AArgs.GetElement(1).ToStringLiteral.Value else - Flags := TGocciaObjectValue(PatternArg).GetProperty(PROP_FLAGS) - .ToStringLiteral.Value; + begin + PropVal := TGocciaObjectValue(PatternArg).GetProperty(PROP_FLAGS); + if not (PropVal is TGocciaUndefinedLiteralValue) then + Flags := PropVal.ToStringLiteral.Value; + end; end else begin @@ -729,7 +739,7 @@ function TGocciaGlobalRegExp.RegExpConstruct( const ANewTarget: TGocciaValue): TGocciaValue; var Proto: TGocciaObjectValue; - PatternArg: TGocciaValue; + PatternArg, PropVal: TGocciaValue; Pattern, Flags: string; PatternIsRegExp, FlagsProvided: Boolean; begin @@ -739,18 +749,22 @@ function TGocciaGlobalRegExp.RegExpConstruct( FlagsProvided := (AArgs.Length > 1) and not (AArgs.GetElement(1) is TGocciaUndefinedLiteralValue); - // §22.2.4.1 step 3: if pattern is a RegExp, capture source before step 6 + // §22.2.4.1 steps 1, 3–4: IsRegExp check, then read source/flags if regexp-like if AArgs.Length > 0 then begin PatternArg := AArgs.GetElement(0); - if IsRegExpValue(PatternArg) then + PatternIsRegExp := IsRegExp(PatternArg); + if PatternIsRegExp then begin - PatternIsRegExp := True; - Pattern := TGocciaObjectValue(PatternArg).GetProperty(PROP_SOURCE) - .ToStringLiteral.Value; + PropVal := TGocciaObjectValue(PatternArg).GetProperty(PROP_SOURCE); + if not (PropVal is TGocciaUndefinedLiteralValue) then + Pattern := PropVal.ToStringLiteral.Value; if not FlagsProvided then - Flags := TGocciaObjectValue(PatternArg).GetProperty(PROP_FLAGS) - .ToStringLiteral.Value; + begin + PropVal := TGocciaObjectValue(PatternArg).GetProperty(PROP_FLAGS); + if not (PropVal is TGocciaUndefinedLiteralValue) then + Flags := PropVal.ToStringLiteral.Value; + end; end; end; @@ -774,7 +788,7 @@ function TGocciaGlobalRegExp.RegExpExec(const AArgs: TGocciaArgumentsCollection; Input: string; MatchValue: TGocciaValue; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpExecNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -795,7 +809,7 @@ function TGocciaGlobalRegExp.RegExpTest(const AArgs: TGocciaArgumentsCollection; Input: string; MatchValue: TGocciaValue; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpTestNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -812,7 +826,7 @@ function TGocciaGlobalRegExp.RegExpToStringMethod( const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpToStringNonRegExp, SSuggestRegExpThisType); Result := TGocciaStringLiteralValue.Create(RegExpObjectToString(AThisValue)); @@ -829,7 +843,7 @@ function TGocciaGlobalRegExp.RegExpSymbolMatch( ResultArray: TGocciaArrayValue; MatchIndex, MatchEnd, NextIndex: Integer; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpMatchNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -869,7 +883,7 @@ function TGocciaGlobalRegExp.RegExpSymbolMatchAll( RegexClone: TGocciaObjectValue; IsGlobal: Boolean; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpMatchAllNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -896,7 +910,7 @@ function TGocciaGlobalRegExp.RegExpSymbolReplace( MatchArray: TGocciaObjectValue; MatchIndex, MatchEnd, NextIndex, SearchIndex, OutputIndex: Integer; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpReplaceNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -961,7 +975,7 @@ function TGocciaGlobalRegExp.RegExpSymbolSearch( MatchArray: TGocciaObjectValue; MatchIndex, MatchEnd, NextIndex: Integer; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpSearchNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then @@ -992,7 +1006,7 @@ function TGocciaGlobalRegExp.RegExpSymbolSplit( LastMatchWasZeroWidth: Boolean; I: Integer; begin - if not IsRegExpValue(AThisValue) then + if not IsRegExpInstance(AThisValue) then ThrowTypeError(SErrorRegExpSplitNonRegExp, SSuggestRegExpThisType); if AArgs.Length > 0 then diff --git a/source/units/Goccia.Builtins.TestingLibrary.pas b/source/units/Goccia.Builtins.TestingLibrary.pas index 0bd6e90f..c242e397 100644 --- a/source/units/Goccia.Builtins.TestingLibrary.pas +++ b/source/units/Goccia.Builtins.TestingLibrary.pas @@ -902,7 +902,7 @@ function TGocciaExpectationValue.ToMatch(const AArgs: TGocciaArgumentsCollection end; Expected := AArgs.GetElement(0); - if not (Expected is TGocciaStringLiteralValue) and not IsRegExpValue(Expected) then + if not (Expected is TGocciaStringLiteralValue) and not IsRegExpInstance(Expected) then begin if FIsNegated then TGocciaTestAssertions(FTestAssertions).AssertionPassed('toMatch') @@ -915,7 +915,7 @@ function TGocciaExpectationValue.ToMatch(const AArgs: TGocciaArgumentsCollection end; ActualString := FormatForDisplay(FActualValue); - if IsRegExpValue(Expected) then + if IsRegExpInstance(Expected) then begin ExpectedDescription := RegExpObjectToString(Expected); Matches := MatchRegExpObject(Expected, ActualString, 0, False, False, diff --git a/source/units/Goccia.Error.Messages.pas b/source/units/Goccia.Error.Messages.pas index 97420322..cc9e2da9 100644 --- a/source/units/Goccia.Error.Messages.pas +++ b/source/units/Goccia.Error.Messages.pas @@ -396,6 +396,7 @@ interface SErrorMatchAllRequiresGlobalRegExp = 'String.prototype.matchAll requires a global RegExp'; SErrorSymbolMatchAllNotCallable = '@@matchAll is not callable'; SErrorSymbolSearchNotCallable = '@@search is not callable'; + SErrorFirstArgMustNotBeRegExp = 'First argument to %s must not be a regular expression'; SErrorInvalidRepeatCount = 'Invalid count value: %s'; SErrorInvalidNormalizationForm = 'The normalization form should be one of NFC, NFD, NFKC, NFKD'; SErrorStringPrototypeRequiresNonNullish = 'String.prototype method requires that ''this'' not be null or undefined'; diff --git a/source/units/Goccia.Error.Suggestions.pas b/source/units/Goccia.Error.Suggestions.pas index f753c6f6..9538fa32 100644 --- a/source/units/Goccia.Error.Suggestions.pas +++ b/source/units/Goccia.Error.Suggestions.pas @@ -225,6 +225,7 @@ interface // Runtime errors — string SSuggestWellKnownSymbolCallable = 'the well-known Symbol method must be a function on the object'; SSuggestReplaceAllGlobalFlag = 'add the ''g'' flag to the RegExp: /pattern/g'; + SSuggestUseMatchOrSearch = 'use String.prototype.match or String.prototype.search instead'; SSuggestRepeatCountRange = 'the count must be a non-negative finite integer'; SSuggestNormalizationForm = 'valid forms are NFC, NFD, NFKC, or NFKD'; diff --git a/source/units/Goccia.REPL.Formatter.pas b/source/units/Goccia.REPL.Formatter.pas index 044a54cb..eec20c0a 100644 --- a/source/units/Goccia.REPL.Formatter.pas +++ b/source/units/Goccia.REPL.Formatter.pas @@ -202,7 +202,7 @@ function FormatREPLValue(const AValue: TGocciaValue; end; // RegExp — JSON.stringify would show empty object - if IsRegExpValue(AValue) then + if IsRegExpInstance(AValue) then Exit(Colorize(RegExpObjectToString(AValue), ANSI_RED, AUseColor)); // Maps — JSON.stringify can't see internal entries diff --git a/source/units/Goccia.RegExp.Runtime.pas b/source/units/Goccia.RegExp.Runtime.pas index 29d3f48d..4c637fc9 100644 --- a/source/units/Goccia.RegExp.Runtime.pas +++ b/source/units/Goccia.RegExp.Runtime.pas @@ -11,7 +11,8 @@ interface function GetRegExpPrototype: TGocciaValue; procedure SetRegExpPrototype(const APrototype: TGocciaValue); -function IsRegExpValue(const AValue: TGocciaValue): Boolean; +function IsRegExpInstance(const AValue: TGocciaValue): Boolean; +function IsRegExp(const AValue: TGocciaValue): Boolean; function CreateRegExpObject(const APattern, AFlags: string): TGocciaValue; function CloneRegExpObject(const AValue: TGocciaValue): TGocciaValue; function MatchRegExpObjectOnce(const AValue: TGocciaValue; const AInput: string; @@ -117,12 +118,24 @@ function BuildMatchArray(const AInput: string; Result := MatchArray; end; -function IsRegExpValue(const AValue: TGocciaValue): Boolean; +function IsRegExpInstance(const AValue: TGocciaValue): Boolean; begin if not (AValue is TGocciaObjectValue) then Exit(False); - Result := TGocciaObjectValue(AValue).HasOwnProperty(PROP_SOURCE) and - TGocciaObjectValue(AValue).HasOwnProperty(PROP_FLAGS); + Result := TGocciaObjectValue(AValue).HasRegExpData; +end; + +function IsRegExp(const AValue: TGocciaValue): Boolean; +var + Matcher: TGocciaValue; +begin + if not (AValue is TGocciaObjectValue) then + Exit(False); + Matcher := TGocciaObjectValue(AValue).GetSymbolProperty( + TGocciaSymbolValue.WellKnownMatch); + if not (Matcher is TGocciaUndefinedLiteralValue) then + Exit(Matcher.ToBooleanLiteral.Value); + Result := IsRegExpInstance(AValue); end; function CreateRegExpObject(const APattern, AFlags: string): TGocciaValue; @@ -141,6 +154,7 @@ function CreateRegExpObject(const APattern, AFlags: string): TGocciaValue; Source := NormalizeRegExpSource(APattern); CanonicalFlags := CanonicalizeRegExpFlags(AFlags); Obj := TGocciaObjectValue.Create(GRegExpPrototype); + Obj.HasRegExpData := True; Obj.DefineProperty(PROP_SOURCE, TGocciaPropertyDescriptorData.Create( TGocciaStringLiteralValue.Create(Source), [])); diff --git a/source/units/Goccia.Values.ObjectValue.pas b/source/units/Goccia.Values.ObjectValue.pas index a24cf019..e16ca70f 100644 --- a/source/units/Goccia.Values.ObjectValue.pas +++ b/source/units/Goccia.Values.ObjectValue.pas @@ -29,6 +29,7 @@ TGocciaObjectValue = class(TGocciaValue) FSealed: Boolean; FExtensible: Boolean; FHasErrorData: Boolean; + FHasRegExpData: Boolean; public class procedure InitializeSharedPrototype; class function GetSharedObjectPrototype: TGocciaObjectValue; static; @@ -96,6 +97,7 @@ TGocciaObjectValue = class(TGocciaValue) property Sealed: Boolean read FSealed; property Extensible: Boolean read FExtensible; property HasErrorData: Boolean read FHasErrorData write FHasErrorData; + property HasRegExpData: Boolean read FHasRegExpData write FHasRegExpData; published function ObjectPrototypeToString(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function ObjectPrototypeIsPrototypeOf(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; diff --git a/source/units/Goccia.Values.StringObjectValue.pas b/source/units/Goccia.Values.StringObjectValue.pas index 275e3840..fd2bbae6 100644 --- a/source/units/Goccia.Values.StringObjectValue.pas +++ b/source/units/Goccia.Values.StringObjectValue.pas @@ -126,18 +126,35 @@ function GetSharedStringPrototype: TGocciaObjectValue; inline; function CoerceRegExpValue(const AValue: TGocciaValue; const ANewFlags: string = ''): TGocciaObjectValue; var - Pattern: string; + Pattern, Flags: string; + PropVal: TGocciaValue; begin - if IsRegExpValue(AValue) then - Result := TGocciaObjectValue(CloneRegExpObject(AValue)) + if IsRegExp(AValue) then + begin + PropVal := TGocciaObjectValue(AValue).GetProperty(PROP_SOURCE); + if PropVal is TGocciaUndefinedLiteralValue then + Pattern := '' + else + Pattern := PropVal.ToStringLiteral.Value; + if ANewFlags <> '' then + Flags := ANewFlags + else + begin + PropVal := TGocciaObjectValue(AValue).GetProperty(PROP_FLAGS); + if PropVal is TGocciaUndefinedLiteralValue then + Flags := '' + else + Flags := PropVal.ToStringLiteral.Value; + end; + Result := TGocciaObjectValue(CreateRegExpObject(Pattern, Flags)); + end else begin if AValue is TGocciaUndefinedLiteralValue then Pattern := '' else Pattern := AValue.ToStringLiteral.Value; - Result := TGocciaObjectValue(CreateRegExpObject( - Pattern, ANewFlags)); + Result := TGocciaObjectValue(CreateRegExpObject(Pattern, ANewFlags)); end; end; @@ -827,11 +844,15 @@ function TGocciaStringObjectValue.StringIncludes(const AArgs: TGocciaArgumentsCo FoundIndex: Integer; Len: Integer; begin - // Step 1: Let O be RequireObjectCoercible(this value) - // Step 2: Let S be ToString(O) + // Step 1–2: Let S be ToString(RequireObjectCoercible(this)) StringValue := ExtractStringValue(AThisValue); - // Step 3: Let searchStr be ToString(searchString) + // §22.1.3.8 steps 3–4: throw if searchString is regexp-like + if (AArgs.Length > 0) and IsRegExp(AArgs.GetElement(0)) then + ThrowTypeError(Format(SErrorFirstArgMustNotBeRegExp, + ['String.prototype.includes']), SSuggestUseMatchOrSearch); + + // Step 5: Let searchStr be ToString(searchString) if AArgs.Length > 0 then SearchValue := AArgs.GetElement(0).ToStringLiteral.Value else @@ -875,23 +896,25 @@ function TGocciaStringObjectValue.StringStartsWith(const AArgs: TGocciaArguments StartPosition: Integer; Len: Integer; begin - // Step 1: Let O be RequireObjectCoercible(this value) - // Step 2: Let S be ToString(O) + // Step 1–2: Let S be ToString(RequireObjectCoercible(this)) StringValue := ExtractStringValue(AThisValue); - // Step 3: Let searchStr be ToString(searchString) + // §22.1.3.24 steps 3–4: throw if searchString is regexp-like + if (AArgs.Length > 0) and IsRegExp(AArgs.GetElement(0)) then + ThrowTypeError(Format(SErrorFirstArgMustNotBeRegExp, + ['String.prototype.startsWith']), SSuggestUseMatchOrSearch); + + // Step 5: Let searchStr be ToString(searchString) if AArgs.Length > 0 then SearchValue := AArgs.GetElement(0).ToStringLiteral.Value else SearchValue := 'undefined'; - // Step 4: Let searchLength be the length of searchStr - // Step 5: Let start be min(max(ToIntegerOrInfinity(position), 0), len) + // Step 6: Let start be min(max(ToIntegerOrInfinity(position), 0), len) Len := UTF16CodeUnitLength(StringValue); StartPosition := Min(Max(0, ToIntegerFromArgs(AArgs, 1)), Len); - // Step 6: If searchLength + start > len(S), return false - // Step 7: If the code units of S starting at start match searchStr, return true; else false + // Step 7–8: If searchLength + start > len(S), return false; else compare if StartPosition + UTF16CodeUnitLength(SearchValue) > Len then Result := TGocciaBooleanLiteralValue.FalseValue else if UTF16IndexOf(StringValue, SearchValue, StartPosition) = StartPosition then @@ -908,19 +931,23 @@ function TGocciaStringObjectValue.StringEndsWith(const AArgs: TGocciaArgumentsCo SearchLength: Integer; StartPosition: Integer; begin - // Step 1: Let O be RequireObjectCoercible(this value) - // Step 2: Let S be ToString(O) + // Step 1–2: Let S be ToString(RequireObjectCoercible(this)) StringValue := ExtractStringValue(AThisValue); - // Step 3: Let searchStr be ToString(searchString) + // §22.1.3.6 steps 3–4: throw if searchString is regexp-like + if (AArgs.Length > 0) and IsRegExp(AArgs.GetElement(0)) then + ThrowTypeError(Format(SErrorFirstArgMustNotBeRegExp, + ['String.prototype.endsWith']), SSuggestUseMatchOrSearch); + + // Step 5: Let searchStr be ToString(searchString) if AArgs.Length > 0 then SearchValue := AArgs.GetElement(0).ToStringLiteral.Value else SearchValue := 'undefined'; - // Step 4: Let searchLength be the length of searchStr - // Step 5: If endPosition is undefined, let pos be len; else let pos be ToIntegerOrInfinity(endPosition) - // Step 6: Let end be min(max(pos, 0), len) + // Step 6: Let searchLength be the length of searchStr + // Step 7: If endPosition is undefined, let pos be len; else let pos be ToIntegerOrInfinity(endPosition) + // Step 8: Let end be min(max(pos, 0), len) EndPosition := Min(Max(0, ToIntegerFromArgs(AArgs, 1, UTF16CodeUnitLength(StringValue))), UTF16CodeUnitLength(StringValue)); @@ -1076,48 +1103,7 @@ function TGocciaStringObjectValue.StringReplaceMethod(const AArgs: TGocciaArgume // Step 3: Let string be ? ToString(O). StringValue := ExtractStringValue(AThisValue); - if IsRegExpValue(SearchArg) then - begin - RegexValue := CoerceRegExpValue(SearchArg); - TGarbageCollector.Instance.AddTempRoot(RegexValue); - try - if GetRegExpBooleanProperty(RegexValue, PROP_GLOBAL) then - begin - ResultStr := ''; - Offset := 0; - while MatchRegExpObjectValue(RegexValue, StringValue, Offset, False, - False, MatchArray, MatchIndex, MatchEnd, NextIndex) do - begin - ResultStr := ResultStr + Copy(StringValue, Offset + 1, - MatchIndex - Offset); - ResultStr := ResultStr + BuildRegexReplacement(ReplaceArg, - TGocciaArrayValue(MatchArray), MatchIndex, StringValue); - Offset := NextIndex; - if Offset > Length(StringValue) then - Break; - end; - if Offset <= Length(StringValue) then - ResultStr := ResultStr + Copy(StringValue, Offset + 1, MaxInt); - Result := TGocciaStringLiteralValue.Create(ResultStr); - end - else if MatchRegExpObjectValue(RegexValue, StringValue, 0, False, False, - MatchArray, MatchIndex, MatchEnd, NextIndex) then - begin - ReplaceValue := BuildRegexReplacement(ReplaceArg, - TGocciaArrayValue(MatchArray), - MatchIndex, StringValue); - Result := TGocciaStringLiteralValue.Create( - Copy(StringValue, 1, MatchIndex) + ReplaceValue + - Copy(StringValue, MatchEnd + 1, MaxInt)); - end - else - Result := TGocciaStringLiteralValue.Create(StringValue); - finally - TGarbageCollector.Instance.RemoveTempRoot(RegexValue); - end; - Exit; - end; - + // §22.1.3.19 step 4: Let searchString be ? ToString(searchValue). SearchValue := SearchArg.ToStringLiteral.Value; // Step 5: Let pos be StringIndexOf(string, searchString, 0) @@ -1171,7 +1157,7 @@ function TGocciaStringObjectValue.StringReplaceMethod(const AArgs: TGocciaArgume function TGocciaStringObjectValue.StringReplaceAllMethod(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; var StringValue, SearchValue, ReplaceValue, ResultStr: string; - SearchArg: TGocciaValue; + SearchArg, FlagsVal: TGocciaValue; ReplaceArg: TGocciaValue; CallArgs: TGocciaArgumentsCollection; CallResult: TGocciaValue; @@ -1196,16 +1182,16 @@ function TGocciaStringObjectValue.StringReplaceAllMethod(const AArgs: TGocciaArg else ReplaceArg := TGocciaUndefinedLiteralValue.UndefinedValue; - // Step 2 (spec): If searchValue is neither undefined nor null, invoke - // GetMethod(searchValue, @@replace) and dispatch through it BEFORE doing - // any ToString on `O` or the args. ES2026 §22.1.3.20 step 2.a.iii: the - // RegExp-flags global check uses Get + ToString on searchValue.flags - // only when searchValue is itself a RegExp; for arbitrary user-supplied - // searchValues with @@replace, the replacer call is the only observable - // side effect. - if IsRegExpValue(SearchArg) and - not GetRegExpBooleanProperty(TGocciaObjectValue(SearchArg), PROP_GLOBAL) then - ThrowTypeError(SErrorReplaceAllRequiresGlobalRegExp, SSuggestReplaceAllGlobalFlag); + // §22.1.3.20 step 2a–b: IsRegExp check; RequireObjectCoercible(flags); 'g' check + if IsRegExp(SearchArg) then + begin + FlagsVal := TGocciaObjectValue(SearchArg).GetProperty(PROP_FLAGS); + if (FlagsVal is TGocciaUndefinedLiteralValue) or + (FlagsVal is TGocciaNullLiteralValue) then + ThrowTypeError(SErrorCannotConvertNullOrUndefined, SSuggestObjectArgType); + if Pos('g', FlagsVal.ToStringLiteral.Value) = 0 then + ThrowTypeError(SErrorReplaceAllRequiresGlobalRegExp, SSuggestReplaceAllGlobalFlag); + end; ReplaceMethod := GetMethodBySymbol(SearchArg, TGocciaSymbolValue.WellKnownReplace); @@ -1231,34 +1217,7 @@ function TGocciaStringObjectValue.StringReplaceAllMethod(const AArgs: TGocciaArg // Step 3 (spec): Let string be ? ToString(O) StringValue := ExtractStringValue(AThisValue); - if IsRegExpValue(SearchArg) then - begin - RegexValue := CoerceRegExpValue(SearchArg); - TGarbageCollector.Instance.AddTempRoot(RegexValue); - try - ResultStr := ''; - Offset := 0; - while MatchRegExpObjectValue(RegexValue, StringValue, Offset, False, - False, - MatchArray, MatchIndex, MatchEnd, NextIndex) do - begin - ResultStr := ResultStr + Copy(StringValue, Offset + 1, - MatchIndex - Offset); - ResultStr := ResultStr + BuildRegexReplacement(ReplaceArg, - TGocciaArrayValue(MatchArray), MatchIndex, StringValue); - Offset := NextIndex; - if Offset > Length(StringValue) then - Break; - end; - if Offset <= Length(StringValue) then - ResultStr := ResultStr + Copy(StringValue, Offset + 1, MaxInt); - Result := TGocciaStringLiteralValue.Create(ResultStr); - finally - TGarbageCollector.Instance.RemoveTempRoot(RegexValue); - end; - Exit; - end; - + // §22.1.3.20 step 7: Let searchString be ? ToString(searchValue). SearchValue := SearchArg.ToStringLiteral.Value; // Step 5: Let searchLength be the length of searchString @@ -1452,48 +1411,7 @@ function TGocciaStringObjectValue.StringSplit(const AArgs: TGocciaArgumentsColle // Step 3 (spec): Let S be ? ToString(O). Only reached when no @@split. StringValue := ExtractStringValue(AThisValue); - if IsRegExpValue(SeparatorArg) then - begin - RegexValue := CoerceRegExpValue(SeparatorArg); - TGarbageCollector.Instance.AddTempRoot(RegexValue); - try - PreviousIndex := 0; - SearchIndex := 0; - while MatchRegExpObjectValue(RegexValue, StringValue, SearchIndex, - False, - False, MatchArray, MatchIndex, MatchEnd, NextIndex) do - begin - ResultArray.Elements.Add(TGocciaStringLiteralValue.Create( - Copy(StringValue, PreviousIndex + 1, MatchIndex - PreviousIndex))); - if HasLimit and (Cardinal(ResultArray.Elements.Count) >= Limit) then - Break; - - for I := 1 to TGocciaArrayValue(MatchArray).Elements.Count - 1 do - begin - ResultArray.Elements.Add(TGocciaArrayValue(MatchArray).Elements[I]); - if HasLimit and (Cardinal(ResultArray.Elements.Count) >= Limit) then - Break; - end; - if HasLimit and (Cardinal(ResultArray.Elements.Count) >= Limit) then - Break; - - PreviousIndex := MatchEnd; - SearchIndex := NextIndex; - if SearchIndex > Length(StringValue) then - Break; - end; - - if not HasLimit or (Cardinal(ResultArray.Elements.Count) < Limit) then - ResultArray.Elements.Add(TGocciaStringLiteralValue.Create( - Copy(StringValue, PreviousIndex + 1, Length(StringValue) - PreviousIndex))); - - Result := ResultArray; - finally - TGarbageCollector.Instance.RemoveTempRoot(RegexValue); - end; - Exit; - end; - + // §22.1.3.23 step 5: Let R be ? ToString(separator). Separator := SeparatorArg.ToStringLiteral.Value; // Step 5: If separator is undefined, return [S] @@ -1646,7 +1564,7 @@ function TGocciaStringObjectValue.StringMatch(const AArgs: TGocciaArgumentsColle function TGocciaStringObjectValue.StringMatchAll(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; var StringValue: string; - MatchAllMethod: TGocciaValue; + RegExpArg, FlagsVal, MatchAllMethod: TGocciaValue; CallArgs: TGocciaArgumentsCollection; RegexValue: TGocciaObjectValue; begin @@ -1660,14 +1578,21 @@ function TGocciaStringObjectValue.StringMatchAll(const AArgs: TGocciaArgumentsCo (AThisValue is TGocciaNullLiteralValue) then ThrowTypeError(SErrorStringPrototypeRequiresNonNullish, SSuggestCheckNullBeforeAccess); - if (AArgs.Length > 0) and IsRegExpValue(AArgs.GetElement(0)) and - not GetRegExpBooleanProperty(TGocciaObjectValue(AArgs.GetElement(0)), - PROP_GLOBAL) then - ThrowTypeError(SErrorMatchAllRequiresGlobalRegExp, SSuggestReplaceAllGlobalFlag); - + // §22.1.3.13 step 2a–b: IsRegExp check; RequireObjectCoercible(flags); 'g' check if AArgs.Length > 0 then begin - MatchAllMethod := GetMethodBySymbol(AArgs.GetElement(0), + RegExpArg := AArgs.GetElement(0); + if IsRegExp(RegExpArg) then + begin + FlagsVal := TGocciaObjectValue(RegExpArg).GetProperty(PROP_FLAGS); + if (FlagsVal is TGocciaUndefinedLiteralValue) or + (FlagsVal is TGocciaNullLiteralValue) then + ThrowTypeError(SErrorCannotConvertNullOrUndefined, SSuggestObjectArgType); + if Pos('g', FlagsVal.ToStringLiteral.Value) = 0 then + ThrowTypeError(SErrorMatchAllRequiresGlobalRegExp, SSuggestReplaceAllGlobalFlag); + end; + + MatchAllMethod := GetMethodBySymbol(RegExpArg, TGocciaSymbolValue.WellKnownMatchAll); if not (MatchAllMethod is TGocciaUndefinedLiteralValue) then begin @@ -1678,7 +1603,7 @@ function TGocciaStringObjectValue.StringMatchAll(const AArgs: TGocciaArgumentsCo AThisValue ]); try - Result := InvokeCallable(MatchAllMethod, CallArgs, AArgs.GetElement(0)); + Result := InvokeCallable(MatchAllMethod, CallArgs, RegExpArg); finally CallArgs.Free; end; @@ -1690,7 +1615,7 @@ function TGocciaStringObjectValue.StringMatchAll(const AArgs: TGocciaArgumentsCo StringValue := ExtractStringValue(AThisValue); if AArgs.Length > 0 then - RegexValue := CoerceRegExpValue(AArgs.GetElement(0), 'g') + RegexValue := CoerceRegExpValue(RegExpArg, 'g') else RegexValue := CoerceRegExpValue(TGocciaUndefinedLiteralValue.UndefinedValue, 'g'); diff --git a/tests/built-ins/RegExp/constructor.js b/tests/built-ins/RegExp/constructor.js index 392fb7b7..e1f73983 100644 --- a/tests/built-ins/RegExp/constructor.js +++ b/tests/built-ins/RegExp/constructor.js @@ -134,3 +134,76 @@ test("test on Object.create(RegExp.prototype) throws TypeError", () => { const obj = Object.create(RegExp.prototype); expect(() => { RegExp.prototype.test.call(obj, "test"); }).toThrow(TypeError); }); + +// --- IsRegExp via Symbol.match --- + +test("new RegExp reads source/flags from object with Symbol.match truthy", () => { + const obj = { source: "abc", flags: "g", [Symbol.match]: true }; + const r = new RegExp(obj); + expect(r.source).toBe("abc"); + expect(r.flags).toBe("g"); + expect(r.toString()).toBe("/abc/g"); +}); + +test("new RegExp uses empty strings when source/flags are absent on Symbol.match object", () => { + const obj = { [Symbol.match]: true }; + const r = new RegExp(obj); + expect(r.source).toBe("(?:)"); + expect(r.flags).toBe(""); +}); + +test("new RegExp stringifies object when Symbol.match is false", () => { + const obj = { source: "abc", flags: "", [Symbol.match]: false }; + const r = new RegExp(obj); + expect(r.source).toBe("[object Object]"); +}); + +test("RegExp() without new returns actual RegExp as-is when flags omitted", () => { + const r = /abc/g; + const result = RegExp(r); + expect(result).toBe(r); +}); + +test("RegExp() without new wraps Symbol.match object whose constructor differs", () => { + const obj = { source: "abc", flags: "", [Symbol.match]: true }; + const result = RegExp(obj); + expect(result).not.toBe(obj); + expect(result.source).toBe("abc"); +}); + +test("new RegExp overrides flags from Symbol.match object when second arg provided", () => { + const obj = { source: "abc", flags: "g", [Symbol.match]: true }; + const r = new RegExp(obj, "i"); + expect(r.source).toBe("abc"); + expect(r.flags).toBe("i"); +}); + +test("new RegExp treats Symbol.match with truthy non-boolean as regexp-like", () => { + const obj = { source: "x", flags: "", [Symbol.match]: 1 }; + const r = new RegExp(obj); + expect(r.source).toBe("x"); +}); + +test("new RegExp stringifies object when Symbol.match is null", () => { + const obj = { source: "abc", flags: "", [Symbol.match]: null }; + const r = new RegExp(obj); + expect(r.source).toBe("[object Object]"); +}); + +test("new RegExp stringifies object when Symbol.match is 0", () => { + const obj = { source: "abc", flags: "", [Symbol.match]: 0 }; + const r = new RegExp(obj); + expect(r.source).toBe("[object Object]"); +}); + +test("new RegExp stringifies object when Symbol.match is empty string", () => { + const obj = { source: "abc", flags: "", [Symbol.match]: "" }; + const r = new RegExp(obj); + expect(r.source).toBe("[object Object]"); +}); + +test("plain object with source and flags is not a RegExp instance", () => { + const fake = { source: "abc", flags: "g" }; + expect(() => { RegExp.prototype.exec.call(fake, "abc"); }).toThrow(TypeError); + expect(() => { RegExp.prototype.test.call(fake, "abc"); }).toThrow(TypeError); +}); diff --git a/tests/built-ins/String/prototype/endsWith.js b/tests/built-ins/String/prototype/endsWith.js index 9d72e5a5..e5f15ded 100644 --- a/tests/built-ins/String/prototype/endsWith.js +++ b/tests/built-ins/String/prototype/endsWith.js @@ -30,4 +30,16 @@ describe("String.prototype.endsWith", () => { test("entire string match", () => { expect("hello".endsWith("hello")).toBe(true); }); + + test("throws TypeError for regexp argument", () => { + expect(() => { "hello".endsWith(/lo/); }).toThrow(TypeError); + }); + + test("throws TypeError for object with Symbol.match truthy", () => { + expect(() => { "hello".endsWith({ [Symbol.match]: true }); }).toThrow(TypeError); + }); + + test("allows object with Symbol.match false", () => { + expect("hello".endsWith({ [Symbol.match]: false })).toBe(false); + }); }); diff --git a/tests/built-ins/String/prototype/includes.js b/tests/built-ins/String/prototype/includes.js index 583532de..a27aa532 100644 --- a/tests/built-ins/String/prototype/includes.js +++ b/tests/built-ins/String/prototype/includes.js @@ -38,4 +38,16 @@ describe("String.prototype.includes", () => { expect(String.prototype.includes.name).toBe("includes"); expect(String.prototype.includes.length).toBe(1); }); + + test("throws TypeError for regexp argument", () => { + expect(() => { "hello".includes(/ell/); }).toThrow(TypeError); + }); + + test("throws TypeError for object with Symbol.match truthy", () => { + expect(() => { "hello".includes({ [Symbol.match]: true }); }).toThrow(TypeError); + }); + + test("allows object with Symbol.match false", () => { + expect("hello".includes({ [Symbol.match]: false })).toBe(false); + }); }); diff --git a/tests/built-ins/String/prototype/matchAll.js b/tests/built-ins/String/prototype/matchAll.js index 11570ce3..0ad53235 100644 --- a/tests/built-ins/String/prototype/matchAll.js +++ b/tests/built-ins/String/prototype/matchAll.js @@ -149,3 +149,30 @@ test("matchAll throws TypeError when called on null or undefined", () => { expect(() => "".matchAll.call(null, /x/g)).toThrow(TypeError); expect(() => "".matchAll.call(undefined, /x/g)).toThrow(TypeError); }); + +test("matchAll rejects Symbol.match truthy object without global flag", () => { + expect(() => { + "abc".matchAll({ [Symbol.match]: true, flags: "" }); + }).toThrow(TypeError); +}); + +test("matchAll accepts Symbol.match truthy object with global flag", () => { + const obj = { + flags: "g", + [Symbol.match]: true, + [Symbol.matchAll](str) { return ["ok"]; }, + }; + expect("abc".matchAll(obj)).toEqual(["ok"]); +}); + +test("matchAll throws TypeError when IsRegExp object has null flags", () => { + expect(() => { + "abc".matchAll({ [Symbol.match]: true, flags: null }); + }).toThrow(TypeError); +}); + +test("matchAll throws TypeError when IsRegExp object has undefined flags", () => { + expect(() => { + "abc".matchAll({ [Symbol.match]: true }); + }).toThrow(TypeError); +}); diff --git a/tests/built-ins/String/prototype/replaceAll.js b/tests/built-ins/String/prototype/replaceAll.js index 3db7dfdd..59748812 100644 --- a/tests/built-ins/String/prototype/replaceAll.js +++ b/tests/built-ins/String/prototype/replaceAll.js @@ -116,4 +116,31 @@ describe('String.prototype.replaceAll', () => { expect(() => ''.replaceAll.call(null, /x/g, 'y')).toThrow(TypeError); expect(() => ''.replaceAll.call(undefined, /x/g, 'y')).toThrow(TypeError); }); + + test('rejects Symbol.match truthy object without global flag', () => { + expect(() => { + 'abc'.replaceAll({ [Symbol.match]: true, flags: '' }, 'x'); + }).toThrow(TypeError); + }); + + test('accepts Symbol.match truthy object with global flag', () => { + const obj = { + flags: 'g', + [Symbol.match]: true, + [Symbol.replace](str, rep) { return 'ok'; }, + }; + expect('abc'.replaceAll(obj, 'x')).toBe('ok'); + }); + + test('throws TypeError when IsRegExp object has null flags', () => { + expect(() => { + 'abc'.replaceAll({ [Symbol.match]: true, flags: null }, 'x'); + }).toThrow(TypeError); + }); + + test('throws TypeError when IsRegExp object has undefined flags', () => { + expect(() => { + 'abc'.replaceAll({ [Symbol.match]: true }, 'x'); + }).toThrow(TypeError); + }); }); diff --git a/tests/built-ins/String/prototype/startsWith.js b/tests/built-ins/String/prototype/startsWith.js index b03d09dd..57ac349e 100644 --- a/tests/built-ins/String/prototype/startsWith.js +++ b/tests/built-ins/String/prototype/startsWith.js @@ -26,4 +26,16 @@ describe("String.prototype.startsWith", () => { test("case sensitive", () => { expect("Hello".startsWith("hello")).toBe(false); }); + + test("throws TypeError for regexp argument", () => { + expect(() => { "hello".startsWith(/hel/); }).toThrow(TypeError); + }); + + test("throws TypeError for object with Symbol.match truthy", () => { + expect(() => { "hello".startsWith({ [Symbol.match]: true }); }).toThrow(TypeError); + }); + + test("allows object with Symbol.match false", () => { + expect("hello".startsWith({ [Symbol.match]: false })).toBe(false); + }); });