diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 891a1db8d0..32568175cd 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -5,6 +5,7 @@ on:
branches: [ master ]
pull_request:
branches: [ master ]
+ workflow_dispatch:
permissions: read-all
diff --git a/README.md b/README.md
index 4a6b28445a..e8305716ed 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ The current release is Rhino 1.9.1
February 15, 2026 |
| Rhino 1.9.0 | December 22, 2025 |
| Rhino 1.8.1 | December 2, 2025 |
| Rhino 1.8.0 | January 2, 2025 |
@@ -132,7 +133,7 @@ this using the command:
./gradlew -q javaToolchains
Not all installers seem to put JDKs in the places where Gradle can find them. When in doubt,
-installatioons from [Adoptium](https://adoptium.net) seem to work on most platforms.
+installations from [Adoptium](https://adoptium.net) seem to work on most platforms.
### Testing on Android
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index ef198d6b8b..94aacefae3 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,3 +1,15 @@
+# Rhino 1.9.1
+## February 15, 2026
+
+This release fixes a few small regressions introduced in Rhino 1.9.0.
+
+* Ensure that the "global" object is present, necessary for core-js to run
+* Prevent compiled methods from introducing illegal characters in their names
+* Support reserved words like "class" as names of XML attributes
+* Fix a performance regression in the RegExp engine.
+
+Thanks to all who contributed!
+
# Rhino 1.9.0
## December 22, 2025
diff --git a/gradle.properties b/gradle.properties
index cbbbbaac99..91c1688286 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
rootProject.name=rhino-root
group=org.mozilla
-version=1.9.0
+version=1.9.1-PATCHED
mavenReleaseRepo=https://oss.sonatype.org/service/local/staging/deploy/maven2/
mavenSnapshotRepo=https://oss.sonatype.org/content/repositories/snapshots
githubPackagesRepo=https://maven.pkg.github.com/mozilla/rhino
diff --git a/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java b/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java
index 143ba67e69..9af01cc080 100644
--- a/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java
+++ b/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java
@@ -17,7 +17,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
@@ -25,6 +24,7 @@
import org.mozilla.javascript.Kit;
import org.mozilla.javascript.NativeCall;
import org.mozilla.javascript.NativeObject;
+import org.mozilla.javascript.Script;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
@@ -745,8 +745,15 @@ private static String do_eval(Context cx, StackFrame frame, String expr) {
cx.setInterpretedMode(false);
cx.setGeneratingDebug(false);
try {
- Callable script = (Callable) cx.compileString(expr, "", 0, null);
- Object result = script.call(cx, frame.scope, frame.thisObj, ScriptRuntime.emptyArgs);
+ Scriptable scope = frame.scope;
+ if (!frame.isFunction && scope != null) {
+ Scriptable parentScope = scope.getParentScope();
+ if (parentScope != null) {
+ scope = parentScope;
+ }
+ }
+ Script script = cx.compileString(expr, "", 0, null);
+ Object result = script.exec(cx, scope, frame.thisObj);
if (result == Undefined.instance) {
resultString = "";
} else {
@@ -896,7 +903,7 @@ public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) {
// Can not debug if source is not available
return null;
}
- return new StackFrame(cx, dim, item);
+ return new StackFrame(cx, dim, item, fnOrScript.isFunction());
}
/** Called when compilation is finished. */
@@ -974,6 +981,9 @@ public static class StackFrame implements DebugFrame {
/** The 'this' object. */
private Scriptable thisObj;
+ /** Whether this frame represents a function (vs a top-level script). */
+ private boolean isFunction;
+
/** Information about the function. */
private FunctionSource fsource;
@@ -984,10 +994,11 @@ public static class StackFrame implements DebugFrame {
private int lineNumber;
/** Creates a new StackFrame. */
- private StackFrame(Context cx, Dim dim, FunctionSource fsource) {
+ private StackFrame(Context cx, Dim dim, FunctionSource fsource, boolean isFunction) {
this.dim = dim;
this.contextData = ContextData.get(cx);
this.fsource = fsource;
+ this.isFunction = isFunction;
this.breakpoints = fsource.sourceInfo().breakpoints;
this.lineNumber = fsource.firstLine();
}
diff --git a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java
index 98f23c749c..cc7751d404 100644
--- a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java
+++ b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java
@@ -1,57 +1,46 @@
package org.mozilla.javascript;
+import java.util.ArrayList;
+import java.util.List;
+
/** Abstract operations for string manipulation as defined by EcmaScript */
public class AbstractEcmaStringOperations {
- /**
- * GetSubstitution(matched, str, position, captures, namedCaptures, replacementTemplate)
- *
- * 22.1.3.19.1
- * GetSubstitution (matched, str, position, captures, namedCaptures, replacementTemplate)
- */
- public static String getSubstitution(
- Context cx,
- Scriptable scope,
- String matched,
- String str,
- int position,
- NativeArray capturesArray,
- Object namedCaptures,
- String replacementTemplate) {
- // See ECMAScript spec 22.1.3.19.1
- int stringLength = str.length();
- if (position > stringLength) Kit.codeBug();
- StringBuilder result = new StringBuilder();
- String templateRemainder = replacementTemplate;
- while (!templateRemainder.isEmpty()) {
- String ref = templateRemainder.substring(0, 1);
- String refReplacement = ref;
-
- if (templateRemainder.charAt(0) == '$') {
- if (templateRemainder.length() > 1) {
- char c = templateRemainder.charAt(1);
+
+ public static List buildReplacementList(String replacementTemplate) {
+ List ops = new ArrayList<>();
+ int position = 0;
+ int start = 0;
+
+ while (position < replacementTemplate.length()) {
+ if (replacementTemplate.charAt(position) == '$') {
+ if (position < (replacementTemplate.length() - 1)) {
+ if (start != position) {
+ ops.add(
+ new LiteralReplacement(
+ replacementTemplate.substring(start, position)));
+ }
+ String ref = replacementTemplate.substring(position, position + 1);
+ char c = replacementTemplate.charAt(position + 1);
switch (c) {
case '$':
ref = "$$";
- refReplacement = "$";
+ ops.add(new LiteralReplacement("$"));
break;
case '`':
ref = "$`";
- refReplacement = str.substring(0, position);
+ ops.add(new FromStartToMatchReplacement());
break;
case '&':
ref = "$&";
- refReplacement = matched;
+ ops.add(new MatchedReplacement());
break;
case '\'':
{
ref = "$'";
- int matchLength = matched.length();
- int tailPos = position + matchLength;
- refReplacement = str.substring(Math.min(tailPos, stringLength));
+ ops.add(new FromMatchToEndReplacement());
break;
}
@@ -67,66 +56,78 @@ public static String getSubstitution(
case '9':
{
int digitCount = 1;
- if (templateRemainder.length() > 2) {
- char c2 = templateRemainder.charAt(2);
+ if (replacementTemplate.length() > position + 2) {
+ char c2 = replacementTemplate.charAt(position + 2);
if (isAsciiDigit(c2)) {
digitCount = 2;
}
}
- String digits = templateRemainder.substring(1, 1 + digitCount);
+ String digits =
+ replacementTemplate.substring(
+ position + 1, position + 1 + digitCount);
+ ref =
+ replacementTemplate.substring(
+ position, position + 1 + digitCount);
// No need for ScriptRuntime version; we know the string is one or
// two characters and
// contains only [0-9]
int index = Integer.parseInt(digits);
- long captureLen = capturesArray.getLength();
- if (index > captureLen && digitCount == 2) {
- digitCount = 1;
- digits = digits.substring(0, 1);
- index = Integer.parseInt(digits);
- }
- ref = templateRemainder.substring(0, 1 + digitCount);
- if (1 <= index && index <= captureLen) {
- Object capture = capturesArray.get(index - 1);
- if (capture
- == null) { // Undefined or missing are returned as null
- refReplacement = "";
- } else {
- refReplacement = ScriptRuntime.toString(capture);
- }
+ if (digits.length() == 1) {
+ ops.add(new OneDigitCaptureReplacement(index));
} else {
- refReplacement = ref;
+ ops.add(new TwoDigitCaptureReplacement(index));
}
break;
}
case '<':
{
- int gtPos = templateRemainder.indexOf('>');
- if (gtPos == -1 || Undefined.isUndefined(namedCaptures)) {
+ int gtPos = replacementTemplate.indexOf('>', position + 2);
+ if (gtPos == -1) {
ref = "$<";
- refReplacement = ref;
+ ops.add(new LiteralReplacement(ref));
} else {
- ref = templateRemainder.substring(0, gtPos + 1);
- String groupName = templateRemainder.substring(2, gtPos);
- Object capture =
- ScriptRuntime.getObjectProp(
- namedCaptures, groupName, cx, scope);
- if (Undefined.isUndefined(capture)) {
- refReplacement = "";
- } else {
- refReplacement = ScriptRuntime.toString(capture);
- }
+ ref = replacementTemplate.substring(position, gtPos + 1);
+ String groupName =
+ replacementTemplate.substring(position + 2, gtPos);
+ ops.add(new NamedCaptureReplacement(groupName));
}
}
break;
+ default:
+ ops.add(new LiteralReplacement(ref));
+ break;
}
+ position += ref.length();
+ start = position;
+ } else {
+ position++;
}
+ } else {
+ position++;
}
+ }
+ if (start != position) {
+ ops.add(new LiteralReplacement(replacementTemplate.substring(start, position)));
+ }
+ return ops;
+ }
- int refLength = ref.length();
- templateRemainder = templateRemainder.substring(refLength);
- result.append(refReplacement);
+ public static String getSubstitution(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List capturesList,
+ Object namedCaptures,
+ List replacementTemplate) {
+ if (position > str.length()) Kit.codeBug();
+ StringBuilder result = new StringBuilder();
+ for (var op : replacementTemplate) {
+ result.append(
+ op.replacement(cx, scope, matched, str, position, capturesList, namedCaptures));
}
return result.toString();
}
@@ -149,4 +150,181 @@ private static boolean isAsciiDigit(char c) {
return false;
}
}
+
+ public abstract static class ReplacementOperation {
+
+ abstract String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures);
+ }
+
+ private static class LiteralReplacement extends ReplacementOperation {
+
+ private final String replacement;
+
+ LiteralReplacement(String replacement) {
+ this.replacement = replacement;
+ }
+
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ return replacement;
+ }
+ }
+
+ private static class OneDigitCaptureReplacement extends ReplacementOperation {
+ private final int capture;
+
+ OneDigitCaptureReplacement(int capture) {
+ this.capture = capture;
+ }
+
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ if (capture >= 1 && capture <= captures.size()) {
+ var v = captures.get(capture - 1);
+ if (v == null || v == Undefined.instance) {
+ return "";
+ } else {
+ return v.toString();
+ }
+ } else {
+ return "$" + Integer.toString(capture);
+ }
+ }
+ }
+
+ private static class TwoDigitCaptureReplacement extends ReplacementOperation {
+ private final int capture;
+
+ TwoDigitCaptureReplacement(int capture) {
+ this.capture = capture;
+ }
+
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ int i = capture;
+ if (i > 9 && i > captures.size() && i / 10 <= captures.size()) {
+ i = i / 10; // Just take the first digit.
+ var v = captures.get(i - 1);
+ if (v == null || v == Undefined.instance) {
+ return "" + Integer.toString(capture % 10);
+ } else {
+ return v.toString() + Integer.toString(capture % 10);
+ }
+ } else if (i >= 1 && i <= captures.size()) {
+ var v = captures.get(i - 1);
+ if (v == null || v == Undefined.instance) {
+ return "";
+ } else {
+ return v.toString();
+ }
+ } else {
+ return (capture >= 10 ? "$" : "$0") + Integer.toString(capture);
+ }
+ }
+ }
+
+ private static class FromStartToMatchReplacement extends ReplacementOperation {
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ return str.substring(0, position);
+ }
+ }
+
+ private static class MatchedReplacement extends ReplacementOperation {
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ return matched;
+ }
+ }
+
+ private static class FromMatchToEndReplacement extends ReplacementOperation {
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ int matchLength = matched.length();
+ int tailPos = position + matchLength;
+ return str.substring(Math.min(str.length(), tailPos));
+ }
+ }
+
+ private static class NamedCaptureReplacement extends ReplacementOperation {
+ final String groupName;
+
+ NamedCaptureReplacement(String groupName) {
+ this.groupName = groupName;
+ }
+
+ @Override
+ String replacement(
+ Context cx,
+ Scriptable scope,
+ String matched,
+ String str,
+ int position,
+ List captures,
+ Object namedCaptures) {
+ if (Undefined.isUndefined(namedCaptures)) {
+ List ops = buildReplacementList(groupName);
+ return "$<"
+ + getSubstitution(
+ cx, scope, matched, str, position, captures, namedCaptures, ops)
+ + ">";
+ }
+
+ Object capture = ScriptRuntime.getObjectProp(namedCaptures, groupName, cx, scope);
+ if (Undefined.isUndefined(capture)) {
+ return "";
+ } else {
+ return ScriptRuntime.toString(capture);
+ }
+ }
+ }
}
diff --git a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java
index a9d91b85ce..dc91c3ccc0 100644
--- a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java
+++ b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java
@@ -1552,7 +1552,7 @@ private void visitArrayLiteral(Node node, Node child) {
}
addIndexOp(Icode_LITERAL_NEW_ARRAY, count - numberOfSpread);
- addUint8(skipIndexesId + 1);
+ addUint16(skipIndexesId + 1);
stackChange(1);
int childIdx = 0;
@@ -1561,7 +1561,7 @@ private void visitArrayLiteral(Node node, Node child) {
visitExpression(child.getFirstChild(), 0);
addIcode(Icode_SPREAD);
if (skipIndexes != null) {
- addUint8(sourcePositions[childIdx]);
+ addUint16(sourcePositions[childIdx]);
}
stackChange(-1);
} else {
diff --git a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
index 14a829b861..c3eb585747 100644
--- a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
+++ b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java
@@ -1191,6 +1191,10 @@ private static int bytecodeSpan(int bytecode) {
// make a copy or not flag
return 1 + 1;
+ case Icode_LITERAL_NEW_ARRAY:
+ // skip indexes ID (uint16)
+ return 1 + 2;
+
case Icode_REG_BIGINT1:
// ubyte bigint index
return 1 + 1;
@@ -4400,8 +4404,8 @@ NewState execute(Context cx, CallFrame frame, InterpreterState state, int op) {
// indexReg: number of values in the literal
NewLiteralStorage storage = NewLiteralStorage.create(cx, state.indexReg, false);
- int skipIdx = 0xFF & frame.idata.itsICode[frame.pc];
- ++frame.pc;
+ int skipIdx = getIndex(frame.idata.itsICode, frame.pc);
+ frame.pc += 2;
// fill in skip indexes in array literal storage
if (skipIdx > 0) { // 0 - no skip index, otherwise subtract 1 from idx
@@ -4468,8 +4472,8 @@ NewState execute(Context cx, CallFrame frame, InterpreterState state, int op) {
NewLiteralStorage store = (NewLiteralStorage) frame.stack[state.stackTop];
if (store.hasSkipIndexes()) {
- int sourcePos = 0xFF & frame.idata.itsICode[frame.pc];
- ++frame.pc;
+ int sourcePos = getIndex(frame.idata.itsICode, frame.pc);
+ frame.pc += 2;
store.spread(cx, frame.scope, source, sourcePos);
} else {
store.spread(cx, frame.scope, source, 0);
diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeString.java b/rhino/src/main/java/org/mozilla/javascript/NativeString.java
index 90a8b9a9d7..97271653f0 100644
--- a/rhino/src/main/java/org/mozilla/javascript/NativeString.java
+++ b/rhino/src/main/java/org/mozilla/javascript/NativeString.java
@@ -16,6 +16,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import org.mozilla.javascript.AbstractEcmaStringOperations.ReplacementOperation;
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
/**
@@ -910,8 +911,14 @@ private static Object js_replace(
String string = ScriptRuntime.toString(o);
String searchString = ScriptRuntime.toString(searchValue);
boolean functionalReplace = replaceValue instanceof Callable;
+ List replaceOps;
+
if (!functionalReplace) {
- replaceValue = ScriptRuntime.toString(replaceValue);
+ replaceOps =
+ AbstractEcmaStringOperations.buildReplacementList(
+ ScriptRuntime.toString(replaceValue));
+ } else {
+ replaceOps = List.of();
}
int searchLength = searchString.length();
int position = string.indexOf(searchString);
@@ -937,7 +944,7 @@ private static Object js_replace(
});
replacement = ScriptRuntime.toString(replacementObj);
} else {
- NativeArray captures = (NativeArray) cx.newArray(scope, 0);
+ List