/** * Copyright (c) 2012-2016 André Bargull * Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms. * * <https://github.com/anba/es6draft> */ package com.github.anba.es6draft.runtime.objects.text; import static com.github.anba.es6draft.runtime.AbstractOperations.*; import static com.github.anba.es6draft.runtime.internal.Errors.newTypeError; import static com.github.anba.es6draft.runtime.internal.Properties.createProperties; import static com.github.anba.es6draft.runtime.objects.text.RegExpConstructor.EscapeRegExpPattern; import static com.github.anba.es6draft.runtime.objects.text.RegExpConstructor.RegExpCreate; import static com.github.anba.es6draft.runtime.objects.text.RegExpConstructor.RegExpInitialize; import static com.github.anba.es6draft.runtime.types.Null.NULL; import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED; import static com.github.anba.es6draft.runtime.types.builtins.ArrayObject.ArrayCreate; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.regex.MatchResult; import com.github.anba.es6draft.regexp.IterableMatchResult; import com.github.anba.es6draft.regexp.MatchState; import com.github.anba.es6draft.regexp.RegExpMatcher; import com.github.anba.es6draft.runtime.ExecutionContext; import com.github.anba.es6draft.runtime.Realm; import com.github.anba.es6draft.runtime.internal.CompatibilityOption; import com.github.anba.es6draft.runtime.internal.Initializable; import com.github.anba.es6draft.runtime.internal.Messages; import com.github.anba.es6draft.runtime.internal.Properties.Accessor; import com.github.anba.es6draft.runtime.internal.Properties.CompatibilityExtension; import com.github.anba.es6draft.runtime.internal.Properties.Function; import com.github.anba.es6draft.runtime.internal.Properties.Prototype; import com.github.anba.es6draft.runtime.internal.Properties.Value; import com.github.anba.es6draft.runtime.types.BuiltinSymbol; import com.github.anba.es6draft.runtime.types.Callable; import com.github.anba.es6draft.runtime.types.Constructor; import com.github.anba.es6draft.runtime.types.Intrinsics; import com.github.anba.es6draft.runtime.types.Property; import com.github.anba.es6draft.runtime.types.ScriptObject; import com.github.anba.es6draft.runtime.types.Type; import com.github.anba.es6draft.runtime.types.builtins.ArrayObject; import com.github.anba.es6draft.runtime.types.builtins.NativeFunction; import com.github.anba.es6draft.runtime.types.builtins.OrdinaryObject; /** * <h1>21 Text Processing</h1><br> * <h2>21.2 RegExp (Regular Expression) Objects</h2> * <ul> * <li>21.2.5 Properties of the RegExp Prototype Object * <li>21.2.6 Properties of RegExp Instances * </ul> */ public final class RegExpPrototype extends OrdinaryObject implements Initializable { /** * Constructs a new RegExp prototype object. * * @param realm * the realm object */ public RegExpPrototype(Realm realm) { super(realm); } @Override public void initialize(Realm realm) { createProperties(realm, this, Properties.class); createProperties(realm, this, AdditionalProperties.class); } private static RegExpObject thisRegExpObject(ExecutionContext cx, Object object) { if (object instanceof RegExpObject) { return (RegExpObject) object; } throw newTypeError(cx, Messages.Key.IncompatibleObject); } /** * 21.2.5 Properties of the RegExp Prototype Object */ public enum Properties { ; @Prototype public static final Intrinsics __proto__ = Intrinsics.ObjectPrototype; /** * 21.2.5.1 RegExp.prototype.constructor */ @Value(name = "constructor") public static final Intrinsics constructor = Intrinsics.RegExp; /** * 21.2.5.2 RegExp.prototype.exec ( string ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @return the match result array */ @Function(name = "exec", arity = 1, nativeId = RegExpPrototypeExec.class) public static Object exec(ExecutionContext cx, Object thisValue, Object string) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-5 */ String s = ToFlatString(cx, string); /* step 6 */ ArrayObject result = RegExpBuiltinExec(cx, r, s); return result != null ? result : NULL; } /** * 21.2.5.3 get RegExp.prototype.flags * * @param cx * the execution context * @param thisValue * the function this-value * @return the regular expressions flags */ @Accessor(name = "flags", type = Accessor.Type.Getter) public static Object flags(ExecutionContext cx, Object thisValue) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject r = Type.objectValue(thisValue); /* step 3 */ int c = 0; char[] result = new char[5]; /* steps 4-6 */ if (ToBoolean(Get(cx, r, "global"))) { result[c++] = 'g'; } /* steps 7-9 */ if (ToBoolean(Get(cx, r, "ignoreCase"))) { result[c++] = 'i'; } /* steps 10-12 */ if (ToBoolean(Get(cx, r, "multiline"))) { result[c++] = 'm'; } /* steps 13-15 */ if (ToBoolean(Get(cx, r, "unicode"))) { result[c++] = 'u'; } /* steps 16-18 */ if (ToBoolean(Get(cx, r, "sticky"))) { result[c++] = 'y'; } /* step 19 */ return String.valueOf(result, 0, c); } /** * 21.2.5.4 get RegExp.prototype.global * * @param cx * the execution context * @param thisValue * the function this-value * @return the global flag */ @Accessor(name = "global", type = Accessor.Type.Getter, nativeId = RegExpPrototypeGlobal.class) public static Object global(ExecutionContext cx, Object thisValue) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-6 */ return r.isSet(RegExpObject.Flags.Global); } /** * 21.2.5.5 get RegExp.prototype.ignoreCase * * @param cx * the execution context * @param thisValue * the function this-value * @return the ignoreCase flag */ @Accessor(name = "ignoreCase", type = Accessor.Type.Getter) public static Object ignoreCase(ExecutionContext cx, Object thisValue) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-6 */ return r.isSet(RegExpObject.Flags.IgnoreCase); } /** * 21.2.5.7 get RegExp.prototype.multiline * * @param cx * the execution context * @param thisValue * the function this-value * @return the multiline flag */ @Accessor(name = "multiline", type = Accessor.Type.Getter) public static Object multiline(ExecutionContext cx, Object thisValue) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-6 */ return r.isSet(RegExpObject.Flags.Multiline); } /** * 21.2.5.10 get RegExp.prototype.source * * @param cx * the execution context * @param thisValue * the function this-value * @return the source property */ @Accessor(name = "source", type = Accessor.Type.Getter) public static Object source(ExecutionContext cx, Object thisValue) { /* steps 1-4 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 5-7 */ return EscapeRegExpPattern(cx.getRealm(), r.getOriginalSource(), r.getOriginalFlags()); } /** * 21.2.5.12 get RegExp.prototype.sticky * * @param cx * the execution context * @param thisValue * the function this-value * @return the sticky flag */ @Accessor(name = "sticky", type = Accessor.Type.Getter, nativeId = RegExpPrototypeSticky.class) public static Object sticky(ExecutionContext cx, Object thisValue) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-6 */ return r.isSet(RegExpObject.Flags.Sticky); } /** * 21.2.5.13 RegExp.prototype.test ( S ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @return {@code true} if the string matches the pattern */ @Function(name = "test", arity = 1) public static Object test(ExecutionContext cx, Object thisValue, Object string) { /* step 1 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 2 */ ScriptObject r = Type.objectValue(thisValue); /* steps 3-4 */ // TODO: ToFlatString for small strings, ToString for large strings? String s = ToFlatString(cx, string); /* steps 5-6 */ MatchResult match = matchResultOrNull(cx, r, s, true); /* step 7 */ return match != null; } /** * 21.2.5.14 RegExp.prototype.toString() * * @param cx * the execution context * @param thisValue * the function this-value * @return the string representation */ @Function(name = "toString", arity = 0) public static Object toString(ExecutionContext cx, Object thisValue) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject r = Type.objectValue(thisValue); /* steps 3-4 */ CharSequence pattern = ToString(cx, Get(cx, r, "source")); /* steps 5-6 */ CharSequence flags = ToString(cx, Get(cx, r, "flags")); /* step 7 */ return new StringBuilder().append('/').append(pattern).append('/').append(flags).toString(); } /** * 21.2.5.15 get RegExp.prototype.unicode * * @param cx * the execution context * @param thisValue * the function this-value * @return the unicode flag */ @Accessor(name = "unicode", type = Accessor.Type.Getter) public static Object unicode(ExecutionContext cx, Object thisValue) { /* steps 1-3 */ RegExpObject r = thisRegExpObject(cx, thisValue); /* steps 4-6 */ return r.isSet(RegExpObject.Flags.Unicode); } /** * 21.2.5.6 RegExp.prototype[ @@match ] ( string ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @return the match result array */ @Function(name = "[Symbol.match]", symbol = BuiltinSymbol.match, arity = 1) public static Object match(ExecutionContext cx, Object thisValue, Object string) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject rx = Type.objectValue(thisValue); /* steps 3-4 */ String s = ToFlatString(cx, string); /* steps 5-6 */ boolean global = ToBoolean(Get(cx, rx, "global")); /* steps 7-8 */ if (!global) { ScriptObject result = RegExpExec(cx, rx, s); return result != null ? result : NULL; } else { /* steps 8.a-b */ boolean fullUnicode = ToBoolean(Get(cx, rx, "unicode")); /* steps 8.c-d */ Set(cx, rx, "lastIndex", 0, true); /* step 8.e */ ArrayObject array = ArrayCreate(cx, 0); /* steps 8.f-g */ for (int n = 0;; ++n) { // ScriptObject result = RegExpExec(cx, rx, s); MatchResult result = matchResultOrNull(cx, rx, s, true); if (result == null) { return n == 0 ? NULL : array; } // String matchStr = ToFlatString(cx, Get(cx, result, "0")); String matchStr = result.group(0); boolean status = CreateDataProperty(cx, array, n, matchStr); assert status; if (matchStr.isEmpty()) { long thisIndex = ToLength(cx, Get(cx, rx, "lastIndex")); long nextIndex = AdvanceStringIndex(s, thisIndex, fullUnicode); Set(cx, rx, "lastIndex", nextIndex, true); } } } } /** * 21.2.5.8 RegExp.prototype[ @@replace ] ( string, replaceValue ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @param replaceValue * the replace string or replacer function * @return the new string */ @Function(name = "[Symbol.replace]", symbol = BuiltinSymbol.replace, arity = 2) public static Object replace(ExecutionContext cx, Object thisValue, Object string, Object replaceValue) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject rx = Type.objectValue(thisValue); /* steps 3-4 */ String s = ToFlatString(cx, string); /* step 5 */ int lengthS = s.length(); /* step 6 */ boolean functionalReplace = IsCallable(replaceValue); /* step 7 */ String replaceValueString = null; Callable replaceValueCallable = null; if (!functionalReplace) { replaceValueString = ToFlatString(cx, replaceValue); } else { replaceValueCallable = (Callable) replaceValue; } /* steps 8-9 */ boolean global = ToBoolean(Get(cx, rx, "global")); /* step 10 */ boolean fullUnicode = false; if (global) { /* steps 10.a-b */ fullUnicode = ToBoolean(Get(cx, rx, "unicode")); /* steps 10.c-d */ Set(cx, rx, "lastIndex", 0, true); } /* step 11 */ ArrayList<MatchResult> results = new ArrayList<>(); /* step 12 */ boolean done = false; /* step 13 */ while (!done) { /* steps 13.a-b */ MatchResult result = matchResultOrNull(cx, rx, s, false); /* steps 13.c-d */ if (result == null) { /* step 13.c */ done = true; } else { /* step 13.d */ results.add(result); if (!global) { done = true; } else { String matchStr = result.group(0); if (matchStr.isEmpty()) { long thisIndex = ToLength(cx, Get(cx, rx, "lastIndex")); long nextIndex = AdvanceStringIndex(s, thisIndex, fullUnicode); Set(cx, rx, "lastIndex", nextIndex, true); } } } } // fast-path if no matches were found if (results.isEmpty()) { return s; } /* step 14 */ StringBuilder accumulatedResult = new StringBuilder(); /* step 15 */ int nextSourcePosition = 0; /* step 16 */ for (MatchResult result : results) { if (!(result instanceof ScriptObjectMatchResult)) { RegExpConstructor.storeLastMatchResult(cx, s, result); } /* steps 16.a-c */ int nCaptures = result.groupCount(); /* steps 16.d-e */ String matched = result.group(0); /* step 16.f */ int matchLength = matched.length(); /* steps 16.g-i */ int position = Math.max(Math.min(result.start(), lengthS), 0); /* steps 16.j-o */ String replacement; if (functionalReplace) { Object[] replacerArgs = GetReplacerArguments(matched, s, position, result, nCaptures); Object replValue = replaceValueCallable.call(cx, UNDEFINED, replacerArgs); replacement = ToFlatString(cx, replValue); } else { String[] captures = groups(result, nCaptures); replacement = GetSubstitution(matched, s, position, captures, replaceValueString); } /* step 16.p */ if (position >= nextSourcePosition) { accumulatedResult.append(s, nextSourcePosition, position).append(replacement); nextSourcePosition = position + matchLength; } } /* step 17 */ if (nextSourcePosition >= lengthS) { return accumulatedResult.toString(); } /* step 18 */ return accumulatedResult.append(s, nextSourcePosition, lengthS).toString(); } /** * 21.2.5.9 RegExp.prototype[ @@search ] ( string ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @return the string index of the first match */ @Function(name = "[Symbol.search]", symbol = BuiltinSymbol.search, arity = 1) public static Object search(ExecutionContext cx, Object thisValue, Object string) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject rx = Type.objectValue(thisValue); /* steps 3-4 */ String s = ToFlatString(cx, string); /* steps 5-6 */ Object previousLastIndex = Get(cx, rx, "lastIndex"); /* steps 7-8 */ Set(cx, rx, "lastIndex", 0, true); /* steps 9-10 */ MatchResult result = matchResultOrNull(cx, rx, s, true); /* steps 11-12 */ Set(cx, rx, "lastIndex", previousLastIndex, true); /* step 13 */ if (result == null) { return -1; } /* step 14 */ if (result instanceof ScriptObjectMatchResult) { // Extract wrapped script object to ensure no ToInteger conversion takes place ScriptObject object = ((ScriptObjectMatchResult) result).object; return Get(cx, object, "index"); } return result.start(); } /** * 21.2.5.11 RegExp.prototype[ @@split ] ( string, limit ) * * @param cx * the execution context * @param thisValue * the function this-value * @param string * the string * @param limit * the optional split array limit * @return the split array object */ @Function(name = "[Symbol.split]", symbol = BuiltinSymbol.split, arity = 2) public static Object split(ExecutionContext cx, Object thisValue, Object string, Object limit) { /* step 2 */ if (!Type.isObject(thisValue)) { throw newTypeError(cx, Messages.Key.NotObjectType); } /* step 1 */ ScriptObject rx = Type.objectValue(thisValue); /* steps 3-4 */ String s = ToFlatString(cx, string); /* steps 5-6 */ Constructor c = SpeciesConstructor(cx, rx, Intrinsics.RegExp); /* steps 7-8 */ String flags = ToFlatString(cx, Get(cx, rx, "flags")); /* steps 9-10 */ boolean unicodeMatching = flags.indexOf('u') != -1; /* steps 11-12 */ String newFlags = flags.indexOf('y') != -1 ? flags : flags + 'y'; // Optimization ScriptObject splitter; long lim; if (rx instanceof RegExpObject && c instanceof RegExpConstructor && c == cx.getIntrinsic(Intrinsics.RegExp)) { RegExpObject re = (RegExpObject) rx; RegExpConstructor reConstructor = (RegExpConstructor) c; // 21.2.3.1 RegExp ( pattern, flags ) - steps 1-2 // NB: Only executed for its side-effects. IsRegExp(cx, re); // Extract pattern and default multiline before calling ToLength. String pattern = re.getOriginalSource(); boolean defaultMultiline = RegExpConstructor.isDefaultMultiline(cx); /* steps 17-18 */ lim = Type.isUndefined(limit) ? 0xFFFF_FFFFL : ToUint32(cx, limit); // Test if required RegExp.prototype methods are still the default values. // Also test if pattern/flags match the original pattern/flags. Side-effects in // IsRegExp or ToLength may have called RegExp.prototype.compile. if (isBuiltinRegExpPrototypeForExec(cx) && pattern.equals(re.getOriginalSource()) && flags.equals(re.getOriginalFlags())) { return RegExpSplit(cx, re, s, unicodeMatching, lim); } // Optimization not applicable, fall back to slower spec algorithm. RegExpCreate // must be side-effect free, otherwise these steps are detectable from script code. splitter = RegExpCreate(cx, reConstructor, pattern, newFlags, defaultMultiline); } else { /* steps 13-14 */ splitter = c.construct(cx, c, rx, newFlags); /* steps 17-18 */ lim = Type.isUndefined(limit) ? 0xFFFF_FFFFL : ToUint32(cx, limit); } /* step 15 */ ArrayObject a = ArrayCreate(cx, 0); /* step 16 */ int lengthA = 0; /* steps 17-18 (above) */ /* step 19 */ int size = s.length(); /* step 20 */ int p = 0; /* step 21 */ if (lim == 0) { return a; } /* step 22 */ if (size == 0) { // ScriptObject z = RegExpExec(cx, splitter, s); MatchResult z = matchResultOrNull(cx, splitter, s, true); if (z != null) { return a; } CreateDataProperty(cx, a, 0, s); return a; } /* step 23 */ int q = p; /* step 24 */ while (q < size) { /* steps 24.a-b */ Set(cx, splitter, "lastIndex", q, true); /* steps 24.c-d */ // ScriptObject z = RegExpExec(cx, splitter, s); MatchResult z = matchResultOrNull(cx, splitter, s, true); /* step 24.e */ if (z == null) { q = AdvanceStringIndex(s, q, unicodeMatching); continue; } /* step 24.f */ /* step 24.f.i-ii */ int e = Math.min((int) ToLength(cx, Get(cx, splitter, "lastIndex")), size); /* step 24.f.iii */ if (e == p) { q = AdvanceStringIndex(s, q, unicodeMatching); continue; } /* step 24.f.iv */ String t = s.substring(p, q); CreateDataProperty(cx, a, lengthA, t); lengthA += 1; if (lengthA == lim) { return a; } Iterator<String> iterator = groupIterator(z, z.groupCount()); while (iterator.hasNext()) { Object nextCap = iterator.next(); CreateDataProperty(cx, a, lengthA, nextCap != null ? nextCap : UNDEFINED); lengthA += 1; if (lengthA == lim) { return a; } } p = e; q = p; } /* step 25 */ String t = s.substring(p, size); /* steps 26-27 */ CreateDataProperty(cx, a, lengthA, t); /* step 28 */ return a; } } /** * B.2.5 Additional Properties of the RegExp.prototype Object */ @CompatibilityExtension(CompatibilityOption.RegExpPrototype) public enum AdditionalProperties { ; /** * B.2.5.1 RegExp.prototype.compile (pattern, flags ) * * @param cx * the execution context * @param thisValue * the function this-value * @param pattern * the regular expression pattern * @param flags * the regular expression flags * @return the regular expression object */ @Function(name = "compile", arity = 2) public static Object compile(ExecutionContext cx, Object thisValue, Object pattern, Object flags) { /* step 1 (omitted) */ /* step 2 */ if (!(thisValue instanceof RegExpObject)) { throw newTypeError(cx, Messages.Key.IncompatibleObject); } RegExpObject r = (RegExpObject) thisValue; /* steps 3-4 */ Object p, f; if (pattern instanceof RegExpObject) { /* step 3 */ RegExpObject rx = (RegExpObject) pattern; if (!Type.isUndefined(flags)) { throw newTypeError(cx, Messages.Key.NotUndefined); } p = rx.getOriginalSource(); f = rx.getOriginalFlags(); } else { /* step 4 */ p = pattern; f = flags; } /* step 5 */ return RegExpInitialize(cx, r, p, f); } } /** * Marker class for {@code RegExp.prototype.exec}. */ private static final class RegExpPrototypeExec { } /** * Marker class for {@code RegExp.prototype.global}. */ private static final class RegExpPrototypeGlobal { } /** * Marker class for {@code RegExp.prototype.sticky}. */ private static final class RegExpPrototypeSticky { } private static boolean isBuiltinExec(Realm realm, Object value) { return NativeFunction.isNative(realm, value, RegExpPrototypeExec.class); } private static boolean isBuiltinGlobal(Realm realm, Object value) { return NativeFunction.isNative(realm, value, RegExpPrototypeGlobal.class); } private static boolean isBuiltinSticky(Realm realm, Object value) { return NativeFunction.isNative(realm, value, RegExpPrototypeSticky.class); } /** * 21.2.5.2.1 Runtime Semantics: RegExpExec ( R, S ) * * @param cx * the execution context * @param r * the regular expression object * @param s * the string * @return the match result object or null */ public static ScriptObject RegExpExec(ExecutionContext cx, ScriptObject r, String s) { /* steps 1-2 (not applicable) */ /* steps 3-4 */ Object exec = Get(cx, r, "exec"); /* step 5 */ // Don't take the slow path for built-in RegExp.prototype.exec if (IsCallable(exec) && !isBuiltinExec(cx.getRealm(), exec)) { return RegExpUserExec(cx, (Callable) exec, r, s); } /* step 6 */ RegExpObject rx = thisRegExpObject(cx, r); /* step 7 */ return RegExpBuiltinExec(cx, rx, s); } /** * 21.2.5.2.1 Runtime Semantics: RegExpExec ( R, S ) (1) * * @param cx * the execution context * @param r * the regular expression object * @param s * the string * @param storeResult * {@code true} if the match result is stored * @return the match result object or null */ private static MatchResult matchResultOrNull(ExecutionContext cx, ScriptObject r, String s, boolean storeResult) { /* steps 1-2 (not applicable) */ /* steps 3-4 */ Object exec = Get(cx, r, "exec"); /* step 5 */ // Don't take the slow path for built-in RegExp.prototype.exec if (IsCallable(exec) && !isBuiltinExec(cx.getRealm(), exec)) { ScriptObject o = RegExpUserExec(cx, (Callable) exec, r, s); return o != null ? new ScriptObjectMatchResult(cx, o) : null; } /* step 6 */ RegExpObject rx = thisRegExpObject(cx, r); /* step 7 */ return matchResultOrNull(cx, rx, s, storeResult); } private static ScriptObject RegExpUserExec(ExecutionContext cx, Callable exec, ScriptObject r, String s) { /* steps 5.a-5.b */ Object result = ((Callable) exec).call(cx, r, s); /* step 5.c */ if (!Type.isObjectOrNull(result)) { throw newTypeError(cx, Messages.Key.NotObjectOrNull); } /* step 5.d */ return Type.objectValueOrNull(result); } /** * 21.2.5.2.2 Runtime Semantics: RegExpBuiltinExec ( R, S ) * * @param cx * the execution context * @param r * the regular expression object * @param s * the string * @return the match result object or {@code null} */ private static ArrayObject RegExpBuiltinExec(ExecutionContext cx, RegExpObject r, String s) { /* steps 1-18 */ MatchResult m = matchResultOrNull(cx, r, s, true); if (m == null) { return null; } /* steps 19-29 */ return toMatchArray(cx, s, m); } /** * 21.2.5.2.2 Runtime Semantics: RegExpBuiltinExec ( R, S ) (1) * * @param cx * the execution context * @param r * the regular expression object * @param s * the string * @param storeResult * {@code true} if the match result is stored * @return the match result or {@code null} */ private static MatchResult matchResultOrNull(ExecutionContext cx, RegExpObject r, String s, boolean storeResult) { /* step 1 */ assert r.getRegExpMatcher() != null; /* steps 2-3 (not applicable) */ /* steps 4-5 */ int lastIndex = (int) Math.min(ToLength(cx, Get(cx, r, "lastIndex")), Integer.MAX_VALUE); /* steps 6-7 */ boolean global = ToBoolean(Get(cx, r, "global")); /* steps 8-9 */ boolean sticky = ToBoolean(Get(cx, r, "sticky")); /* steps 10-15 */ MatchState m = matchOrNull(r, s, lastIndex, global, sticky); /* step 15.a, 15.c */ if (m == null) { Set(cx, r, "lastIndex", 0, true); return null; } /* steps 16-17 */ int e = m.end(); /* step 18 */ if (global || sticky) { Set(cx, r, "lastIndex", e, true); } MatchResult result = m.toMatchResult(); if (storeResult) { RegExpConstructor.storeLastMatchResult(cx, s, result); } return result; } /** * 21.2.5.2.2 Runtime Semantics: RegExpBuiltinExec ( R, S ) (1) * * @param r * the regular expression object * @param s * the string * @param lastIndex * the lastIndex position * @return the match result or {@code null} */ private static MatchResult matchResultOrNull(RegExpObject r, String s, int lastIndex) { /* step 1 */ assert r.getRegExpMatcher() != null; /* steps 2-5 (not applicable) */ /* steps 6-7 */ boolean global = r.isSet(RegExpObject.Flags.Global); /* steps 8-9 */ boolean sticky = r.isSet(RegExpObject.Flags.Sticky); /* steps 10-15 */ return matchOrNull(r, s, lastIndex, global, sticky); } /** * 21.2.5.2.2 Runtime Semantics: RegExpBuiltinExec ( R, S ) (1) * * @param r * the regular expression object * @param s * the string * @param lastIndex * the lastIndex position * @param global * the global flag * @param sticky * the sticky flag * @return the match state or {@code null} */ private static MatchState matchOrNull(RegExpObject r, String s, int lastIndex, boolean global, boolean sticky) { /* step 1 */ assert r.getRegExpMatcher() != null; /* steps 2-9 (not applicable) */ /* step 10 */ if (!global && !sticky) { lastIndex = 0; } /* step 15.a */ if (lastIndex > s.length()) { return null; } /* step 11 */ RegExpMatcher matcher = r.getRegExpMatcher(); /* steps 12-13 (not applicable) */ /* steps 14-15 */ MatchState m = matcher.matcher(s); boolean matchSucceeded; if (!sticky) { matchSucceeded = m.find(lastIndex); } else { matchSucceeded = m.matches(lastIndex); } /* step 15.a, 15.c */ if (!matchSucceeded) { return null; } return m; } /** * 21.2.5.2.2 Runtime Semantics: RegExpBuiltinExec ( R, S ) (2) * * @param cx * the execution context * @param s * the string * @param m * the match result * @return the match result script object */ private static ArrayObject toMatchArray(ExecutionContext cx, String s, MatchResult m) { /* steps 16-17 */ int e = m.end(); /* step 19 */ int n = m.groupCount(); /* step 20 */ ArrayObject array = ArrayCreate(cx, n + 1); /* step 21 (omitted) */ /* step 22 */ int matchIndex = m.start(); /* steps 23-25 */ CreateDataProperty(cx, array, "index", matchIndex); CreateDataProperty(cx, array, "input", s); /* step 26 */ String matchedSubstr = s.substring(matchIndex, e); /* step 27 */ CreateDataProperty(cx, array, 0, matchedSubstr); /* step 28 */ Iterator<String> iterator = groupIterator(m, n); for (int i = 1; iterator.hasNext(); ++i) { String capture = iterator.next(); CreateDataProperty(cx, array, i, (capture != null ? capture : UNDEFINED)); } /* step 29 */ return array; } /** * 21.2.5.2.3 AdvanceStringIndex ( S, index, unicode ) * * @param s * the string * @param index * the current string index * @param unicode * the unicode flag * @return the next string index */ private static int AdvanceStringIndex(String s, int index, boolean unicode) { /* steps 1-3 (not applicable) */ /* step 4 */ if (!unicode) { return index + 1; } /* steps 5-6 */ if (index + 1 >= s.length()) { return index + 1; } /* steps 7-10 */ int p = index; if (!Character.isHighSurrogate(s.charAt(p)) || !Character.isLowSurrogate(s.charAt(p + 1))) { return index + 1; } /* step 12 */ return index + 2; } /** * 21.2.5.2.3 AdvanceStringIndex ( S, index, unicode ) * * @param s * the string * @param index * the current string index * @param unicode * the unicode flag * @return the next string index */ private static long AdvanceStringIndex(String s, long index, boolean unicode) { /* steps 1-3 (not applicable) */ /* step 4 */ if (!unicode) { return index + 1; } /* steps 5-6 */ if (index + 1 >= s.length()) { return index + 1; } /* steps 7-10 */ int p = (int) index; if (!Character.isHighSurrogate(s.charAt(p)) || !Character.isLowSurrogate(s.charAt(p + 1))) { return index + 1; } /* step 12 */ return index + 2; } private static boolean isBuiltinRegExpPrototypeForExec(ExecutionContext cx) { OrdinaryObject prototype = cx.getIntrinsic(Intrinsics.RegExpPrototype); Property execProp = prototype.getOwnProperty(cx, "exec"); if (execProp == null || !isBuiltinExec(cx.getRealm(), execProp.getValue())) { return false; } Property globalProp = prototype.getOwnProperty(cx, "global"); if (globalProp == null || !isBuiltinGlobal(cx.getRealm(), globalProp.getGetter())) { return false; } Property stickyProp = prototype.getOwnProperty(cx, "sticky"); if (stickyProp == null || !isBuiltinSticky(cx.getRealm(), stickyProp.getGetter())) { return false; } return true; } private static Object[] GetReplacerArguments(String matched, String string, int position, MatchResult matchResult, int groupCount) { Object[] arguments = new Object[groupCount + 3]; arguments[0] = matched; Iterator<String> iterator = groupIterator(matchResult, groupCount); for (int i = 1; iterator.hasNext(); ++i) { String group = iterator.next(); arguments[i] = (group != null ? group : UNDEFINED); } arguments[groupCount + 1] = position; arguments[groupCount + 2] = string; return arguments; } /** * 21.1.3.14.1 Runtime Semantics: GetSubstitution(matched, str, position, captures, replacement) * * @param matched * the matched substring * @param string * the input string * @param position * the match position * @param captures * the captured groups * @param replacement * the replace string * @return the replacement string */ private static String GetSubstitution(String matched, String string, int position, String[] captures, String replacement) { /* step 1 (not applicable) */ /* step 2 */ int matchLength = matched.length(); /* step 3 (not applicable) */ /* step 4 */ int stringLength = string.length(); /* step 5 */ assert position >= 0; /* step 6 */ assert position <= stringLength; /* steps 7-8 (not applicable) */ /* step 9 */ int tailPos = Math.min(position + matchLength, stringLength); /* step 10 */ int m = captures.length; /* step 11 */ int cursor = replacement.indexOf('$'); if (cursor < 0) { return replacement; } final int length = replacement.length(); int lastCursor = 0; StringBuilder result = new StringBuilder(); for (;;) { if (lastCursor < cursor) { result.append(replacement, lastCursor, cursor); } if (++cursor == length) { result.append('$'); break; } assert cursor < length; char c = replacement.charAt(cursor++); switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { int n = c - '0'; if (cursor < length) { char d = replacement.charAt(cursor); if (d >= (n == 0 ? '1' : '0') && d <= '9') { int nn = n * 10 + (d - '0'); if (nn <= m) { cursor += 1; n = nn; } } } if (n == 0 || n > m) { assert n >= 0 && n <= 9; result.append('$').append(c); } else { assert n >= 1 && n <= 99; String capture = captures[n - 1]; if (capture != null) { result.append(capture); } } break; } case '&': result.append(matched); break; case '`': result.append(string, 0, position); break; case '\'': result.append(string, tailPos, stringLength); break; case '$': result.append('$'); break; default: result.append('$').append(c); break; } lastCursor = cursor; cursor = replacement.indexOf('$', cursor); if (cursor < 0) { if (lastCursor < length) { result.append(replacement, lastCursor, length); } break; } } /* step 12 */ return result.toString(); } /** * Internal {@code RegExp.prototype.test()} function. * * @param cx * the execution context * @param rx * the regular expression object * @param string * the string * @return the result string */ public static boolean RegExpTest(ExecutionContext cx, RegExpObject rx, String string) { int lastIndex = 0; return matchResultOrNull(rx, string, lastIndex) != null; } /** * Internal {@code RegExp.prototype.replace()} function. * * @param cx * the execution context * @param rx * the regular expression object * @param string * the string * @param replaceValue * the replacement string * @return the result string */ public static String RegExpReplace(ExecutionContext cx, RegExpObject rx, String string, String replaceValue) { /* steps 1-5 (not applicable) */ /* step 6 */ int lengthS = string.length(); /* steps 7-8 (not applicable) */ /* steps 9-10 */ boolean global = rx.isSet(RegExpObject.Flags.Global); /* step 11 */ int lastIndex = 0; /* steps 12-14 (not applicable) */ /* step 16 */ StringBuilder accumulatedResult = new StringBuilder(); /* step 17 */ int nextSrcPosition = 0; /* step 18 (omitted) */ /* steps 15, 19 */ do { /* steps 15.a-15.b */ MatchResult result = matchResultOrNull(rx, string, lastIndex); /* step 15.c */ if (result == null) { break; } /* step 15.d */ String matched = result.group(0); int matchLength = matched.length(); lastIndex = (matchLength > 0 ? result.end() : result.end() + 1); /* step 15.e (not applicable) */ /* step 19 */ int position = result.start(); assert 0 <= position && position < lengthS; assert position >= nextSrcPosition; String[] captures = groups(result); String replacement = GetSubstitution(matched, string, position, captures, replaceValue); accumulatedResult.append(string, nextSrcPosition, position).append(replacement); nextSrcPosition = position + matchLength; } while (global); /* step 20 (not applicable) */ assert nextSrcPosition <= lengthS; /* step 21 */ return accumulatedResult.append(string, nextSrcPosition, lengthS).toString(); } /** * Internal {@code RegExp.prototype.split()} function. * * @param cx * the execution context * @param rx * the regular expression object * @param s * the string * @param unicodeMatching * {@code true} for unicode matching * @param lim * the split limit * @return the split result array */ private static ArrayObject RegExpSplit(ExecutionContext cx, RegExpObject rx, String s, boolean unicodeMatching, long lim) { /* steps 1-12, 17-18 (not applicable) */ /* step 15 */ ArrayObject a = ArrayCreate(cx, 0); /* step 16 */ int lengthA = 0; /* step 19 */ int size = s.length(); /* step 20 */ int p = 0; /* step 21 */ if (lim == 0) { return a; } /* step 22 */ if (size == 0) { MatchState matcher = rx.getRegExpMatcher().matcher(s); if (matcher.find(0)) { RegExpConstructor.storeLastMatchResult(cx, s, matcher.toMatchResult()); return a; } CreateDataProperty(cx, a, 0, s); return a; } /* step 23 */ int q = p; /* step 24 */ int lastStart = -1; MatchState matcher = rx.getRegExpMatcher().matcher(s); while (q != size) { /* steps 24.a-d */ boolean match = matcher.find(q); /* step 24.e (implicit) */ if (!match) { break; } RegExpConstructor.storeLastMatchResult(cx, s, matcher.toMatchResult()); /* steps 24.f.i-ii */ int e = matcher.end(); /* step 24.f.iii-iv */ if (e == p) { /* step 24.f.iii */ q = AdvanceStringIndex(s, q, unicodeMatching); } else { /* step 24.f.iv */ String t = s.substring(p, lastStart = matcher.start()); CreateDataProperty(cx, a, lengthA, t); lengthA += 1; if (lengthA == lim) { return a; } Iterator<String> iterator = groupIterator(matcher, matcher.groupCount()); while (iterator.hasNext()) { String cap = iterator.next(); CreateDataProperty(cx, a, lengthA, cap != null ? cap : UNDEFINED); lengthA += 1; if (lengthA == lim) { return a; } } p = e; q = p; } } if (lastStart == size) { return a; } /* step 25 */ String t = s.substring(p, size); /* steps 26-27 */ CreateDataProperty(cx, a, lengthA, t); /* step 28 */ return a; } private static final String[] EMPTY_GROUPS = new String[0]; /** * Returns the capturing groups of the {@link MatchResult} argument. * * @param matchResult * the match result * @return the match groups */ /*package*/static String[] groups(MatchResult matchResult) { return groups(matchResult, matchResult.groupCount()); } /** * Returns the capturing groups of the {@link MatchResult} argument. * * @param matchResult * the match result * @param groupCount * the number of capturing groups * @return the match groups */ private static String[] groups(MatchResult matchResult, int groupCount) { if (groupCount == 0) { return EMPTY_GROUPS; } Iterator<String> iterator = groupIterator(matchResult, groupCount); String[] groups = new String[groupCount]; for (int i = 0; iterator.hasNext(); ++i) { groups[i] = iterator.next(); } return groups; } private static Iterator<String> groupIterator(MatchResult matchResult, int groupCount) { if (groupCount == 0) { return Collections.emptyIterator(); } if (matchResult instanceof IterableMatchResult) { return ((IterableMatchResult) matchResult).iterator(); } return new GroupIterator(matchResult, groupCount); } private static final class GroupIterator implements Iterator<String> { private final MatchResult result; private final int groupCount; private int group = 1; GroupIterator(MatchResult result, int groupCount) { this.result = result; this.groupCount = groupCount; } @Override public boolean hasNext() { return group <= groupCount; } @Override public String next() { if (group > groupCount) { throw new NoSuchElementException(); } return result.group(group++); } } private static final class ScriptObjectMatchResult implements MatchResult { private final ExecutionContext cx; private final ScriptObject object; ScriptObjectMatchResult(ExecutionContext cx, ScriptObject object) { this.cx = cx; this.object = object; } @Override public int start() { return start(0); } @Override public int start(int group) { if (group == 0) { return (int) ToInteger(cx, Get(cx, object, "index")); } throw new UnsupportedOperationException(); } @Override public int end() { return end(0); } @Override public int end(int group) { return start(group) + group(group).length(); } @Override public String group() { return group(0); } @Override public String group(int group) { Object captured = Get(cx, object, group); if (group > 0 && Type.isUndefined(captured)) { return null; } return ToFlatString(cx, captured); } @Override public int groupCount() { long nCaptures = ToLength(cx, Get(cx, object, "length")); return (int) Math.max(nCaptures - 1, 0); } } }