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