diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs
index 5d1b975..88b41b7 100644
--- a/src/AngleSharp.Js.Tests/DomTests.cs
+++ b/src/AngleSharp.Js.Tests/DomTests.cs
@@ -26,5 +26,47 @@ public async Task NodeHasChildNodesWithChildren()
var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.hasChildNodes()".EvalScriptAsync();
Assert.AreEqual("True", result);
}
+
+ [Test]
+ public async Task PrototypeChainOfElementIsBuiltCompletely()
+ {
+ var result = await "(function () { var p = Object.getPrototypeOf(document.createElement('div')), t = []; while (p) { t.push(p[Symbol.toStringTag]); p = Object.getPrototypeOf(p); } return t.join(); })()".EvalScriptAsync();
+ Assert.AreEqual("HTMLDivElement,HTMLElement,Element,Node,EventTarget,", result);
+ }
+
+ [Test]
+ public async Task ConstructorPropertyOfPrototypeRefersBackToTheConstructor()
+ {
+ var result = await "HTMLDivElement.prototype.constructor === HTMLDivElement".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/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs
index 64bd0d3..4c36f1f 100644
--- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs
+++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs
@@ -22,7 +22,19 @@ public DomConstructorInstance(EngineInstance engine, Type type)
_instance = engine;
FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true));
SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false));
- _objectPrototype.FastSetProperty("constructor", new PropertyDescriptor(this, true, false, true));
+
+ var constructor = new PropertyDescriptor(this, true, false, true);
+
+ // Every exposed type gets a constructor, so writing this directly would make
+ // each of their prototypes register its members right away.
+ if (_objectPrototype is DomPrototypeInstance domPrototype)
+ {
+ domPrototype.DefineDeferredProperty("constructor", constructor);
+ }
+ else
+ {
+ _objectPrototype.FastSetProperty("constructor", constructor);
+ }
}
public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor)
diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs
index df226b9..a3a393c 100644
--- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs
+++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs
@@ -15,24 +15,75 @@ sealed class DomPrototypeInstance : ObjectInstance
{
private readonly String _name;
private readonly EngineInstance _instance;
+ private readonly Type _type;
+ private readonly Type _baseType;
- private PropertyInfo _numericIndexer;
- private PropertyInfo _stringIndexer;
+ private List> _deferred;
+ private Boolean _membersSet;
+ private MethodInfo _numericIndexer;
+ private MethodInfo _stringIndexer;
public DomPrototypeInstance(EngineInstance engine, Type type)
: base(engine.Jint)
{
- var baseType = type.GetTypeInfo().BaseType ?? typeof(Object);
- _name = type.GetOfficialName(baseType);
+ _baseType = type.GetTypeInfo().BaseType ?? typeof(Object);
+ _name = type.GetOfficialName(_baseType);
_instance = engine;
+ _type = type;
+ }
+
+ // A document uses a handful of the DOM types an assembly exposes, but a prototype
+ // is created for every one of them. Reflecting over the whole type tree is by far
+ // the most expensive part of that, so it waits until the prototype is looked at.
+ // Jint calls this before serving any property, and marks the instance initialized
+ // beforehand, so the registration below can use the object as usual.
+ protected override void Initialize()
+ {
+ _membersSet = true;
Set(GlobalSymbolRegistry.ToStringTag, _name);
- SetAllMembers(type);
+ SetAllMembers(_type);
SetExtensionMembers();
// DOM objects can have properties added dynamically
- Prototype = engine.GetDomPrototype(baseType);
+ Prototype = _instance.GetDomPrototype(_baseType);
+
+ if (_deferred != null)
+ {
+ foreach (var property in _deferred)
+ {
+ FastSetProperty(property.Key, property.Value);
+ }
+
+ _deferred = null;
+ }
+ }
+
+ // The prototype link is only established once the members are known, so reading it
+ // has to initialize as well - not every reader goes through a property lookup.
+ protected override ObjectInstance GetPrototypeOf()
+ {
+ EnsureInitialized();
+ return base.GetPrototypeOf();
+ }
+
+ ///
+ /// Defines a property on the prototype without forcing its members to be
+ /// registered. The property is applied after the members, so it keeps
+ /// overriding a member of the same name.
+ ///
+ public void DefineDeferredProperty(String name, PropertyDescriptor descriptor)
+ {
+ if (_membersSet)
+ {
+ FastSetProperty(name, descriptor);
+ }
+ else
+ {
+ _deferred = _deferred ?? new List>();
+ _deferred.Add(new KeyValuePair(name, descriptor));
+ }
}
public Boolean TryGetFromIndex(Object value, String index, out PropertyDescriptor result)
@@ -40,12 +91,14 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto
// If we have a numeric indexer and the property is numeric
result = default;
+ EnsureInitialized();
+
if (_numericIndexer != null && Int32.TryParse(index, out var numericIndex))
{
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 +123,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,19 +282,54 @@ 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)
{
//TODO Jint