#nullable enable using Esprima.Ast; using Jint.Collections; using Jint.Native; using Jint.Native.Function; using Jint.Native.Object; using Jint.Runtime.Descriptors; namespace Jint.Runtime.Interpreter.Expressions { /// /// https://tc39.es/ecma262/#sec-object-initializer /// internal sealed class JintObjectExpression : JintExpression { private JintExpression[] _valueExpressions = System.Array.Empty(); private ObjectProperty?[] _properties = System.Array.Empty(); // check if we can do a shortcut when all are object properties // and don't require duplicate checking private bool _canBuildFast; private class ObjectProperty { internal readonly string? _key; private JsString? _keyJsString; internal readonly Property _value; private JintFunctionDefinition? _functionDefinition; public ObjectProperty(string? key, Property property) { _key = key; _value = property; } public JsString? KeyJsString => _keyJsString ??= _key != null ? JsString.Create(_key) : null; public JintFunctionDefinition GetFunctionDefinition(Engine engine) { if (_functionDefinition is not null) { return _functionDefinition; } var function = _value.Value as IFunction; if (function is null) { ExceptionHelper.ThrowSyntaxError(engine.Realm); } _functionDefinition = new JintFunctionDefinition(engine, function); return _functionDefinition; } } public JintObjectExpression(ObjectExpression expression) : base(expression) { _initialized = false; } protected override void Initialize(EvaluationContext context) { _canBuildFast = true; var expression = (ObjectExpression) _expression; if (expression.Properties.Count == 0) { // empty object initializer return; } var engine = context.Engine; _valueExpressions = new JintExpression[expression.Properties.Count]; _properties = new ObjectProperty[expression.Properties.Count]; for (var i = 0; i < _properties.Length; i++) { string? propName = null; var property = expression.Properties[i]; if (property is Property p) { if (p.Key is Literal literal) { propName = EsprimaExtensions.LiteralKeyToString(literal); } if (!p.Computed && p.Key is Identifier identifier) { propName = identifier.Name; } _properties[i] = new ObjectProperty(propName, p); if (p.Kind == PropertyKind.Init || p.Kind == PropertyKind.Data) { var propertyValue = p.Value; _valueExpressions[i] = Build(engine, propertyValue); _canBuildFast &= !propertyValue.IsFunctionDefinition(); } else { _canBuildFast = false; } } else if (property is SpreadElement spreadElement) { _canBuildFast = false; _properties[i] = null; _valueExpressions[i] = Build(engine, spreadElement.Argument); } else { ExceptionHelper.ThrowArgumentOutOfRangeException("property", "cannot handle property " + property); } _canBuildFast &= propName != null; } } protected override ExpressionResult EvaluateInternal(EvaluationContext context) { return _canBuildFast ? BuildObjectFast(context) : BuildObjectNormal(context); } /// /// Version that can safely build plain object with only normal init/data fields fast. /// private ExpressionResult BuildObjectFast(EvaluationContext context) { var obj = context.Engine.Realm.Intrinsics.Object.Construct(0); if (_properties.Length == 0) { return NormalCompletion(obj); } var properties = new PropertyDictionary(_properties.Length, checkExistingKeys: true); for (var i = 0; i < _properties.Length; i++) { var objectProperty = _properties[i]; var valueExpression = _valueExpressions[i]; var propValue = valueExpression.GetValue(context).Value!.Clone(); properties[objectProperty!._key] = new PropertyDescriptor(propValue, PropertyFlag.ConfigurableEnumerableWritable); } obj.SetProperties(properties); return NormalCompletion(obj); } /// /// https://tc39.es/ecma262/#sec-object-initializer-runtime-semantics-propertydefinitionevaluation /// private ExpressionResult BuildObjectNormal(EvaluationContext context) { var engine = context.Engine; var obj = engine.Realm.Intrinsics.Object.Construct(_properties.Length); for (var i = 0; i < _properties.Length; i++) { var objectProperty = _properties[i]; if (objectProperty is null) { // spread if (_valueExpressions[i].GetValue(context).Value is ObjectInstance source) { source.CopyDataProperties(obj, null); } continue; } var property = objectProperty._value; if (property.Method) { var methodDef = property.DefineMethod(obj); methodDef.Closure.SetFunctionName(methodDef.Key); var desc = new PropertyDescriptor(methodDef.Closure, PropertyFlag.ConfigurableEnumerableWritable); obj.DefinePropertyOrThrow(methodDef.Key, desc); continue; } JsValue? propName = objectProperty.KeyJsString; if (propName is null) { propName = TypeConverter.ToPropertyKey(property.GetKey(engine)); } if (property.Kind == PropertyKind.Init || property.Kind == PropertyKind.Data) { var expr = _valueExpressions[i]; var completion = expr.GetValue(context); if (completion.IsAbrupt()) { return completion; } var propValue = completion.Value!.Clone(); if (expr._expression.IsFunctionDefinition()) { var closure = (FunctionInstance) propValue; closure.SetFunctionName(propName); } var propDesc = new PropertyDescriptor(propValue, PropertyFlag.ConfigurableEnumerableWritable); obj.DefinePropertyOrThrow(propName, propDesc); } else if (property.Kind == PropertyKind.Get || property.Kind == PropertyKind.Set) { var function = objectProperty.GetFunctionDefinition(engine); var closure = new ScriptFunctionInstance( engine, function, engine.ExecutionContext.LexicalEnvironment, function.ThisMode); closure.SetFunctionName(propName, property.Kind == PropertyKind.Get ? "get" : "set"); closure.MakeMethod(obj); var propDesc = new GetSetPropertyDescriptor( get: property.Kind == PropertyKind.Get ? closure : null, set: property.Kind == PropertyKind.Set ? closure : null, PropertyFlag.Enumerable | PropertyFlag.Configurable); obj.DefinePropertyOrThrow(propName, propDesc); } } return NormalCompletion(obj); } } }