Skip to content

Fix shared regex state across repeated matcher use (#749) - #834

Open
chiliec wants to merge 1 commit into
jsonata-js:masterfrom
chiliec:fix/issue-749-shared-regex-state
Open

Fix shared regex state across repeated matcher use (#749)#834
chiliec wants to merge 1 commit into
jsonata-js:masterfrom
chiliec:fix/issue-749-shared-regex-state

Conversation

@chiliec

@chiliec chiliec commented Aug 27, 2026

Copy link
Copy Markdown

Fixes #749.

Problem

A regex bound to a variable and reused across multiple $match/$replace/$split calls produces different results on repeated use, breaking JSONata's immutability guarantee. It worked in 1.8.7 / 2.0.1 and regressed by 2.0.6.

(
  $input := "a,b,c";
  $pattern := /[,-]/;
  {
    "2x": [$replace($input, $pattern, "|"), $replace($input, $pattern, "|")],
    "3x": [$replace($input, $pattern, "|"), $replace($input, $pattern, "|"), $replace($input, $pattern, "|")]
  }
)

Expected every element to be "a|b|c". On master it returns corrupted values such as "a|b|,b,|,|b,c".

Root cause

evaluateRegex builds one RegExp and closes over it:

var re = new environment.base.RegexEngine(expr.value);
var closure = function(str, fromIndex) {
    re.lastIndex = fromIndex || 0;
    ...

A regex literal bound to a variable is evaluated once, so the returned closure — and that single stateful re with its mutable lastIndex — is shared by every subsequent matcher call. $replace walks matches via the next() chain, which depends on re.lastIndex persisting; when another call reuses the same closure it resets/advances the same lastIndex, so independent calls interfere and corrupt each other's results.

Fix

Create a fresh RegExp for each top-level match walk (when the closure is entered without an existing instance), and thread that walk's own instance through the next() chain so multi-match iteration within a single walk is unchanged. Independent calls no longer share mutable regex state.

Test

Adds test/test-suite/groups/regex/case039.json, the repeated-use scenario from the issue (asserts every $replace yields "a|b|c").

Verification (local, Node 20.18.1)

  • RED→GREEN: with the fix reverted, case039 fails (1799 passing / 1 failing); with the fix it passes (1800 passing / 0 failing).
  • npm test — full suite green, and the strict gate holds: Statements/Branches/Functions/Lines all 100% (nyc check-coverage --branches 100 exits 0). The new branch is covered by case039.
  • ESLint clean.

Signed-off-by: Vladimir Babin vovababin@gmail.com

evaluateRegex created a single RegExp and closed over it, so a regex bound to
a variable (evaluated once) reused the same stateful RegExp — and its mutable
lastIndex — across every $match/$replace/$split call. Independent calls then
interfered through the shared lastIndex, so the same input produced different
output on repeated use, breaking JSONata's immutability guarantee (a regression
between 2.0.1 and 2.0.6).

Create a fresh RegExp for each top-level match walk, and thread that walk's own
instance through the next() chain so multi-match iteration is unchanged. Adds a
regression test (regex/case039) covering the repeated-use scenario from the
issue. Full suite passes with 100% coverage maintained.

Fixes jsonata-js#749.

Signed-off-by: Vladimir Babin <vovababin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent Behavior When Using Regex Variables in JSONata 2.0.6

1 participant