Commit graph

1 commit

Author SHA1 Message Date
Marko Lahma ddc95d734d
Stop attaching Array.prototype to dictionary-like objects in JavaScript expressions (#7890)
* fix(javascript): stop attaching Array.prototype to dictionary-like objects

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

* test(javascript): cast the ExpandoObject to its dictionary interface

`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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 03:52:37 +02:00