/* * Copyright (C) 2016-2018 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @globalPrivate @constructor function RegExpStringIterator(regExp, string, global, fullUnicode) { "use strict"; @putByIdDirectPrivate(this, "regExpStringIteratorRegExp", regExp); @putByIdDirectPrivate(this, "regExpStringIteratorString", string); @putByIdDirectPrivate(this, "regExpStringIteratorGlobal", global); @putByIdDirectPrivate(this, "regExpStringIteratorUnicode", fullUnicode); @putByIdDirectPrivate(this, "regExpStringIteratorDone", false); } @globalPrivate function advanceStringIndex(string, index, unicode) { // This function implements AdvanceStringIndex described in ES6 21.2.5.2.3. "use strict"; if (!unicode) return index + 1; if (index + 1 >= string.length) return index + 1; let first = string.@charCodeAt(index); if (first < 0xD800 || first > 0xDBFF) return index + 1; let second = string.@charCodeAt(index + 1); if (second < 0xDC00 || second > 0xDFFF) return index + 1; return index + 2; } @globalPrivate function regExpExec(regexp, str) { "use strict"; let exec = regexp.exec; let builtinExec = @regExpBuiltinExec; if (exec !== builtinExec && typeof exec === "function") { let result = exec.@call(regexp, str); if (result !== null && !@isObject(result)) @throwTypeError("The result of a RegExp exec must be null or an object"); return result; } return builtinExec.@call(regexp, str); } @globalPrivate function hasObservableSideEffectsForRegExpMatch(regexp) { "use strict"; if (!@isRegExpObject(regexp)) return true; // This is accessed by the RegExpExec internal function. let regexpExec = @tryGetById(regexp, "exec"); if (regexpExec !== @regExpBuiltinExec) return true; let regexpGlobal = @tryGetById(regexp, "global"); if (regexpGlobal !== @regExpProtoGlobalGetter) return true; let regexpUnicode = @tryGetById(regexp, "unicode"); if (regexpUnicode !== @regExpProtoUnicodeGetter) return true; return typeof regexp.lastIndex !== "number"; } @globalPrivate function matchSlow(regexp, str) { "use strict"; if (!regexp.global) return @regExpExec(regexp, str); let unicode = regexp.unicode; regexp.lastIndex = 0; let resultList = []; // FIXME: It would be great to implement a solution similar to what we do in // RegExpObject::matchGlobal(). It's not clear if this is possible, since this loop has // effects. https://bugs.webkit.org/show_bug.cgi?id=158145 const maximumReasonableMatchSize = 100000000; while (true) { let result = @regExpExec(regexp, str); if (result === null) { if (resultList.length === 0) return null; return resultList; } if (resultList.length > maximumReasonableMatchSize) @throwOutOfMemoryError(); let resultString = @toString(result[0]); if (!resultString.length) regexp.lastIndex = @advanceStringIndex(str, regexp.lastIndex, unicode); resultList.@push(resultString); } } @overriddenName="[Symbol.match]" function match(strArg) { "use strict"; if (!@isObject(this)) @throwTypeError("RegExp.prototype.@@match requires that |this| be an Object"); let str = @toString(strArg); // Check for observable side effects and call the fast path if there aren't any. if (!@hasObservableSideEffectsForRegExpMatch(this)) return @regExpMatchFast.@call(this, str); return @matchSlow(this, str); } @overriddenName="[Symbol.matchAll]" function matchAll(strArg) { "use strict"; let regExp = this; if (!@isObject(regExp)) @throwTypeError("RegExp.prototype.@@matchAll requires |this| to be an Object"); let string = @toString(strArg); let Matcher = @speciesConstructor(regExp, @RegExp); let flags = @toString(regExp.flags); let matcher = new Matcher(regExp, flags); matcher.lastIndex = @toLength(regExp.lastIndex); let global = @stringIncludesInternal.@call(flags, "g"); let fullUnicode = @stringIncludesInternal.@call(flags, "u"); return new @RegExpStringIterator(matcher, string, global, fullUnicode); } @overriddenName="[Symbol.replace]" function replace(strArg, replace) { "use strict"; function getSubstitution(matched, str, position, captures, replacement) { "use strict"; let matchLength = matched.length; let stringLength = str.length; let tailPos = position + matchLength; let m = captures.length; let replacementLength = replacement.length; let result = ""; let lastStart = 0; for (let start = 0; start = replacement.indexOf("$", lastStart), start !== -1; lastStart = start) { if (start - lastStart > 0) result = result + replacement.substring(lastStart, start); start++; let ch = replacement.charAt(start); if (ch === "") result = result + "$"; else { switch (ch) { case "$": result = result + "$"; start++; break; case "&": result = result + matched; start++; break; case "`": if (position > 0) result = result + str.substring(0, position); start++; break; case "'": if (tailPos < stringLength) result = result + str.substring(tailPos); start++; break; default: let chCode = ch.charCodeAt(0); if (chCode >= 0x30 && chCode <= 0x39) { start++; let n = chCode - 0x30; if (n > m) break; if (start < replacementLength) { let nextChCode = replacement.charCodeAt(start); if (nextChCode >= 0x30 && nextChCode <= 0x39) { let nn = 10 * n + nextChCode - 0x30; if (nn <= m) { n = nn; start++; } } } if (n == 0) break; if (captures[n] != @undefined) result = result + captures[n]; } else result = result + "$"; break; } } } return result + replacement.substring(lastStart); } if (!@isObject(this)) @throwTypeError("RegExp.prototype.@@replace requires that |this| be an Object"); let regexp = this; let str = @toString(strArg); let stringLength = str.length; let functionalReplace = typeof replace === 'function'; if (!functionalReplace) replace = @toString(replace); let global = regexp.global; let unicode = false; if (global) { unicode = regexp.unicode; regexp.lastIndex = 0; } let resultList = []; let result; let done = false; while (!done) { result = @regExpExec(regexp, str); if (result === null) done = true; else { resultList.@push(result); if (!global) done = true; else { let matchStr = @toString(result[0]); if (!matchStr.length) regexp.lastIndex = @advanceStringIndex(str, regexp.lastIndex, unicode); } } } let accumulatedResult = ""; let nextSourcePosition = 0; let lastPosition = 0; for (let i = 0, resultListLength = resultList.length; i < resultListLength; ++i) { let result = resultList[i]; let nCaptures = result.length - 1; if (nCaptures < 0) nCaptures = 0; let matched = @toString(result[0]); let matchLength = matched.length; let position = result.index; position = (position > stringLength) ? stringLength : position; position = (position < 0) ? 0 : position; let captures = []; for (let n = 1; n <= nCaptures; n++) { let capN = result[n]; if (capN !== @undefined) capN = @toString(capN); captures[n] = capN; } let replacement; if (functionalReplace) { let replacerArgs = [ matched ].concat(captures.slice(1)); replacerArgs.@push(position); replacerArgs.@push(str); let replValue = replace.@apply(@undefined, replacerArgs); replacement = @toString(replValue); } else replacement = getSubstitution(matched, str, position, captures, replace); if (position >= nextSourcePosition && position >= lastPosition) { accumulatedResult = accumulatedResult + str.substring(nextSourcePosition, position) + replacement; nextSourcePosition = position + matchLength; lastPosition = position; } } if (nextSourcePosition >= stringLength) return accumulatedResult; return accumulatedResult + str.substring(nextSourcePosition); } // 21.2.5.9 RegExp.prototype[@@search] (string) @overriddenName="[Symbol.search]" function search(strArg) { "use strict"; let regexp = this; // Check for observable side effects and call the fast path if there aren't any. if (@isRegExpObject(regexp) && @tryGetById(regexp, "exec") === @regExpBuiltinExec && typeof regexp.lastIndex === "number") return @regExpSearchFast.@call(regexp, strArg); // 1. Let rx be the this value. // 2. If Type(rx) is not Object, throw a TypeError exception. if (!@isObject(this)) @throwTypeError("RegExp.prototype.@@search requires that |this| be an Object"); // 3. Let S be ? ToString(string). let str = @toString(strArg) // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). let previousLastIndex = regexp.lastIndex; // 5.If SameValue(previousLastIndex, 0) is false, then // 5.a. Perform ? Set(rx, "lastIndex", 0, true). // FIXME: Add SameValue support. https://bugs.webkit.org/show_bug.cgi?id=173226 if (previousLastIndex !== 0) regexp.lastIndex = 0; // 6. Let result be ? RegExpExec(rx, S). let result = @regExpExec(regexp, str); // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then // 8.a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). // FIXME: Add SameValue support. https://bugs.webkit.org/show_bug.cgi?id=173226 if (regexp.lastIndex !== previousLastIndex) regexp.lastIndex = previousLastIndex; // 9. If result is null, return -1. if (result === null) return -1; // 10. Return ? Get(result, "index"). return result.index; } @globalPrivate function hasObservableSideEffectsForRegExpSplit(regexp) { "use strict"; if (!@isRegExpObject(regexp)) return true; // This is accessed by the RegExpExec internal function. let regexpExec = @tryGetById(regexp, "exec"); if (regexpExec !== @regExpBuiltinExec) return true; // This is accessed by step 5 below. let regexpFlags = @tryGetById(regexp, "flags"); if (regexpFlags !== @regExpProtoFlagsGetter) return true; // These are accessed by the builtin flags getter. let regexpGlobal = @tryGetById(regexp, "global"); if (regexpGlobal !== @regExpProtoGlobalGetter) return true; let regexpIgnoreCase = @tryGetById(regexp, "ignoreCase"); if (regexpIgnoreCase !== @regExpProtoIgnoreCaseGetter) return true; let regexpMultiline = @tryGetById(regexp, "multiline"); if (regexpMultiline !== @regExpProtoMultilineGetter) return true; let regexpSticky = @tryGetById(regexp, "sticky"); if (regexpSticky !== @regExpProtoStickyGetter) return true; let regexpUnicode = @tryGetById(regexp, "unicode"); if (regexpUnicode !== @regExpProtoUnicodeGetter) return true; // This is accessed by the RegExp species constructor. let regexpSource = @tryGetById(regexp, "source"); if (regexpSource !== @regExpProtoSourceGetter) return true; return typeof regexp.lastIndex !== "number"; } // ES 21.2.5.11 RegExp.prototype[@@split](string, limit) @overriddenName="[Symbol.split]" function split(string, limit) { "use strict"; // 1. Let rx be the this value. // 2. If Type(rx) is not Object, throw a TypeError exception. if (!@isObject(this)) @throwTypeError("RegExp.prototype.@@split requires that |this| be an Object"); let regexp = this; // 3. Let S be ? ToString(string). let str = @toString(string); // 4. Let C be ? SpeciesConstructor(rx, %RegExp%). let speciesConstructor = @speciesConstructor(regexp, @RegExp); if (speciesConstructor === @RegExp && !@hasObservableSideEffectsForRegExpSplit(regexp)) return @regExpSplitFast.@call(regexp, str, limit); // 5. Let flags be ? ToString(? Get(rx, "flags")). let flags = @toString(regexp.flags); // 6. If flags contains "u", let unicodeMatching be true. // 7. Else, let unicodeMatching be false. let unicodeMatching = @stringIncludesInternal.@call(flags, "u"); // 8. If flags contains "y", let newFlags be flags. // 9. Else, let newFlags be the string that is the concatenation of flags and "y". let newFlags = @stringIncludesInternal.@call(flags, "y") ? flags : flags + "y"; // 10. Let splitter be ? Construct(C, « rx, newFlags »). let splitter = new speciesConstructor(regexp, newFlags); // We need to check again for RegExp subclasses that will fail the speciesConstructor test // but can still use the fast path after we invoke the constructor above. if (!@hasObservableSideEffectsForRegExpSplit(splitter)) return @regExpSplitFast.@call(splitter, str, limit); // 11. Let A be ArrayCreate(0). // 12. Let lengthA be 0. let result = []; // 13. If limit is undefined, let lim be 2^32-1; else let lim be ? ToUint32(limit). limit = (limit === @undefined) ? 0xffffffff : limit >>> 0; // 16. If lim = 0, return A. if (!limit) return result; // 14. [Defered from above] Let size be the number of elements in S. let size = str.length; // 17. If size = 0, then if (!size) { // a. Let z be ? RegExpExec(splitter, S). let z = @regExpExec(splitter, str); // b. If z is not null, return A. if (z != null) return result; // c. Perform ! CreateDataProperty(A, "0", S). @putByValDirect(result, 0, str); // d. Return A. return result; } // 15. [Defered from above] Let p be 0. let position = 0; // 18. Let q be p. let matchPosition = 0; // 19. Repeat, while q < size while (matchPosition < size) { // a. Perform ? Set(splitter, "lastIndex", q, true). splitter.lastIndex = matchPosition; // b. Let z be ? RegExpExec(splitter, S). let matches = @regExpExec(splitter, str); // c. If z is null, let q be AdvanceStringIndex(S, q, unicodeMatching). if (matches === null) matchPosition = @advanceStringIndex(str, matchPosition, unicodeMatching); // d. Else z is not null, else { // i. Let e be ? ToLength(? Get(splitter, "lastIndex")). let endPosition = @toLength(splitter.lastIndex); // ii. Let e be min(e, size). endPosition = (endPosition <= size) ? endPosition : size; // iii. If e = p, let q be AdvanceStringIndex(S, q, unicodeMatching). if (endPosition === position) matchPosition = @advanceStringIndex(str, matchPosition, unicodeMatching); // iv. Else e != p, else { // 1. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through q (exclusive). let subStr = @stringSubstrInternal.@call(str, position, matchPosition - position); // 2. Perform ! CreateDataProperty(A, ! ToString(lengthA), T). // 3. Let lengthA be lengthA + 1. @putByValDirect(result, result.length, subStr); // 4. If lengthA = lim, return A. if (result.length == limit) return result; // 5. Let p be e. position = endPosition; // 6. Let numberOfCaptures be ? ToLength(? Get(z, "length")). // 7. Let numberOfCaptures be max(numberOfCaptures-1, 0). let numberOfCaptures = matches.length > 1 ? matches.length - 1 : 0; // 8. Let i be 1. let i = 1; // 9. Repeat, while i <= numberOfCaptures, while (i <= numberOfCaptures) { // a. Let nextCapture be ? Get(z, ! ToString(i)). let nextCapture = matches[i]; // b. Perform ! CreateDataProperty(A, ! ToString(lengthA), nextCapture). // d. Let lengthA be lengthA + 1. @putByValDirect(result, result.length, nextCapture); // e. If lengthA = lim, return A. if (result.length == limit) return result; // c. Let i be i + 1. i++; } // 10. Let q be p. matchPosition = position; } } } // 20. Let T be a String value equal to the substring of S consisting of the elements at indices p (inclusive) through size (exclusive). let remainingStr = @stringSubstrInternal.@call(str, position, size); // 21. Perform ! CreateDataProperty(A, ! ToString(lengthA), T). @putByValDirect(result, result.length, remainingStr); // 22. Return A. return result; } // ES 21.2.5.13 RegExp.prototype.test(string) @intrinsic=RegExpTestIntrinsic function test(strArg) { "use strict"; let regexp = this; // Check for observable side effects and call the fast path if there aren't any. if (@isRegExpObject(regexp) && @tryGetById(regexp, "exec") === @regExpBuiltinExec && typeof regexp.lastIndex === "number") return @regExpTestFast.@call(regexp, strArg); // 1. Let R be the this value. // 2. If Type(R) is not Object, throw a TypeError exception. if (!@isObject(regexp)) @throwTypeError("RegExp.prototype.test requires that |this| be an Object"); // 3. Let string be ? ToString(S). let str = @toString(strArg); // 4. Let match be ? RegExpExec(R, string). let match = @regExpExec(regexp, str); // 5. If match is not null, return true; else return false. if (match !== null) return true; return false; }