1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.objects;
  27 
  28 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
  29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
  30 
  31 import java.util.ArrayList;
  32 import java.util.Arrays;
  33 import java.util.List;
  34 
  35 import jdk.nashorn.internal.objects.annotations.Attribute;
  36 import jdk.nashorn.internal.objects.annotations.Constructor;
  37 import jdk.nashorn.internal.objects.annotations.Function;
  38 import jdk.nashorn.internal.objects.annotations.Getter;
  39 import jdk.nashorn.internal.objects.annotations.Property;
  40 import jdk.nashorn.internal.objects.annotations.ScriptClass;
  41 import jdk.nashorn.internal.objects.annotations.SpecializedConstructor;
  42 import jdk.nashorn.internal.objects.annotations.Where;
  43 import jdk.nashorn.internal.runtime.BitVector;
  44 import jdk.nashorn.internal.runtime.JSType;
  45 import jdk.nashorn.internal.runtime.ParserException;
  46 import jdk.nashorn.internal.runtime.PropertyMap;
  47 import jdk.nashorn.internal.runtime.regexp.RegExp;
  48 import jdk.nashorn.internal.runtime.regexp.RegExpFactory;
  49 import jdk.nashorn.internal.runtime.regexp.RegExpResult;
  50 import jdk.nashorn.internal.runtime.regexp.RegExpMatcher;
  51 import jdk.nashorn.internal.runtime.ScriptFunction;
  52 import jdk.nashorn.internal.runtime.ScriptObject;
  53 import jdk.nashorn.internal.runtime.ScriptRuntime;
  54 
  55 /**
  56  * ECMA 15.10 RegExp Objects.
  57  */
  58 @ScriptClass("RegExp")
  59 public final class NativeRegExp extends ScriptObject {
  60     /** ECMA 15.10.7.5 lastIndex property */
  61     @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE)
  62     public Object lastIndex;
  63 
  64     /** Compiled regexp */
  65     private RegExp regexp;
  66 
  67     // Reference to global object needed to support static RegExp properties
  68     private final Global globalObject;
  69 
  70     // initialized by nasgen
  71     private static PropertyMap $nasgenmap$;
  72 
  73     static PropertyMap getInitialMap() {
  74         return $nasgenmap$;
  75     }
  76 
  77     private NativeRegExp(final Global global) {
  78         super(global.getRegExpPrototype(), getInitialMap());
  79         this.globalObject = global;
  80     }
  81 
  82     NativeRegExp(final String input, final String flagString, final Global global) {
  83         this(global);
  84         try {
  85             this.regexp = RegExpFactory.create(input, flagString);
  86         } catch (final ParserException e) {
  87             // translate it as SyntaxError object and throw it
  88             e.throwAsEcmaException();
  89             throw new AssertionError(); //guard against null warnings below
  90         }
  91 
  92         this.setLastIndex(0);
  93     }
  94 
  95     NativeRegExp(final String input, final String flagString) {
  96         this(input, flagString, Global.instance());
  97     }
  98 
  99     NativeRegExp(final String string, final Global global) {
 100         this(string, "", global);
 101     }
 102 
 103     NativeRegExp(final String string) {
 104         this(string, Global.instance());
 105     }
 106 
 107     NativeRegExp(final NativeRegExp regExp) {
 108         this(Global.instance());
 109         this.lastIndex  = regExp.getLastIndexObject();
 110         this.regexp      = regExp.getRegExp();
 111     }
 112 
 113     @Override
 114     public String getClassName() {
 115         return "RegExp";
 116     }
 117 
 118     /**
 119      * ECMA 15.10.4
 120      *
 121      * Constructor
 122      *
 123      * @param isNew is the new operator used for instantiating this regexp
 124      * @param self  self reference
 125      * @param args  arguments (optional: pattern and flags)
 126      * @return new NativeRegExp
 127      */
 128     @Constructor(arity = 2)
 129     public static Object constructor(final boolean isNew, final Object self, final Object... args) {
 130         if (args.length > 1) {
 131             return newRegExp(args[0], args[1]);
 132         } else if (args.length > 0) {
 133             return newRegExp(args[0], UNDEFINED);
 134         }
 135 
 136         return newRegExp(UNDEFINED, UNDEFINED);
 137     }
 138 
 139     /**
 140      * ECMA 15.10.4
 141      *
 142      * Constructor - specialized version, no args, empty regexp
 143      *
 144      * @param isNew is the new operator used for instantiating this regexp
 145      * @param self  self reference
 146      * @return new NativeRegExp
 147      */
 148     @SpecializedConstructor
 149     public static Object constructor(final boolean isNew, final Object self) {
 150         return new NativeRegExp("", "");
 151     }
 152 
 153     /**
 154      * ECMA 15.10.4
 155      *
 156      * Constructor - specialized version, pattern, no flags
 157      *
 158      * @param isNew is the new operator used for instantiating this regexp
 159      * @param self  self reference
 160      * @param pattern pattern
 161      * @return new NativeRegExp
 162      */
 163     @SpecializedConstructor
 164     public static Object constructor(final boolean isNew, final Object self, final Object pattern) {
 165         return newRegExp(pattern, UNDEFINED);
 166     }
 167 
 168     /**
 169      * ECMA 15.10.4
 170      *
 171      * Constructor - specialized version, pattern and flags
 172      *
 173      * @param isNew is the new operator used for instantiating this regexp
 174      * @param self  self reference
 175      * @param pattern pattern
 176      * @param flags  flags
 177      * @return new NativeRegExp
 178      */
 179     @SpecializedConstructor
 180     public static Object constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) {
 181         return newRegExp(pattern, flags);
 182     }
 183 
 184     /**
 185      * External constructor used in generated code, which explains the public access
 186      *
 187      * @param regexp regexp
 188      * @param flags  flags
 189      * @return new NativeRegExp
 190      */
 191     public static NativeRegExp newRegExp(final Object regexp, final Object flags) {
 192         String  patternString = "";
 193         String  flagString    = "";
 194 
 195         if (regexp != UNDEFINED) {
 196             if (regexp instanceof NativeRegExp) {
 197                 if (flags != UNDEFINED) {
 198                     throw typeError("regex.cant.supply.flags");
 199                 }
 200                 return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as
 201             }
 202             patternString = JSType.toString(regexp);
 203         }
 204 
 205         if (flags != UNDEFINED) {
 206             flagString = JSType.toString(flags);
 207         }
 208 
 209         return new NativeRegExp(patternString, flagString);
 210     }
 211 
 212     /**
 213      * Build a regexp that matches {@code string} as-is. All meta-characters will be escaped.
 214      *
 215      * @param string pattern string
 216      * @return flat regexp
 217      */
 218     static NativeRegExp flatRegExp(String string) {
 219         // escape special characters
 220         StringBuilder sb = null;
 221         final int length = string.length();
 222 
 223         for (int i = 0; i < length; i++) {
 224             final char c = string.charAt(i);
 225             switch (c) {
 226                 case '^':
 227                 case '$':
 228                 case '\\':
 229                 case '.':
 230                 case '*':
 231                 case '+':
 232                 case '?':
 233                 case '(':
 234                 case ')':
 235                 case '[':
 236                 case '{':
 237                 case '|':
 238                     if (sb == null) {
 239                         sb = new StringBuilder(length * 2);
 240                         sb.append(string, 0, i);
 241                     }
 242                     sb.append('\\');
 243                     sb.append(c);
 244                     break;
 245                 default:
 246                     if (sb != null) {
 247                         sb.append(c);
 248                     }
 249                     break;
 250             }
 251         }
 252         return new NativeRegExp(sb == null ? string : sb.toString(), "");
 253     }
 254 
 255     private String getFlagString() {
 256         final StringBuilder sb = new StringBuilder(3);
 257 
 258         if (regexp.isGlobal()) {
 259             sb.append('g');
 260         }
 261         if (regexp.isIgnoreCase()) {
 262             sb.append('i');
 263         }
 264         if (regexp.isMultiline()) {
 265             sb.append('m');
 266         }
 267 
 268         return sb.toString();
 269     }
 270 
 271     @Override
 272     public String safeToString() {
 273         return "[RegExp " + toString() + "]";
 274     }
 275 
 276     @Override
 277     public String toString() {
 278         return "/" + regexp.getSource() + "/" + getFlagString();
 279     }
 280 
 281     /**
 282      * Nashorn extension: RegExp.prototype.compile - everybody implements this!
 283      *
 284      * @param self    self reference
 285      * @param pattern pattern
 286      * @param flags   flags
 287      * @return new NativeRegExp
 288      */
 289     @Function(attributes = Attribute.NOT_ENUMERABLE)
 290     public static Object compile(final Object self, final Object pattern, final Object flags) {
 291         final NativeRegExp regExp   = checkRegExp(self);
 292         final NativeRegExp compiled = newRegExp(pattern, flags);
 293         // copy over regexp to 'self'
 294         regExp.setRegExp(compiled.getRegExp());
 295 
 296         // Some implementations return undefined. Some return 'self'. Since return
 297         // value is most likely be ignored, we can play safe and return 'self'.
 298         return regExp;
 299     }
 300 
 301     /**
 302      * ECMA 15.10.6.2 RegExp.prototype.exec(string)
 303      *
 304      * @param self   self reference
 305      * @param string string to match against regexp
 306      * @return array containing the matches or {@code null} if no match
 307      */
 308     @Function(attributes = Attribute.NOT_ENUMERABLE)
 309     public static Object exec(final Object self, final Object string) {
 310         return checkRegExp(self).exec(JSType.toString(string));
 311     }
 312 
 313     /**
 314      * ECMA 15.10.6.3 RegExp.prototype.test(string)
 315      *
 316      * @param self   self reference
 317      * @param string string to test for matches against regexp
 318      * @return true if matches found, false otherwise
 319      */
 320     @Function(attributes = Attribute.NOT_ENUMERABLE)
 321     public static Object test(final Object self, final Object string) {
 322         return checkRegExp(self).test(JSType.toString(string));
 323     }
 324 
 325     /**
 326      * ECMA 15.10.6.4 RegExp.prototype.toString()
 327      *
 328      * @param self self reference
 329      * @return string version of regexp
 330      */
 331     @Function(attributes = Attribute.NOT_ENUMERABLE)
 332     public static Object toString(final Object self) {
 333         return checkRegExp(self).toString();
 334     }
 335 
 336     /**
 337      * ECMA 15.10.7.1 source
 338      *
 339      * @param self self reference
 340      * @return the input string for the regexp
 341      */
 342     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 343     public static Object source(final Object self) {
 344         return checkRegExp(self).getRegExp().getSource();
 345     }
 346 
 347     /**
 348      * ECMA 15.10.7.2 global
 349      *
 350      * @param self self reference
 351      * @return true if this regexp is flagged global, false otherwise
 352      */
 353     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 354     public static Object global(final Object self) {
 355         return checkRegExp(self).getRegExp().isGlobal();
 356     }
 357 
 358     /**
 359      * ECMA 15.10.7.3 ignoreCase
 360      *
 361      * @param self self reference
 362      * @return true if this regexp if flagged to ignore case, false otherwise
 363      */
 364     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 365     public static Object ignoreCase(final Object self) {
 366         return checkRegExp(self).getRegExp().isIgnoreCase();
 367     }
 368 
 369     /**
 370      * ECMA 15.10.7.4 multiline
 371      *
 372      * @param self self reference
 373      * @return true if this regexp is flagged to be multiline, false otherwise
 374      */
 375     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 376     public static Object multiline(final Object self) {
 377         return checkRegExp(self).getRegExp().isMultiline();
 378     }
 379 
 380     /**
 381      * Getter for non-standard RegExp.input property.
 382      * @param self self object
 383      * @return last regexp input
 384      */
 385     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input")
 386     public static Object getLastInput(Object self) {
 387         final RegExpResult match = Global.instance().getLastRegExpResult();
 388         return match == null ? "" : match.getInput();
 389     }
 390 
 391     /**
 392      * Getter for non-standard RegExp.multiline property.
 393      * @param self self object
 394      * @return last regexp input
 395      */
 396     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline")
 397     public static Object getLastMultiline(Object self) {
 398         return false; // doesn't ever seem to become true and isn't documented anyhwere
 399     }
 400 
 401     /**
 402      * Getter for non-standard RegExp.lastMatch property.
 403      * @param self self object
 404      * @return last regexp input
 405      */
 406     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch")
 407     public static Object getLastMatch(Object self) {
 408         final RegExpResult match = Global.instance().getLastRegExpResult();
 409         return match == null ? "" : match.getGroup(0);
 410     }
 411 
 412     /**
 413      * Getter for non-standard RegExp.lastParen property.
 414      * @param self self object
 415      * @return last regexp input
 416      */
 417     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen")
 418     public static Object getLastParen(Object self) {
 419         final RegExpResult match = Global.instance().getLastRegExpResult();
 420         return match == null ? "" : match.getLastParen();
 421     }
 422 
 423     /**
 424      * Getter for non-standard RegExp.leftContext property.
 425      * @param self self object
 426      * @return last regexp input
 427      */
 428     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext")
 429     public static Object getLeftContext(Object self) {
 430         final RegExpResult match = Global.instance().getLastRegExpResult();
 431         return match == null ? "" : match.getInput().substring(0, match.getIndex());
 432     }
 433 
 434     /**
 435      * Getter for non-standard RegExp.rightContext property.
 436      * @param self self object
 437      * @return last regexp input
 438      */
 439     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext")
 440     public static Object getRightContext(Object self) {
 441         final RegExpResult match = Global.instance().getLastRegExpResult();
 442         return match == null ? "" : match.getInput().substring(match.getIndex() + match.length());
 443     }
 444 
 445     /**
 446      * Getter for non-standard RegExp.$1 property.
 447      * @param self self object
 448      * @return last regexp input
 449      */
 450     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1")
 451     public static Object getGroup1(Object self) {
 452         final RegExpResult match = Global.instance().getLastRegExpResult();
 453         return match == null ? "" : match.getGroup(1);
 454     }
 455 
 456     /**
 457      * Getter for non-standard RegExp.$2 property.
 458      * @param self self object
 459      * @return last regexp input
 460      */
 461     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2")
 462     public static Object getGroup2(Object self) {
 463         final RegExpResult match = Global.instance().getLastRegExpResult();
 464         return match == null ? "" : match.getGroup(2);
 465     }
 466 
 467     /**
 468      * Getter for non-standard RegExp.$3 property.
 469      * @param self self object
 470      * @return last regexp input
 471      */
 472     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3")
 473     public static Object getGroup3(Object self) {
 474         final RegExpResult match = Global.instance().getLastRegExpResult();
 475         return match == null ? "" : match.getGroup(3);
 476     }
 477 
 478     /**
 479      * Getter for non-standard RegExp.$4 property.
 480      * @param self self object
 481      * @return last regexp input
 482      */
 483     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4")
 484     public static Object getGroup4(Object self) {
 485         final RegExpResult match = Global.instance().getLastRegExpResult();
 486         return match == null ? "" : match.getGroup(4);
 487     }
 488 
 489     /**
 490      * Getter for non-standard RegExp.$5 property.
 491      * @param self self object
 492      * @return last regexp input
 493      */
 494     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5")
 495     public static Object getGroup5(Object self) {
 496         final RegExpResult match = Global.instance().getLastRegExpResult();
 497         return match == null ? "" : match.getGroup(5);
 498     }
 499 
 500     /**
 501      * Getter for non-standard RegExp.$6 property.
 502      * @param self self object
 503      * @return last regexp input
 504      */
 505     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6")
 506     public static Object getGroup6(Object self) {
 507         final RegExpResult match = Global.instance().getLastRegExpResult();
 508         return match == null ? "" : match.getGroup(6);
 509     }
 510 
 511     /**
 512      * Getter for non-standard RegExp.$7 property.
 513      * @param self self object
 514      * @return last regexp input
 515      */
 516     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7")
 517     public static Object getGroup7(Object self) {
 518         final RegExpResult match = Global.instance().getLastRegExpResult();
 519         return match == null ? "" : match.getGroup(7);
 520     }
 521 
 522     /**
 523      * Getter for non-standard RegExp.$8 property.
 524      * @param self self object
 525      * @return last regexp input
 526      */
 527     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8")
 528     public static Object getGroup8(Object self) {
 529         final RegExpResult match = Global.instance().getLastRegExpResult();
 530         return match == null ? "" : match.getGroup(8);
 531     }
 532 
 533     /**
 534      * Getter for non-standard RegExp.$9 property.
 535      * @param self self object
 536      * @return last regexp input
 537      */
 538     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9")
 539     public static Object getGroup9(Object self) {
 540         final RegExpResult match = Global.instance().getLastRegExpResult();
 541         return match == null ? "" : match.getGroup(9);
 542     }
 543 
 544     private RegExpResult execInner(final String string) {
 545         final boolean isGlobal = regexp.isGlobal();
 546         int start = getLastIndex();
 547         if (!isGlobal) {
 548             start = 0;
 549         }
 550 
 551         if (start < 0 || start > string.length()) {
 552             if (isGlobal) {
 553                 setLastIndex(0);
 554             }
 555             return null;
 556         }
 557 
 558         final RegExpMatcher matcher = regexp.match(string);
 559         if (matcher == null || !matcher.search(start)) {
 560             if (isGlobal) {
 561                 setLastIndex(0);
 562             }
 563             return null;
 564         }
 565 
 566         if (isGlobal) {
 567             setLastIndex(matcher.end());
 568         }
 569 
 570         final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher));
 571         globalObject.setLastRegExpResult(match);
 572         return match;
 573     }
 574 
 575     // String.prototype.split method ignores the global flag and should not update lastIndex property.
 576     private RegExpResult execSplit(final String string, int start) {
 577         if (start < 0 || start > string.length()) {
 578             return null;
 579         }
 580 
 581         final RegExpMatcher matcher = regexp.match(string);
 582         if (matcher == null || !matcher.search(start)) {
 583             return null;
 584         }
 585 
 586         final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher));
 587         globalObject.setLastRegExpResult(match);
 588         return match;
 589     }
 590 
 591     /**
 592      * Convert java.util.regex.Matcher groups to JavaScript groups.
 593      * That is, replace null and groups that didn't match with undefined.
 594      */
 595     private Object[] groups(final RegExpMatcher matcher) {
 596         final int groupCount = matcher.groupCount();
 597         final Object[] groups = new Object[groupCount + 1];
 598         final BitVector groupsInNegativeLookahead  = regexp.getGroupsInNegativeLookahead();
 599 
 600         for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) {
 601             final int groupStart = matcher.start(i);
 602             if (lastGroupStart > groupStart
 603                     || (groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i))) {
 604                 // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated.
 605                 // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere
 606                 // in the pattern always return undefined because the negative lookahead must fail.
 607                 groups[i] = UNDEFINED;
 608                 continue;
 609             }
 610             final String group = matcher.group(i);
 611             groups[i] = group == null ? UNDEFINED : group;
 612             lastGroupStart = groupStart;
 613         }
 614         return groups;
 615     }
 616 
 617     /**
 618      * Executes a search for a match within a string based on a regular
 619      * expression. It returns an array of information or null if no match is
 620      * found.
 621      *
 622      * @param string String to match.
 623      * @return NativeArray of matches, string or null.
 624      */
 625     public Object exec(final String string) {
 626         final RegExpResult match = execInner(string);
 627 
 628         if (match == null) {
 629             return null;
 630         }
 631 
 632         return new NativeRegExpExecResult(match, globalObject);
 633     }
 634 
 635     /**
 636      * Executes a search for a match within a string based on a regular
 637      * expression.
 638      *
 639      * @param string String to match.
 640      * @return True if a match is found.
 641      */
 642     public Object test(final String string) {
 643         return execInner(string) != null;
 644     }
 645 
 646     /**
 647      * Searches and replaces the regular expression portion (match) with the
 648      * replaced text instead. For the "replacement text" parameter, you can use
 649      * the keywords $1 to $2 to replace the original text with values from
 650      * sub-patterns defined within the main pattern.
 651      *
 652      * @param string String to match.
 653      * @param replacement Replacement string.
 654      * @return String with substitutions.
 655      */
 656     Object replace(final String string, final String replacement, final ScriptFunction function) {
 657         final RegExpMatcher matcher = regexp.match(string);
 658 
 659         if (matcher == null) {
 660             return string;
 661         }
 662 
 663         if (!regexp.isGlobal()) {
 664             if (!matcher.search(0)) {
 665                 return string;
 666             }
 667 
 668             final StringBuilder sb = new StringBuilder();
 669             sb.append(string, 0, matcher.start());
 670 
 671             if (function != null) {
 672                 sb.append(callReplaceValue(function, matcher, string));
 673             } else {
 674                 appendReplacement(matcher, string, replacement, sb);
 675             }
 676             sb.append(string, matcher.end(), string.length());
 677             return sb.toString();
 678         }
 679 
 680         setLastIndex(0);
 681 
 682         if (!matcher.search(0)) {
 683             return string;
 684         }
 685 
 686         int thisIndex = 0;
 687         int previousLastIndex = 0;
 688         final StringBuilder sb = new StringBuilder();
 689 
 690         do {
 691             sb.append(string, thisIndex, matcher.start());
 692             if (function != null) {
 693                 sb.append(callReplaceValue(function, matcher, string));
 694             } else {
 695                 appendReplacement(matcher, string, replacement, sb);
 696             }
 697 
 698             thisIndex = matcher.end();
 699             if (thisIndex == string.length() && matcher.start() == matcher.end()) {
 700                 // Avoid getting empty match at end of string twice
 701                 break;
 702             }
 703 
 704             // ECMA 15.5.4.10 String.prototype.match(regexp)
 705             if (thisIndex == previousLastIndex) {
 706                 setLastIndex(thisIndex + 1);
 707                 previousLastIndex = thisIndex + 1;
 708             } else {
 709                 previousLastIndex = thisIndex;
 710             }
 711         } while (previousLastIndex <= string.length() && matcher.search(previousLastIndex));
 712 
 713         sb.append(string, thisIndex, string.length());
 714 
 715         return sb.toString();
 716     }
 717 
 718     private void appendReplacement(final RegExpMatcher matcher, final String text, final String replacement, final StringBuilder sb) {
 719         /*
 720          * Process substitution patterns:
 721          *
 722          * $$ -> $
 723          * $& -> the matched substring
 724          * $` -> the portion of string that preceeds matched substring
 725          * $' -> the portion of string that follows the matched substring
 726          * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit
 727          * $nn -> the nnth capture, where nn is a two digit decimal number [01-99].
 728          */
 729 
 730         int cursor = 0;
 731         Object[] groups = null;
 732 
 733         while (cursor < replacement.length()) {
 734             char nextChar = replacement.charAt(cursor);
 735             if (nextChar == '$') {
 736                 // Skip past $
 737                 cursor++;
 738                 nextChar = replacement.charAt(cursor);
 739                 final int firstDigit = nextChar - '0';
 740 
 741                 if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) {
 742                     // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit.
 743                     int refNum = firstDigit;
 744                     cursor++;
 745                     if (cursor < replacement.length() && firstDigit < matcher.groupCount()) {
 746                         final int secondDigit = replacement.charAt(cursor) - '0';
 747                         if ((secondDigit >= 0) && (secondDigit <= 9)) {
 748                             final int newRefNum = (firstDigit * 10) + secondDigit;
 749                             if (newRefNum <= matcher.groupCount() && newRefNum > 0) {
 750                                 // $nn ($01-$99)
 751                                 refNum = newRefNum;
 752                                 cursor++;
 753                             }
 754                         }
 755                     }
 756                     if (refNum > 0) {
 757                         if (groups == null) {
 758                             groups = groups(matcher);
 759                         }
 760                         // Append group if matched.
 761                         if (groups[refNum] != UNDEFINED) {
 762                             sb.append((String) groups[refNum]);
 763                         }
 764                     } else { // $0. ignore.
 765                         assert refNum == 0;
 766                         sb.append("$0");
 767                     }
 768                 } else if (nextChar == '$') {
 769                     sb.append('$');
 770                     cursor++;
 771                 } else if (nextChar == '&') {
 772                     sb.append(matcher.group());
 773                     cursor++;
 774                 } else if (nextChar == '`') {
 775                     sb.append(text, 0, matcher.start());
 776                     cursor++;
 777                 } else if (nextChar == '\'') {
 778                     sb.append(text, matcher.end(), text.length());
 779                     cursor++;
 780                 } else {
 781                     // unknown substitution or $n with n>m. skip.
 782                     sb.append('$');
 783                 }
 784             } else {
 785                 sb.append(nextChar);
 786                 cursor++;
 787             }
 788         }
 789     }
 790 
 791     private String callReplaceValue(final ScriptFunction function, final RegExpMatcher matcher, final String string) {
 792         final Object[] groups = groups(matcher);
 793         final Object[] args   = Arrays.copyOf(groups, groups.length + 2);
 794 
 795         args[groups.length]     = matcher.start();
 796         args[groups.length + 1] = string;
 797 
 798         final Object self = function.isStrict() ? UNDEFINED : Global.instance();
 799 
 800         return JSType.toString(ScriptRuntime.apply(function, self, args));
 801     }
 802 
 803     /**
 804      * Breaks up a string into an array of substrings based on a regular
 805      * expression or fixed string.
 806      *
 807      * @param string String to match.
 808      * @param limit  Split limit.
 809      * @return Array of substrings.
 810      */
 811     Object split(final String string, final long limit) {
 812         if (limit == 0L) {
 813             return new NativeArray();
 814         }
 815 
 816         final List<Object> matches = new ArrayList<>();
 817 
 818         RegExpResult match;
 819         final int inputLength = string.length();
 820         int splitLastLength = -1;
 821         int splitLastIndex = 0;
 822         int splitLastLastIndex = 0;
 823 
 824         while ((match = execSplit(string, splitLastIndex)) != null) {
 825             splitLastIndex = match.getIndex() + match.length();
 826 
 827             if (splitLastIndex > splitLastLastIndex) {
 828                 matches.add(string.substring(splitLastLastIndex, match.getIndex()));
 829                 final Object[] groups = match.getGroups();
 830                 if (groups.length > 1 && match.getIndex() < inputLength) {
 831                     for (int index = 1; index < groups.length && matches.size() < limit; index++) {
 832                         matches.add(groups[index]);
 833                     }
 834                 }
 835 
 836                 splitLastLength = match.length();
 837 
 838                 if (matches.size() >= limit) {
 839                     break;
 840                 }
 841             }
 842 
 843             // bump the index to avoid infinite loop
 844             if (splitLastIndex == splitLastLastIndex) {
 845                 splitLastIndex++;
 846             } else {
 847                 splitLastLastIndex = splitLastIndex;
 848             }
 849         }
 850 
 851         if (matches.size() < limit) {
 852             // check special case if we need to append an empty string at the
 853             // end of the match
 854             // if the lastIndex was the entire string
 855             if (splitLastLastIndex == string.length()) {
 856                 if (splitLastLength > 0 || execSplit("", 0) == null) {
 857                     matches.add("");
 858                 }
 859             } else {
 860                 matches.add(string.substring(splitLastLastIndex, inputLength));
 861             }
 862         }
 863 
 864         return new NativeArray(matches.toArray());
 865     }
 866 
 867     /**
 868      * Tests for a match in a string. It returns the index of the match, or -1
 869      * if not found.
 870      *
 871      * @param string String to match.
 872      * @return Index of match.
 873      */
 874     Object search(final String string) {
 875         final RegExpResult match = execInner(string);
 876 
 877         if (match == null) {
 878             return -1;
 879         }
 880 
 881         return match.getIndex();
 882     }
 883 
 884     /**
 885      * Fast lastIndex getter
 886      * @return last index property as int
 887      */
 888     public int getLastIndex() {
 889         return JSType.toInteger(lastIndex);
 890     }
 891 
 892     /**
 893      * Fast lastIndex getter
 894      * @return last index property as boxed integer
 895      */
 896     public Object getLastIndexObject() {
 897         return lastIndex;
 898     }
 899 
 900     /**
 901      * Fast lastIndex setter
 902      * @param lastIndex lastIndex
 903      */
 904     public void setLastIndex(final int lastIndex) {
 905         this.lastIndex = JSType.toObject(lastIndex);
 906     }
 907 
 908     private static NativeRegExp checkRegExp(final Object self) {
 909         Global.checkObjectCoercible(self);
 910         if (self instanceof NativeRegExp) {
 911             return (NativeRegExp)self;
 912         } else if (self != null && self == Global.instance().getRegExpPrototype()) {
 913             return Global.instance().DEFAULT_REGEXP;
 914         } else {
 915             throw typeError("not.a.regexp", ScriptRuntime.safeToString(self));
 916         }
 917     }
 918 
 919     boolean getGlobal() {
 920         return regexp.isGlobal();
 921     }
 922 
 923     private RegExp getRegExp() {
 924         return regexp;
 925     }
 926 
 927     private void setRegExp(final RegExp regexp) {
 928         this.regexp = regexp;
 929     }
 930 
 931 }