Skip to content

Stop attaching Array.prototype to dictionary-like objects in JavaScript expressions - #7890

Merged
sfmskywalker merged 2 commits into
elsa-workflows:mainfrom
lahma:fix/js-object-wrapper-array-prototype
Sep 14, 2026
Merged

sfmskywalker merged 2 commits into
elsa-workflows:mainfrom
lahma:fix/js-object-wrapper-array-prototype

Conversation

@lahma

@lahma lahma commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

JintJavaScriptEvaluator installs a custom WrapObjectDelegate:

options.SetWrapObjectHandler((engine, target, type) =>
{
    var instance = ObjectWrapper.Create(engine, target);

    if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
        instance.Prototype = engine.Intrinsics.Array.PrototypeObject;

    return instance;
});

This duplicates what Jint already does, and gets it wrong in two ways.

Jint's default handler is ObjectWrapper.Create(engine, target, type), and ObjectWrapper attaches Array.prototype to array-like wrappers by itself whenever Options.Interop.AttachArrayPrototype is enabled — which it is by default. Jint's own array-likeness test deliberately treats dictionary-like types, including string-keyed generic dictionaries, as plain objects.

The handler above:

  • drops the declared type argument, so wrapper members are resolved against the runtime type rather than the declared one;
  • delegates the array-likeness decision to ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection, which only excludes the non-generic IDictionary. ExpandoObject does not implement that interface — it implements IDictionary<string, object> and ICollection<KeyValuePair<string, object>> — so it comes out array-like.

Both the variables container (ConfigureEngineWithVariables) and the args container (JavaScriptExpressionHandler) are ExpandoObject instances. The observable result today:

Object.getPrototypeOf(variables) === Array.prototype   // true
typeof variables.map                                   // "function"
typeof variables.filter                                // "function"
variables.length                                       // 0, rather than undefined

Fix

Remove the handler. Jint's default covers every case the custom one was written for — verified against a plain engine:

wrapped value gets Array.prototype
string[], List<T>, HashSet<T>, ImmutableArray<T>, ImmutableList<T>, Queue<T>, Stack<T> yes
Dictionary<string, object>, ExpandoObject, a bare IEnumerable<T> iterator no

ObjectArrayHelper is public, so it is marked [Obsolete] rather than deleted.

Tests

New ObjectWrappingTests covers both directions: the variables container and dictionary-like objects are plain objects, and array-like CLR collections still expose Array.prototype and still support map/reduce where the collection is indexable.

Elsa.JavaScript.IntegrationTests 73 → 80 passing, Elsa.Workflows.IntegrationTests 270 passing, both before and after.

Note for reviewers: HashSet<T> gets Array.prototype but has no integer indexer, so set.map(...) yields empty entries. That is unchanged by this PR — it behaves identically with and without the custom handler — so the test asserts prototype presence for sets and functional iteration only for indexable collections.

🤖 Generated with Claude Code

https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik

Copilot AI review requested due to automatic review settings July 26, 2026 18:09
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

PR author is not in the allowed authors list.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR corrects how CLR objects are wrapped/exposed inside Elsa’s Jint-based JavaScript evaluator by removing a custom WrapObjectDelegate that incorrectly attached Array.prototype to dictionary-like objects (e.g., ExpandoObject-backed variables/args), and by adding integration coverage to prevent regressions.

Changes:

  • Removed the custom Jint object-wrap handler so Jint’s built-in array-likeness logic governs whether Array.prototype is attached.
  • Marked ObjectArrayHelper as [Obsolete] since the evaluator no longer uses it.
  • Added integration tests asserting dictionary-like objects remain plain objects while array-like CLR collections still get array prototype behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/integration/Elsa.JavaScript.IntegrationTests/ObjectWrappingTests.cs Adds integration tests validating prototype behavior for variables, dictionary-like objects, and array-like CLR collections.
src/modules/Elsa.Expressions.JavaScript/Services/JintJavaScriptEvaluator.cs Removes the custom wrap-object handler to rely on Jint’s default object wrapping behavior.
src/modules/Elsa.Expressions.JavaScript/Helpers/ObjectArrayHelper.cs Marks the helper obsolete now that array-likeness decisions are delegated to Jint.

Comment thread test/integration/Elsa.JavaScript.IntegrationTests/ObjectWrappingTests.cs Outdated
Copilot AI review requested due to automatic review settings July 26, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@lahma
lahma force-pushed the fix/js-object-wrapper-array-prototype branch from df85bc0 to 4959eab Compare August 8, 2026 11:53
@lahma

lahma commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (bd903f6). No changes beyond the rebase; Elsa.JavaScript.IntegrationTests green (80 passing).

…jects

The custom `WrapObjectDelegate` installed by `JintJavaScriptEvaluator` duplicated
what Jint already does, and got it wrong in two ways.

Jint's default wrap handler is `ObjectWrapper.Create(engine, target, type)`, and
`ObjectWrapper` attaches `Array.prototype` to array-like wrappers by itself when
`Options.Interop.AttachArrayPrototype` is enabled (the default). Jint's own
array-likeness test deliberately excludes dictionary-like types, including
string-keyed generic dictionaries.

The handler we installed instead:

* Called `ObjectWrapper.Create(engine, target)`, dropping the declared `type`
  argument, so members were resolved against the runtime type rather than the
  declared one.
* Used `ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection`, which only
  excludes the non-generic `IDictionary`. `ExpandoObject` does not implement
  that interface, so it came out array-like.

Both the `variables` container and the `args` container are `ExpandoObject`
instances, which meant `Object.getPrototypeOf(variables) === Array.prototype`
was true and `variables.map`, `variables.filter`, `variables.reduce` and friends
were all visible on them, with `variables.length` reporting `0` instead of
`undefined`.

Removing the handler restores Jint's default, which handles every case the
custom one was written for: `List<T>`, `T[]`, `HashSet<T>`, `ImmutableArray<T>`,
`Queue<T>` and `Stack<T>` all still get `Array.prototype`, while dictionaries and
`ExpandoObject` no longer do.

`ObjectArrayHelper` is public, so it is marked obsolete rather than deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik
@lahma
lahma force-pushed the fix/js-object-wrapper-array-prototype branch from 4959eab to 26bbb02 Compare August 18, 2026 16:54
`new ExpandoObject() as IDictionary<string, object>` reads as a conversion that
might fail and gives the variable a nullable declared type, when `ExpandoObject`
implements the interface unconditionally. A direct cast states that, and matches
the BCL's `IDictionary<string, object?>` annotation exactly so the value type
argument lines up too.

The two other `as IDictionary<string, object>` uses in this test project
(JintJavaScriptFunctionBehaviorTests) are deliberately left alone: there the
operand is the untyped result of a script evaluation, so the `as` is a genuine
type test paired with `Assert.NotNull`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik
@lahma
lahma force-pushed the fix/js-object-wrapper-array-prototype branch from 26bbb02 to d5fa4d4 Compare August 18, 2026 17:09

@sfmskywalker sfmskywalker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lahma!

@sfmskywalker
sfmskywalker merged commit ddc95d7 into elsa-workflows:main Sep 14, 2026
3 checks passed
@lahma
lahma deleted the fix/js-object-wrapper-array-prototype branch September 14, 2026 04:35
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.

3 participants