From ae4485ae0bb3f646b462325f6af044e1d094b65d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:51:18 +0300 Subject: [PATCH] Fix indexed access into DOM collections Indexing a DOM collection from script - `document.getElementsByTagName('p')[0]`, `element.classList[0]`, and everything jQuery builds on top of them - throws a `TargetInvocationException` wrapping `EntryPointNotFoundException`. `IHtmlCollection`, `ITokenList` and `IStringList` declare their numeric indexer as an explicit re-implementation of `IReadOnlyList`'s indexer. A member declared that way on an interface is private and abstract, so the `MethodInfo` obtained from the interface's `PropertyInfo` cannot be invoked: the implementation lives in a different slot and the runtime has no entry point for the one we hand it. Resolve the indexer accessor once, when the prototype is built, against the type the prototype belongs to, and invoke that. Public accessors - the common case, including every string indexer and the numeric indexers of `INodeList`, `INamedNodeMap`, `IStyleSheetList` and friends - are used unchanged. This is what makes the six currently failing tests in `ScriptingTests` and `JqueryTests` fail; they pass again with this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/DomTests.cs | 28 +++++++++ .../Proxies/DomPrototypeInstance.cs | 63 +++++++++++++++---- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index 5d1b975..ffabd0b 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -26,5 +26,33 @@ public async Task NodeHasChildNodesWithChildren() var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.hasChildNodes()".EvalScriptAsync(); Assert.AreEqual("True", result); } + + [Test] + public async Task NumericIndexerOfHtmlCollectionYieldsTheElement() + { + var result = await "document.getElementsByTagName('script')[0].nodeName".EvalScriptAsync(); + Assert.AreEqual("SCRIPT", result); + } + + [Test] + public async Task NumericIndexerOfHtmlCollectionOutOfRangeIsUndefined() + { + var result = await "typeof document.getElementsByTagName('script')[5]".EvalScriptAsync(); + Assert.AreEqual("undefined", result); + } + + [Test] + public async Task NumericIndexerOfNodeListYieldsTheNode() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.childNodes[0].nodeName".EvalScriptAsync(); + Assert.AreEqual("DIV", result); + } + + [Test] + public async Task NumericIndexerOfTokenListYieldsTheToken() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.classList[1]".EvalScriptAsync(); + Assert.AreEqual("b", result); + } } } diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index df226b9..540688a 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -15,9 +15,10 @@ sealed class DomPrototypeInstance : ObjectInstance { private readonly String _name; private readonly EngineInstance _instance; + private readonly Type _type; - private PropertyInfo _numericIndexer; - private PropertyInfo _stringIndexer; + private MethodInfo _numericIndexer; + private MethodInfo _stringIndexer; public DomPrototypeInstance(EngineInstance engine, Type type) : base(engine.Jint) @@ -25,6 +26,7 @@ public DomPrototypeInstance(EngineInstance engine, Type type) var baseType = type.GetTypeInfo().BaseType ?? typeof(Object); _name = type.GetOfficialName(baseType); _instance = engine; + _type = type; Set(GlobalSymbolRegistry.ToStringTag, _name); @@ -45,7 +47,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto try { var args = new Object[] { numericIndex }; - var orig = _numericIndexer.GetMethod.Invoke(value, args); + var orig = _numericIndexer.Invoke(value, args); result = new PropertyDescriptor(orig.ToJsValue(_instance), false, false, false); return true; } @@ -70,7 +72,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto if (_stringIndexer != null && !HasProperty(index)) { var args = new Object[] { index }; - var valueAtIndex = _stringIndexer.GetMethod.Invoke(value, args); + var valueAtIndex = _stringIndexer.Invoke(value, args); if (valueAtIndex == null) { @@ -229,17 +231,52 @@ private void SetProperty(String name, MethodInfo getter, MethodInfo setter, DomP private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) { - if (indexParameters.Length == 1) + if (indexParameters.Length != 1) { - if (indexParameters[0].ParameterType == typeof(Int32)) - { - _numericIndexer = property; - } - else if (indexParameters[0].ParameterType == typeof(String)) - { - _stringIndexer = property; - } + return; + } + + var getter = ResolveAccessor(property.GetMethod); + + if (getter == null) + { + return; + } + + if (indexParameters[0].ParameterType == typeof(Int32)) + { + _numericIndexer = getter; } + else if (indexParameters[0].ParameterType == typeof(String)) + { + _stringIndexer = getter; + } + } + + private MethodInfo ResolveAccessor(MethodInfo accessor) + { + // An interface may re-implement a member of one of its own base interfaces + // explicitly, e.g. "T IReadOnlyList.this[Int32 index]" declared on an + // IHtmlCollection. Such a member is private and abstract - invoking it + // reflectively throws an EntryPointNotFoundException because the actual + // implementation lives in a different slot. Resolve it against the type the + // prototype was created for, which is where the implementation can be found. + if (accessor == null || accessor.IsPublic) + { + return accessor; + } + + var name = accessor.Name; + var simpleName = name.Substring(name.LastIndexOf('.') + 1); + var parameters = accessor.GetParameters(); + var parameterTypes = new Type[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return _type.GetRuntimeMethod(simpleName, parameterTypes) ?? accessor; } private void SetMethod(String name, MethodInfo method)