Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Execution/Interpreter.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,9 @@ private async Task<ExecutionResult> ExecuteForInAsync(Stmt.ForIn forIn)

IEnumerable<string> keys = obj switch
{
SharpTSObject o => o.Fields.Keys,
// Own enumerable keys only, hiding boxed-primitive internal slots and
// honoring enumerability — consistent with Object.keys (#475).
SharpTSObject o => o.OwnEnumerableKeys(),
SharpTSInstance inst => inst.GetFieldNames(),
// for...in skips holes per ECMA-262.
SharpTSArray arr => Enumerable.Range(0, arr.Length).Where(arr.HasIndex).Select(i => i.ToString()),
Expand Down
4 changes: 3 additions & 1 deletion Execution/Interpreter.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,9 @@ private ExecutionResult ExecuteForIn(Stmt.ForIn forIn)

IEnumerable<string> keys = obj switch
{
SharpTSObject o => o.Fields.Keys,
// Own enumerable keys only, hiding boxed-primitive internal slots and
// honoring enumerability — consistent with Object.keys (#475).
SharpTSObject o => o.OwnEnumerableKeys(),
SharpTSInstance i => i.GetFieldNames(),
// for...in skips holes per ECMA-262 (only own enumerable index properties
// that actually exist — holes don't).
Expand Down
6 changes: 5 additions & 1 deletion Runtime/BuiltIns/BuiltInConstructorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,11 @@ private static SharpTSObject CreateBoxedString(IReadOnlyList<object?> args)
};
for (int i = 0; i < value.Length; i++)
dict[i.ToString()] = value[i].ToString();
return new SharpTSObject(dict);
var wrapper = new SharpTSObject(dict);
// ECMA-262 §22.1.4.1: a String exotic's `length` is non-enumerable, so it
// must not surface in Object.keys/for-in (only the indexed chars do) (#475).
wrapper.MarkNonEnumerable("length");
return wrapper;
}

/// <summary>
Expand Down
43 changes: 39 additions & 4 deletions Runtime/BuiltIns/ObjectBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private static RuntimeValue KeysV2(Interpreter _, RuntimeValue receiver, ReadOnl
var arg = args[0].ToObject();
if (arg is SharpTSObject obj)
{
var keys = obj.Fields.Keys.Select(k => (object?)k).ToList();
var keys = obj.OwnEnumerableKeys().Select(k => (object?)k).ToList();
return RuntimeValue.FromObject(new SharpTSArray(keys));
}
if (arg is SharpTSArray arr)
Expand Down Expand Up @@ -100,7 +100,7 @@ private static RuntimeValue ValuesV2(Interpreter _, RuntimeValue receiver, ReadO
var arg = args[0].ToObject();
if (arg is SharpTSObject obj)
{
var values = obj.Fields.Values.ToList();
var values = obj.OwnEnumerableKeys().Select(k => obj.Fields[k]).ToList();
return RuntimeValue.FromObject(new SharpTSArray(values));
}
if (arg is SharpTSArray arr)
Expand Down Expand Up @@ -142,8 +142,8 @@ private static RuntimeValue EntriesV2(Interpreter _, RuntimeValue receiver, Read
var arg = args[0].ToObject();
if (arg is SharpTSObject obj)
{
var entries = obj.Fields.Select(kv =>
(object?)new SharpTSArray([(object?)kv.Key, kv.Value])).ToList();
var entries = obj.OwnEnumerableKeys().Select(k =>
(object?)new SharpTSArray([(object?)k, obj.Fields[k]])).ToList();
return RuntimeValue.FromObject(new SharpTSArray(entries));
}
if (arg is SharpTSArray arr)
Expand Down Expand Up @@ -524,6 +524,8 @@ private static RuntimeValue IsSealedV2(Interpreter _, RuntimeValue receiver, Rea
// (undefined → "undefined", null → "null", -0 → "0", booleans lowercase).
var propertyKey = PropertyKeyConverter.ToPropertyKeyString(args[1]);

PreserveOmittedAttributes(target, propertyKey, descriptor, descriptorArg, interpreter);

bool success;
switch (target)
{
Expand Down Expand Up @@ -1561,6 +1563,39 @@ private static void ApplyBooleanAttributes(SharpTSPropertyDescriptor descriptor,
if (c is not (null or SharpTSUndefined)) descriptor.Configurable = Compilation.RuntimeTypes.IsTruthy(c);
}

/// <summary>
/// ECMA-262 §10.1.6.3 ValidateAndApplyPropertyDescriptor: when redefining an
/// EXISTING own property, attributes the descriptor omits are preserved from
/// the current property rather than reset to false (<see cref="SharpTSPropertyDescriptor"/>
/// defaults absent booleans to false, and <see cref="ApplyBooleanAttributes"/> only
/// sets the ones actually present). Without this,
/// <c>Object.defineProperty(o, "a", { writable:false })</c> on an enumerable data
/// property <c>a</c> wrongly clears its enumerable flag, dropping it from
/// Object.keys/values/entries/for-in (#475). Scoped to plain objects
/// (<see cref="SharpTSObject"/>) — the surface affected by #475's enumerability
/// changes; instances/arrays/dicts keep their existing behavior.
/// </summary>
private static void PreserveOmittedAttributes(
object? target, string propertyKey, SharpTSPropertyDescriptor descriptor, object? descObj, Interpreter interpreter)
{
if (target is not SharpTSObject obj || descObj is null) return;
// Only a plain object/dictionary descriptor; exotic descriptors keep prior behavior.
if (descObj is not (SharpTSObject or Dictionary<string, object?>)) return;
bool exists = obj.Fields.ContainsKey(propertyKey) || obj.AccessorPropertyNames.Contains(propertyKey);
if (!exists) return; // a brand-new property defaults omitted attributes to false (spec)
var existing = obj.GetPropertyFlags(propertyKey);
// Attribute presence is read via interpreter.GetProperty, which walks the
// prototype chain (matching ECMA-262 ToPropertyDescriptor), so an inherited
// attribute is correctly treated as specified rather than preserved.
if (!DescriptorSpecifies(descObj, "writable", interpreter)) descriptor.Writable = existing.Writable;
if (!DescriptorSpecifies(descObj, "enumerable", interpreter)) descriptor.Enumerable = existing.Enumerable;
if (!DescriptorSpecifies(descObj, "configurable", interpreter)) descriptor.Configurable = existing.Configurable;
}

/// <summary>True when the source descriptor object explicitly provides <paramref name="attr"/>.</summary>
private static bool DescriptorSpecifies(object? descObj, string attr, Interpreter interpreter)
=> interpreter.GetProperty(descObj, attr) is not (null or SharpTSUndefined);

/// <summary>
/// Runtime helper for Object.create called from compiled code.
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions Runtime/Types/SharpTSObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -604,5 +604,41 @@ public PropertyDescriptorFlags GetPropertyFlags(string name)
return PropertyDescriptorFlags.Default;
}

/// <summary>
/// Marks an existing own data property as non-enumerable, leaving its other
/// attributes unchanged. Used for a String exotic wrapper's <c>length</c>,
/// which is non-enumerable per ECMA-262 §22.1.4.1 so it stays out of
/// Object.keys/values/entries and for-in (#475).
/// </summary>
internal void MarkNonEnumerable(string name)
{
var cur = GetPropertyFlags(name);
_descriptors ??= new Dictionary<string, PropertyDescriptorFlags>();
_descriptors[name] = PropertyDescriptorFlags.ForDefineProperty(cur.Writable, enumerable: false, cur.Configurable);
}

/// <summary>
/// True for the internal-slot field names that back boxed primitive wrappers
/// (<c>new String/Number/Boolean</c>): they hold [[StringData]]/[[NumberData]]/
/// [[BooleanData]] and the type tag, not real own properties, so enumeration
/// must skip them (#475).
/// </summary>
internal static bool IsInternalSlot(string key) => key is "__primitiveType" or "__primitiveValue";

/// <summary>
/// Own enumerable string-keyed data property names, in insertion order: the
/// data fields minus the boxed-primitive internal slots, honoring per-property
/// enumerability (a <c>defineProperty</c> <c>enumerable:false</c> field, and a
/// String exotic's non-enumerable <c>length</c>). Shared by Object.keys/values/
/// entries and for-in so internal slots no longer leak and enumerability is
/// respected (#475), matching compiled <c>$Runtime.GetKeys</c>.
/// </summary>
internal IEnumerable<string> OwnEnumerableKeys()
{
foreach (var key in _fields.Keys)
if (!IsInternalSlot(key) && GetPropertyFlags(key).Enumerable)
yield return key;
}

public override string ToString() => $"{{ {string.Join(", ", _fields.Select(f => $"{f.Key}: {f.Value}"))} }}";
}
Loading
Loading