using System.Diagnostics; using System.Runtime.CompilerServices; using Jint.Native; using Jint.Native.Object; namespace Jint.Runtime.Descriptors; [DebuggerDisplay("Value: {Value}, Flags: {Flags}")] public class PropertyDescriptor { public static readonly PropertyDescriptor Undefined = new UndefinedPropertyDescriptor(); internal PropertyFlag _flags; internal JsValue? _value; public PropertyDescriptor() : this(PropertyFlag.None) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected PropertyDescriptor(PropertyFlag flags) { _flags = flags & ~PropertyFlag.NonData; } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal PropertyDescriptor(JsValue? value, PropertyFlag flags) : this(flags) { if ((_flags & PropertyFlag.CustomJsValue) != PropertyFlag.None) { #pragma warning disable MA0056 CustomValue = value; #pragma warning restore MA0056 } _value = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public PropertyDescriptor(JsValue? value, bool? writable, bool? enumerable, bool? configurable) { if ((_flags & PropertyFlag.CustomJsValue) != PropertyFlag.None) { #pragma warning disable MA0056 CustomValue = value; #pragma warning restore MA0056 } _value = value; if (writable != null) { Writable = writable.Value; WritableSet = true; } if (enumerable != null) { Enumerable = enumerable.Value; EnumerableSet = true; } if (configurable != null) { Configurable = configurable.Value; ConfigurableSet = true; } } public PropertyDescriptor(PropertyDescriptor descriptor) { Value = descriptor.Value; Enumerable = descriptor.Enumerable; EnumerableSet = descriptor.EnumerableSet; Configurable = descriptor.Configurable; ConfigurableSet = descriptor.ConfigurableSet; Writable = descriptor.Writable; WritableSet = descriptor.WritableSet; } public virtual JsValue? Get => null; public virtual JsValue? Set => null; public bool Enumerable { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & PropertyFlag.Enumerable) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] set { _flags |= PropertyFlag.EnumerableSet; if (value) { _flags |= PropertyFlag.Enumerable; } else { _flags &= ~(PropertyFlag.Enumerable); } } } public bool EnumerableSet { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & (PropertyFlag.EnumerableSet | PropertyFlag.Enumerable)) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set { if (value) { _flags |= PropertyFlag.EnumerableSet; } else { _flags &= ~(PropertyFlag.EnumerableSet); } } } public bool Writable { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & PropertyFlag.Writable) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] set { _flags |= PropertyFlag.WritableSet; if (value) { _flags |= PropertyFlag.Writable; } else { _flags &= ~(PropertyFlag.Writable); } } } public bool WritableSet { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & (PropertyFlag.WritableSet | PropertyFlag.Writable)) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set { if (value) { _flags |= PropertyFlag.WritableSet; } else { _flags &= ~(PropertyFlag.WritableSet); } } } public bool Configurable { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & PropertyFlag.Configurable) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] set { _flags |= PropertyFlag.ConfigurableSet; if (value) { _flags |= PropertyFlag.Configurable; } else { _flags &= ~(PropertyFlag.Configurable); } } } public bool ConfigurableSet { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (_flags & (PropertyFlag.ConfigurableSet | PropertyFlag.Configurable)) != PropertyFlag.None; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set { if (value) { _flags |= PropertyFlag.ConfigurableSet; } else { _flags &= ~(PropertyFlag.ConfigurableSet); } } } public JsValue Value { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if ((_flags & PropertyFlag.CustomJsValue) != PropertyFlag.None) { return CustomValue!; } return _value!; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { if ((_flags & PropertyFlag.CustomJsValue) != PropertyFlag.None) { CustomValue = value; } _value = value; } } protected internal virtual JsValue? CustomValue { get => null; set => Throw.NotImplementedException(); } internal PropertyFlag Flags { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _flags; } /// /// https://tc39.es/ecma262/#sec-topropertydescriptor /// public static PropertyDescriptor ToPropertyDescriptor(Realm realm, JsValue o) { if (o is not ObjectInstance obj) { Throw.TypeError(realm); return null; } bool? enumerable = null; var hasEnumerable = obj.HasProperty(CommonProperties.Enumerable); if (hasEnumerable) { enumerable = TypeConverter.ToBoolean(obj.Get(CommonProperties.Enumerable)); } bool? configurable = null; var hasConfigurable = obj.HasProperty(CommonProperties.Configurable); if (hasConfigurable) { configurable = TypeConverter.ToBoolean(obj.Get(CommonProperties.Configurable)); } JsValue? value = null; var hasValue = obj.HasProperty(CommonProperties.Value); if (hasValue) { value = obj.Get(CommonProperties.Value); } bool? writable = null; var hasWritable = obj.HasProperty(CommonProperties.Writable); if (hasWritable) { writable = TypeConverter.ToBoolean(obj.Get(CommonProperties.Writable)); } JsValue? get = null; var hasGet = obj.HasProperty(CommonProperties.Get); if (hasGet) { get = obj.Get(CommonProperties.Get); } JsValue? set = null; var hasSet = obj.HasProperty(CommonProperties.Set); if (hasSet) { set = obj.Get(CommonProperties.Set); } if ((hasValue || hasWritable) && (hasGet || hasSet)) { Throw.TypeError(realm, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute"); } var desc = hasGet || hasSet ? new GetSetPropertyDescriptor(null, null, PropertyFlag.None) : new PropertyDescriptor(PropertyFlag.None); if (hasEnumerable) { desc.Enumerable = enumerable!.Value; desc.EnumerableSet = true; } if (hasConfigurable) { desc.Configurable = configurable!.Value; desc.ConfigurableSet = true; } if (hasValue) { desc.Value = value!; } if (hasWritable) { desc.Writable = TypeConverter.ToBoolean(writable!.Value); desc.WritableSet = true; } if (hasGet) { if (!get!.IsUndefined() && get!.TryCast() == null) { Throw.TypeError(realm); } ((GetSetPropertyDescriptor) desc).SetGet(get!); } if (hasSet) { if (!set!.IsUndefined() && set!.TryCast() is null) { Throw.TypeError(realm); } ((GetSetPropertyDescriptor) desc).SetSet(set!); } if ((hasSet || hasGet) && (hasValue || hasWritable)) { Throw.TypeError(realm); } return desc; } /// /// https://tc39.es/ecma262/#sec-frompropertydescriptor /// public static JsValue FromPropertyDescriptor(Engine engine, PropertyDescriptor desc, bool strictUndefined = false) { if (ReferenceEquals(desc, Undefined)) { return JsValue.Undefined; } var obj = engine.Realm.Intrinsics.Object.Construct(Arguments.Empty); var properties = new PropertyDictionary(4, checkExistingKeys: false); // TODO should not check for strictUndefined, but needs a bigger cleanup // we should have possibility to leave out the properties in property descriptors as newer tests // also assert properties to be undefined if (desc.IsDataDescriptor()) { properties["value"] = new PropertyDescriptor(desc.Value ?? JsValue.Undefined, PropertyFlag.ConfigurableEnumerableWritable); if (desc._flags != PropertyFlag.None || desc.WritableSet) { properties["writable"] = new PropertyDescriptor(desc.Writable, PropertyFlag.ConfigurableEnumerableWritable); } } else { properties["get"] = new PropertyDescriptor(desc.Get ?? JsValue.Undefined, PropertyFlag.ConfigurableEnumerableWritable); properties["set"] = new PropertyDescriptor(desc.Set ?? JsValue.Undefined, PropertyFlag.ConfigurableEnumerableWritable); } if (!strictUndefined || desc.EnumerableSet) { properties["enumerable"] = new PropertyDescriptor(desc.Enumerable, PropertyFlag.ConfigurableEnumerableWritable); } if (!strictUndefined || desc.ConfigurableSet) { properties["configurable"] = new PropertyDescriptor(desc.Configurable, PropertyFlag.ConfigurableEnumerableWritable); } obj.SetProperties(properties); return obj; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsAccessorDescriptor() { return Get is not null || Set is not null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsDataDescriptor() { if ((_flags & PropertyFlag.NonData) != PropertyFlag.None) { return false; } return (_flags & (PropertyFlag.WritableSet | PropertyFlag.Writable)) != PropertyFlag.None || (_flags & PropertyFlag.CustomJsValue) != PropertyFlag.None && CustomValue is not null || _value is not null; } /// /// http://www.ecma-international.org/ecma-262/5.1/#sec-8.10.3 /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsGenericDescriptor() { return !IsDataDescriptor() && !IsAccessorDescriptor(); } private sealed class UndefinedPropertyDescriptor : PropertyDescriptor { public UndefinedPropertyDescriptor() : base(PropertyFlag.None | PropertyFlag.CustomJsValue) { } protected internal override JsValue? CustomValue { set => Throw.InvalidOperationException("making changes to undefined property's descriptor is not allowed"); } } internal sealed class AllForbiddenDescriptor : PropertyDescriptor { private static readonly PropertyDescriptor[] _cache; public static readonly AllForbiddenDescriptor NumberZero = new AllForbiddenDescriptor(JsNumber.Create(0)); public static readonly AllForbiddenDescriptor NumberOne = new AllForbiddenDescriptor(JsNumber.Create(1)); public static readonly AllForbiddenDescriptor BooleanFalse = new AllForbiddenDescriptor(JsBoolean.False); public static readonly AllForbiddenDescriptor BooleanTrue = new AllForbiddenDescriptor(JsBoolean.True); static AllForbiddenDescriptor() { _cache = new PropertyDescriptor[10]; for (int i = 0; i < _cache.Length; ++i) { _cache[i] = new AllForbiddenDescriptor(JsNumber.Create(i)); } } private AllForbiddenDescriptor(JsValue value) : base(PropertyFlag.AllForbidden) { _value = value; } public static PropertyDescriptor ForNumber(int number) { var temp = _cache; return (uint) number < temp.Length ? temp[number] : new PropertyDescriptor(number, PropertyFlag.AllForbidden); } } }