diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index bfd1e4c..489ee32 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -55,6 +55,24 @@ public async Task NumericIndexerOfHtmlCollectionOutOfRangeIsUndefined() Assert.AreEqual("undefined", result); } + // Two closed forms of IHtmlCollection in one document - each resolves its own + // indexer, and the explicit IReadOnlyList re-implementation behind them is the + // one accessor that cannot be invoked as declared. + [Test] + public async Task NumericIndexerWorksForHtmlCollectionsOfDifferentItemTypes() + { + var result = await "(function () { var d = new DOMParser().parseFromString(``, 'text/html'); return d.getElementsByTagName('img')[0].id + ',' + d.images[0].id; })()".EvalScriptAsync(); + Assert.AreEqual("x,x", result); + } + + // col and colgroup are separate classes sharing the HTMLTableColElement prototype. + [Test] + public async Task NumericIndexerWorksForElementsSharingAPrototype() + { + var result = await "(function () { var d = new DOMParser().parseFromString(`
`, 'text/html'); var g = d.getElementsByTagName('colgroup')[0], c = d.getElementsByTagName('col')[0]; return g.classList[1] + ',' + c.classList[1]; })()".EvalScriptAsync(); + Assert.AreEqual("b,d", result); + } + [Test] public async Task NumericIndexerOfNodeListYieldsTheNode() { diff --git a/src/AngleSharp.Js.Tests/InstanceOfTests.cs b/src/AngleSharp.Js.Tests/InstanceOfTests.cs new file mode 100644 index 0000000..633db5b --- /dev/null +++ b/src/AngleSharp.Js.Tests/InstanceOfTests.cs @@ -0,0 +1,196 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System.Threading.Tasks; + + [TestFixture] + public class InstanceOfTests + { + // The two cases reported in https://github.com/AngleSharp/AngleSharp.Js/issues/103 + + [Test] + public async Task WindowIsAnInstanceOfWindow() + { + var result = await "window instanceof Window".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfElement() + { + var result = await "document.createElement('div') instanceof Element".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Every level of the chain on its own - the leaf passing says nothing about the rest. + + [Test] + public async Task CreatedElementIsAnInstanceOfItsOwnType() + { + var result = await "document.createElement('div') instanceof HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('div') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfNode() + { + var result = await "document.createElement('div') instanceof Node".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfEventTarget() + { + var result = await "document.createElement('div') instanceof EventTarget".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsNotAnInstanceOfAnUnrelatedType() + { + var result = await "document.createElement('div') instanceof HTMLAnchorElement".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task DocumentIsAnInstanceOfHtmlDocument() + { + var result = await "document instanceof HTMLDocument".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task DocumentIsAnInstanceOfDocument() + { + var result = await "document instanceof Document".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // An anchor reaches HTMLElement through the URLUtils mixin, which sits in the chain + // because AngleSharp implements it as an abstract class of its own. + + [Test] + public async Task AnchorIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('a') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // An element class carrying no DOM name of its own - a "b" is an HTMLElement in a + // browser, and now here as well. + + [Test] + public async Task UnnamedElementIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('b') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task UnnamedElementUsesThePrototypeOfHtmlElement() + { + var result = await "Object.getPrototypeOf(document.createElement('b')) === HTMLElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Asserted without "instanceof" on purpose: a Symbol.hasInstance answer would hide a + // broken chain from every test above, but not from these. + + [Test] + public async Task PrototypeOfElementIsThePrototypeOfItsConstructor() + { + var result = await "Object.getPrototypeOf(document.createElement('div')) === HTMLDivElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task PrototypeOfConstructorIsChainedToItsBase() + { + var result = await "Object.getPrototypeOf(HTMLDivElement.prototype) === HTMLElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task PrototypeOfWindowIsThePrototypeOfItsConstructor() + { + var result = await "Object.getPrototypeOf(window) === Window.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ConstructorOfAnElementIsTheExposedConstructor() + { + var result = await "document.createElement('div').constructor === HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Types reached only through a property, never constructed from script. + + [Test] + public async Task TokenListIsAnInstanceOfDomTokenList() + { + var result = await "document.createElement('div').classList instanceof DOMTokenList".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CollectionIsAnInstanceOfHtmlCollection() + { + var result = await "document.getElementsByTagName('div') instanceof HTMLCollection".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // A type constructed from script keeps working - these already did. + + [Test] + public async Task ConstructedEventIsAnInstanceOfEvent() + { + var result = await "new Event('foo') instanceof Event".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ConstructedCustomEventIsAnInstanceOfEvent() + { + var result = await "new CustomEvent('foo') instanceof Event".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ImageIsAnInstanceOfHtmlImageElement() + { + var result = await "new Image() instanceof HTMLImageElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // A DOM constructor is a function, so it inherits from Function.prototype. + + [Test] + public async Task DomConstructorIsAFunction() + { + var result = await "typeof HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("function", result); + } + + [Test] + public async Task DomConstructorIsAnInstanceOfFunction() + { + var result = await "Window instanceof Function".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task DomConstructorInheritsCallFromFunctionPrototype() + { + var result = await "typeof HTMLDivElement.call".EvalScriptAsync(); + Assert.AreEqual("function", result); + } + } +} diff --git a/src/AngleSharp.Js/Cache/PrototypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeCache.cs index ffcc2fd..a65eeb7 100644 --- a/src/AngleSharp.Js/Cache/PrototypeCache.cs +++ b/src/AngleSharp.Js/Cache/PrototypeCache.cs @@ -1,25 +1,35 @@ namespace AngleSharp.Js { + using AngleSharp.Js.Cache; using Jint; using Jint.Native.Object; using System; using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Reflection; sealed class PrototypeCache { private readonly ConcurrentDictionary _prototypes; - private readonly Engine _engine; + private readonly ConcurrentDictionary _canonicalTypes; + private readonly IEnumerable _libs; - public PrototypeCache(Engine engine) + public PrototypeCache(Engine engine, IEnumerable libs) { _prototypes = new ConcurrentDictionary { [typeof(Object)] = engine.Intrinsics.Object.PrototypeObject, }; - _engine = engine; + _canonicalTypes = new ConcurrentDictionary(); + _libs = libs; } public ObjectInstance GetOrCreate(Type type, Func creator) => - _prototypes.GetOrAdd(type, creator.Invoke); + _prototypes.GetOrAdd(Canonicalize(type), creator.Invoke); + + // Memoized per engine rather than globally: the set of libraries a document uses is what + // decides the outcome, and that is fixed for an engine but not for the process. + private Type Canonicalize(Type type) => + _canonicalTypes.GetOrAdd(type, m => m.GetDomPrototypeType(_libs)); } } diff --git a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs new file mode 100644 index 0000000..29e9f16 --- /dev/null +++ b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs @@ -0,0 +1,207 @@ +namespace AngleSharp.Js.Cache +{ + using AngleSharp.Attributes; + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// Maps a CLR type onto the single type whose prototype represents it in JS. + /// + /// + /// An instance is wrapped from an internal concrete class (HtmlDivElement), while the + /// constructor exposed to scripts is built from the exported interface (IHtmlDivElement). + /// Left alone the two end up with a prototype each, so neither "instanceof" nor + /// "Object.getPrototypeOf(div) === HTMLDivElement.prototype" can ever hold. Both sides are + /// therefore folded onto the class that defines the DOM name - the topmost class carrying + /// it - which is also the class the prototype chain is built from. + /// + static class PrototypeTypeCache + { + private static readonly ConcurrentDictionary> _definingTypes = new(); + private static readonly ConcurrentDictionary> _exposedTypes = new(); + + /// + /// Gets what the constructor object of the type a prototype belongs to is built from, + /// or null if the type is not exposed as one. + /// + /// + /// A prototype is keyed by the class defining the DOM name, but it is the exported + /// interface next to it that carries the [DomName] - HtmlDivElement has none of its + /// own, IHtmlDivElement is what names HTMLDivElement - so the class has to be traded + /// back for the interface first. + /// + public static ConstructorDefinition GetConstructorDefinition(this Type type, IEnumerable libs) + { + var definition = type.GetConstructorDefinition(); + + if (definition == null) + { + var name = GetCanonicalName(type); + + if (name != null) + { + foreach (var lib in libs) + { + if (GetExposedTypes(lib).TryGetValue(name, out var exposedType)) + { + return exposedType.GetConstructorDefinition(); + } + } + } + } + + return definition; + } + + public static Type GetDomPrototypeType(this Type type, IEnumerable libs) + { + var typeInfo = type.GetTypeInfo(); + + // An enum carries the [DomName] of its owner (NodeType is named "Document"), and the + // closed instantiations of a generic are mutually non-assignable, so a member resolved + // against one of them cannot be invoked on another (IHtmlCollection). Neither may + // be folded onto somebody else's prototype. + if (typeInfo.IsEnum || typeInfo.IsGenericType) + { + return type; + } + + var name = GetCanonicalName(type); + + if (name != null) + { + foreach (var lib in libs) + { + if (GetDefiningTypes(lib).TryGetValue(name, out var definingType)) + { + return definingType; + } + } + } + + return type; + } + + /// + /// Gets the DOM name a type is represented by, which for the many element classes + /// carrying no name of their own (HtmlBoldElement, HtmlSemanticElement, ...) is the name + /// of their nearest named ancestor - just like in a browser, where a "b" element is an + /// HTMLElement. + /// + private static String GetCanonicalName(Type type) + { + var current = type; + + while (current != null) + { + var baseType = current.GetTypeInfo().BaseType; + var name = current.GetOfficialName(baseType); + + if (name != null) + { + return name; + } + + current = baseType; + } + + return null; + } + + private static IReadOnlyDictionary GetDefiningTypes(Assembly assembly) => + _definingTypes.GetOrAdd(assembly, CreateDefiningTypes); + + private static IReadOnlyDictionary CreateDefiningTypes(Assembly assembly) + { + // Ordinal on purpose: XmlHttpRequest is named "XMLHttpRequest" while the + // RequesterState enum next to it is named "XmlHttpRequest". + var result = new Dictionary(StringComparer.Ordinal); + + foreach (var type in GetLoadableTypes(assembly)) + { + var typeInfo = type.GetTypeInfo(); + + // Only a class can define a prototype: the interface is what the DOM exposes, + // but the class is what an instance is built from and what its base type - and + // hence the prototype chain - is taken from. + if (!typeInfo.IsClass || typeInfo.IsGenericType) + { + continue; + } + + var baseType = typeInfo.BaseType; + var name = type.GetOfficialName(baseType); + + if (name == null || String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal)) + { + // Either nothing to define, or the base type already defines the very same + // name - so this class is not the topmost one carrying it. + continue; + } + + // A name may legitimately be defined twice (col and colgroup are both an + // HTMLTableColElement); share a prototype, but pick the same one every time. + if (!result.TryGetValue(name, out var existing) || + String.CompareOrdinal(type.FullName, existing.FullName) < 0) + { + result[name] = type; + } + } + + return result; + } + + private static String GetNameOf(Type type) => + type?.GetOfficialName(type.GetTypeInfo().BaseType); + + private static IReadOnlyDictionary GetExposedTypes(Assembly assembly) => + _exposedTypes.GetOrAdd(assembly, CreateExposedTypes); + + /// + /// Collects the types a DOM name is exposed by, which is what + /// walks - hence exported types only. + /// + private static IReadOnlyDictionary CreateExposedTypes(Assembly assembly) + { + var result = new Dictionary(StringComparer.Ordinal); + + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsEnum) + { + // An enum carries the [DomName] of the type owning it, not one of its own. + continue; + } + + var name = typeInfo.GetCustomAttributes().FirstOrDefault()?.OfficialName; + + // An interface wins over a class: it is the DOM type, and the class is only + // one way of implementing it. + if (name != null && (!result.TryGetValue(name, out var existing) || + (typeInfo.IsInterface && !existing.GetTypeInfo().IsInterface))) + { + result[name] = type; + } + } + + return result; + } + + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(m => m != null); + } + } + } +} diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 55b30a6..5f2033f 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -35,9 +35,9 @@ public EngineInstance(IWindow window, IDictionary assignments, I { options.EnableModules(new JsModuleLoader(this, window.Document, false)); }); - _prototypes = new PrototypeCache(_engine); - _references = new ReferenceCache(); _libs = libs; + _prototypes = new PrototypeCache(_engine, libs); + _references = new ReferenceCache(); foreach (var assignment in assignments) { diff --git a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs index 348ab1a..6a95c26 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs @@ -3,6 +3,7 @@ namespace AngleSharp.Js.Proxies using Jint.Native; using Jint.Native.Object; using Jint.Runtime; + using Jint.Runtime.Descriptors; using System.Reflection; sealed class DomConstructorFunctionInstance : Constructor @@ -14,6 +15,16 @@ public DomConstructorFunctionInstance(EngineInstance instance, MethodInfo constr { _instance = instance; _constructorFunction = constructorFunction; + + // Jint's Constructor leaves the prototype at Object.prototype, which would make a + // DOM constructor the one function in the engine without call, apply or bind. + Prototype = (ObjectInstance)instance.Jint.Intrinsics.Function.Get("prototype"); + + // Image is a second way of naming HTMLImageElement rather than a type of its own, + // so it publishes the prototype of what it returns. Without one, "instanceof" + // against it is a TypeError rather than an answer. + SetOwnProperty("prototype", new PropertyDescriptor( + instance.GetDomPrototype(constructorFunction.ReturnType), false, false, false)); } public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) diff --git a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs index 00783ee..a85e493 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs @@ -3,9 +3,11 @@ namespace AngleSharp.Js using AngleSharp.Js.Cache; using Jint.Native; using Jint.Native.Object; + using Jint.Native.Symbol; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; + using System; using System.Reflection; sealed class DomConstructorInstance : Constructor @@ -13,6 +15,7 @@ sealed class DomConstructorInstance : Constructor private readonly ConstructorInfo _constructor; private readonly EngineInstance _instance; private readonly ObjectInstance _objectPrototype; + private readonly Type _type; public DomConstructorInstance(EngineInstance engine, ConstructorDefinition definition) : base(engine.Jint, definition.Name) @@ -21,6 +24,12 @@ public DomConstructorInstance(EngineInstance engine, ConstructorDefinition defin _objectPrototype = engine.GetDomPrototype(definition.Type); _instance = engine; _constructor = definition.Info; + _type = definition.Type; + + // Jint's Constructor leaves the prototype at Object.prototype, which would make a + // DOM constructor the one function in the engine without call, apply or bind. + Prototype = (ObjectInstance)engine.Jint.Intrinsics.Function.Get("prototype"); + FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true)); SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false)); @@ -38,6 +47,85 @@ public DomConstructorInstance(EngineInstance engine, ConstructorDefinition defin } } + /// + /// Answers "instanceof" itself, because the prototype chain cannot always carry the + /// answer: a mixin such as ParentNode has no class of its own to hang a prototype off, + /// and the closed instantiations of IHtmlCollection<T> are separate types that a + /// single prototype cannot stand for. Built on first ask - most types are never asked. + /// + public override PropertyDescriptor GetOwnProperty(JsValue property) + { + if (property == GlobalSymbolRegistry.HasInstance) + { + var descriptor = base.GetOwnProperty(property); + + if (descriptor == PropertyDescriptor.Undefined) + { + var hasInstance = new ClrFunction(Engine, "[Symbol.hasInstance]", HasInstance, 1, PropertyFlag.Configurable); + descriptor = new PropertyDescriptor(hasInstance, false, false, false); + SetOwnProperty(property, descriptor); + } + + return descriptor; + } + + return base.GetOwnProperty(property); + } + + private JsValue HasInstance(JsValue thisObject, JsValue[] arguments) + { + var value = arguments.Length > 0 ? arguments[0] : JsValue.Undefined; + + if (value is DomNodeInstance node && IsInstance(node.Value)) + { + return JsBoolean.True; + } + + // Not a DOM object of this type, but something may still have been given this + // prototype - Object.create(HTMLDivElement.prototype) is an instance in a browser. + return InheritsFromPrototype(value as ObjectInstance) ? JsBoolean.True : JsBoolean.False; + } + + private Boolean IsInstance(Object value) + { + var type = value.GetType(); + + if (_type.IsAssignableFrom(type)) + { + return true; + } + + // IHtmlCollection is exposed as HTMLCollection, so an instance of any of its + // closed forms answers to it. + if (_type.GetTypeInfo().IsGenericTypeDefinition) + { + foreach (var contract in type.GetTypeInfo().ImplementedInterfaces) + { + if (contract.GetTypeInfo().IsGenericType && contract.GetGenericTypeDefinition() == _type) + { + return true; + } + } + } + + return false; + } + + private Boolean InheritsFromPrototype(ObjectInstance obj) + { + while (obj != null) + { + obj = obj.Prototype; + + if (ReferenceEquals(obj, _objectPrototype)) + { + return true; + } + } + + return false; + } + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { if (_constructor == null) diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index 43ab720..5a0c3d0 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -8,6 +8,7 @@ namespace AngleSharp.Js using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -22,8 +23,8 @@ sealed class DomPrototypeInstance : ObjectInstance private List> _deferred; private Boolean _membersSet; private DomConstructorInstance _constructor; - private MethodInfo _numericIndexer; - private MethodInfo _stringIndexer; + private Indexer _numericIndexer; + private Indexer _stringIndexer; public DomPrototypeInstance(EngineInstance engine, Type type) : base(engine.Jint) @@ -48,8 +49,17 @@ protected override void Initialize() SetAllMembers(_type); SetExtensionMembers(); - // DOM objects can have properties added dynamically - Prototype = _instance.GetDomPrototype(_baseType); + // The base type may fold onto this very prototype - a class carrying no DOM name of + // its own shares the one of its nearest named ancestor. Jint's setter answers a + // prototype cycle by quietly doing nothing, which would leave Object.prototype in + // place and silently cut the chain short, so rule it out here. + var parent = _instance.GetDomPrototype(_baseType); + + if (!ReferenceEquals(parent, this)) + { + // DOM objects can have properties added dynamically + Prototype = parent; + } if (_deferred != null) { @@ -64,7 +74,7 @@ protected override void Initialize() // It is the constructor object that registers "constructor" here, and it is // only built once script names the type. A prototype reached through an // instance instead - the usual way - would otherwise lack the property. - var definition = _type.GetConstructorDefinition(); + var definition = _type.GetConstructorDefinition(_instance.Libs); if (definition != null) { @@ -307,7 +317,7 @@ private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) return; } - var getter = ResolveAccessor(property.GetMethod); + var getter = property.GetMethod; if (getter == null) { @@ -316,40 +326,14 @@ private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) if (indexParameters[0].ParameterType == typeof(Int32)) { - _numericIndexer = getter; + _numericIndexer = new Indexer(getter); } else if (indexParameters[0].ParameterType == typeof(String)) { - _stringIndexer = getter; + _stringIndexer = new Indexer(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) { //TODO Jint @@ -364,6 +348,57 @@ private void SetMethod(String name, MethodInfo method) ), false, false, false)); } } + + /// + /// One of the indexers a prototype offers, invoked against whichever object is being + /// indexed rather than against the type the prototype was created for. + /// + /// + /// A prototype stands for a DOM type, not for a single class - col and colgroup are + /// both an HTMLTableColElement, and every element class carrying no name of its own + /// shares the prototype of its nearest named ancestor. An accessor bound to one of + /// those classes cannot be invoked on the others, so the target decides. + /// + private sealed class Indexer + { + private readonly MethodInfo _declared; + private readonly ConcurrentDictionary _resolved; + + public Indexer(MethodInfo declared) + { + _declared = declared; + + // A public accessor is dispatched virtually and works on any implementation; + // only an explicitly re-implemented one has to be looked up per target. + _resolved = declared.IsPublic ? null : new ConcurrentDictionary(); + } + + public Object Invoke(Object target, Object[] arguments) => + Resolve(target.GetType()).Invoke(target, arguments); + + private MethodInfo Resolve(Type targetType) => + _resolved == null ? _declared : _resolved.GetOrAdd(targetType, ResolveCore); + + private MethodInfo ResolveCore(Type targetType) + { + // 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. + var name = _declared.Name; + var simpleName = name.Substring(name.LastIndexOf('.') + 1); + var parameters = _declared.GetParameters(); + var parameterTypes = new Type[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return targetType.GetRuntimeMethod(simpleName, parameterTypes) ?? _declared; + } + } } }