using System.Diagnostics.CodeAnalysis;
using Esprima;
using Esprima.Ast;
using Esprima.Utils;
using Jint.Native.Object;
using Jint.Native.Promise;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Environments;
using Jint.Runtime.Interop;
using Jint.Runtime.Interpreter;
using Jint.Runtime.Interpreter.Statements;
using Jint.Runtime.Modules;
using Environment = Jint.Runtime.Environments.Environment;
namespace Jint.Native.ShadowRealm;
///
/// https://tc39.es/proposal-shadowrealm/#sec-properties-of-shadowrealm-instances
///
#pragma warning disable MA0049
public sealed class ShadowRealm : ObjectInstance
#pragma warning restore MA0049
{
private readonly JavaScriptParser _parser;
internal readonly Realm _shadowRealm;
private readonly ExecutionContext _executionContext;
internal ShadowRealm(Engine engine, ExecutionContext executionContext, Realm shadowRealm) : base(engine)
{
_parser = new(new ParserOptions
{
Tolerant = false,
RegexTimeout = engine.Options.Constraints.RegexTimeout
});
_executionContext = executionContext;
_shadowRealm = shadowRealm;
}
public JsValue Evaluate(string sourceText)
{
var callerRealm = _engine.Realm;
return PerformShadowRealmEval(sourceText, callerRealm);
}
public JsValue Evaluate(Script script)
{
var callerRealm = _engine.Realm;
return PerformShadowRealmEval(script, callerRealm);
}
public JsValue ImportValue(string specifier, string exportName)
{
var callerRealm = _engine.Realm;
var value = ShadowRealmImportValue(specifier, exportName, callerRealm);
_engine.RunAvailableContinuations();
return value;
}
[RequiresUnreferencedCode("User supplied delegate")]
public ShadowRealm SetValue(string name, Delegate value)
{
_shadowRealm.GlobalObject.FastSetProperty(name, new PropertyDescriptor(new DelegateWrapper(_engine, value), PropertyFlag.NonEnumerable));
return this;
}
public ShadowRealm SetValue(string name, string value)
{
return SetValue(name, JsString.Create(value));
}
public ShadowRealm SetValue(string name, double value)
{
return SetValue(name, JsNumber.Create(value));
}
public ShadowRealm SetValue(string name, int value)
{
return SetValue(name, JsNumber.Create(value));
}
public ShadowRealm SetValue(string name, bool value)
{
return SetValue(name, value ? JsBoolean.True : JsBoolean.False);
}
public ShadowRealm SetValue(string name, JsValue value)
{
_shadowRealm.GlobalObject.Set(name, value);
return this;
}
public ShadowRealm SetValue(string name, object obj)
{
var value = obj is Type t
? TypeReference.CreateTypeReference(_engine, t)
: JsValue.FromObject(_engine, obj);
return SetValue(name, value);
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval
///
internal JsValue PerformShadowRealmEval(string sourceText, Realm callerRealm)
{
var evalRealm = _shadowRealm;
_engine._host.EnsureCanCompileStrings(callerRealm, evalRealm);
Script script;
try
{
script = _parser.ParseScript(sourceText, source: null, _engine._isStrict);
}
catch (ParserException e)
{
if (string.Equals(e.Description, Messages.InvalidLHSInAssignment, StringComparison.Ordinal))
{
ExceptionHelper.ThrowReferenceError(callerRealm, Messages.InvalidLHSInAssignment);
}
else
{
ExceptionHelper.ThrowSyntaxError(callerRealm, e.Message);
}
return default;
}
return PerformShadowRealmEvalInternal(script, callerRealm);
}
internal JsValue PerformShadowRealmEval(Script script, Realm callerRealm)
{
var evalRealm = _shadowRealm;
_engine._host.EnsureCanCompileStrings(callerRealm, evalRealm);
return PerformShadowRealmEvalInternal(script, callerRealm);
}
internal JsValue PerformShadowRealmEvalInternal(Script script, Realm callerRealm)
{
var evalRealm = _shadowRealm;
ref readonly var body = ref script.Body;
if (body.Count == 0)
{
return Undefined;
}
var validator = new ShadowScriptValidator(callerRealm);
validator.Visit(script);
var strictEval = script.Strict;
var runningContext = _engine.ExecutionContext;
var lexEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, evalRealm.GlobalEnv);
Environment varEnv = evalRealm.GlobalEnv;
if (strictEval)
{
varEnv = lexEnv;
}
// If runningContext is not already suspended, suspend runningContext.
var evalContext = new ExecutionContext(null, lexEnv, varEnv, null, evalRealm, null);
_engine.EnterExecutionContext(evalContext);
Completion result;
try
{
_engine.EvalDeclarationInstantiation(script, varEnv, lexEnv, privateEnv: null, strictEval);
using (new StrictModeScope(strictEval, force: true))
{
result = new JintScript(script).Execute(new EvaluationContext(_engine));
}
if (result.Type == CompletionType.Throw)
{
ThrowCrossRealmError(callerRealm, result.GetValueOrDefault().ToString());
}
}
finally
{
_engine.LeaveExecutionContext();
}
return GetWrappedValue(callerRealm, callerRealm, result.Value);
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-getwrappedvalue
///
private static JsValue GetWrappedValue(Realm throwerRealm, Realm callerRealm, JsValue value)
{
if (value is ObjectInstance oi)
{
if (!oi.IsCallable)
{
ThrowCrossRealmError(throwerRealm, "Result is not callable");
}
return WrappedFunctionCreate(throwerRealm, callerRealm, oi);
}
return value;
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-wrappedfunctioncreate
///
private static WrappedFunction WrappedFunctionCreate(Realm throwerRealm, Realm callerRealm, ObjectInstance target)
{
var wrapped = new WrappedFunction(callerRealm.GlobalEnv._engine, callerRealm, target);
try
{
CopyNameAndLength(wrapped, target);
}
catch (JavaScriptException ex)
{
ThrowCrossRealmError(throwerRealm, ex.Message);
}
return wrapped;
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-copynameandlength
///
private static void CopyNameAndLength(WrappedFunction f, ObjectInstance target, string? prefix = null, int argCount = 0)
{
var L = JsNumber.PositiveZero;
var targetHasLength = target.HasOwnProperty("length");
if (targetHasLength)
{
var targetLen = target.Get("length");
if (targetLen is JsNumber number)
{
if (number.IsPositiveInfinity())
{
L = number;
}
else if (number.IsNegativeInfinity())
{
L = JsNumber.PositiveZero;
}
else
{
var targetLenAsInt = TypeConverter.ToIntegerOrInfinity(targetLen);
L = JsNumber.Create(System.Math.Max(targetLenAsInt - argCount, 0));
}
}
}
f.SetFunctionLength(L);
var targetName = target.Get(CommonProperties.Name);
if (!targetName.IsString())
{
targetName = JsString.Empty;
}
f.SetFunctionName(targetName, prefix);
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue
///
internal JsValue ShadowRealmImportValue(
string specifierString,
string exportNameString,
Realm callerRealm)
{
var innerCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise);
// var runningContext = _engine.ExecutionContext;
// 4. If runningContext is not already suspended, suspend runningContext.
_engine.EnterExecutionContext(_executionContext);
_engine._host.LoadImportedModule(null, new ModuleRequest(specifierString, []), innerCapability);
_engine.LeaveExecutionContext();
var onFulfilled = new StepsFunction(_engine, callerRealm, exportNameString);
var promiseCapability = PromiseConstructor.NewPromiseCapability(_engine, _engine.Realm.Intrinsics.Promise);
var value = PromiseOperations.PerformPromiseThen(_engine, (JsPromise) innerCapability.PromiseInstance, onFulfilled, callerRealm.Intrinsics.ThrowTypeError, promiseCapability);
return value;
}
private sealed class StepsFunction : Function.Function
{
private readonly string _exportNameString;
public StepsFunction(Engine engine, Realm realm, string exportNameString) : base(engine, realm, JsString.Empty)
{
_exportNameString = exportNameString;
SetFunctionLength(JsNumber.PositiveOne);
}
protected internal override JsValue Call(JsValue thisObject, JsValue[] arguments)
{
var exports = (ModuleNamespace) arguments.At(0);
var f = this;
var s = _exportNameString;
var hasOwn = exports.HasOwnProperty(s);
if (!hasOwn)
{
ExceptionHelper.ThrowTypeError(_realm, $"export name {s} missing");
}
var value = exports.Get(s);
var realm = f._realm;
return GetWrappedValue(_engine.Realm, realm, value);
}
}
private static ShadowRealm ValidateShadowRealmObject(Realm callerRealm, JsValue thisObj)
{
var instance = thisObj as ShadowRealm;
if (instance is null)
{
ExceptionHelper.ThrowTypeError(callerRealm, "object must be a ShadowRealm");
}
return instance;
}
private static void ThrowCrossRealmError(Realm callerRealm, string message)
{
ExceptionHelper.ThrowTypeError(callerRealm, "Cross-Realm Error: " + message);
}
private sealed class WrappedFunction : Function.Function
{
private readonly ObjectInstance _wrappedTargetFunction;
public WrappedFunction(
Engine engine,
Realm callerRealm,
ObjectInstance wrappedTargetFunction) : base(engine, callerRealm, null)
{
_wrappedTargetFunction = wrappedTargetFunction;
_prototype = callerRealm.Intrinsics.Function.PrototypeObject;
}
///
/// https://tc39.es/proposal-shadowrealm/#sec-wrapped-function-exotic-objects-call-thisargument-argumentslist
///
protected internal override JsValue Call(JsValue thisArgument, JsValue[] arguments)
{
var target = _wrappedTargetFunction;
var targetRealm = GetFunctionRealm(target);
var callerRealm = GetFunctionRealm(this);
var wrappedArgs = new JsValue[arguments.Length];
for (var i = 0; i < arguments.Length; i++)
{
wrappedArgs[i] = GetWrappedValue(callerRealm, targetRealm, arguments[i]);
}
var wrappedThisArgument = GetWrappedValue(callerRealm, targetRealm, thisArgument);
JsValue result;
try
{
result = target.Call(wrappedThisArgument, wrappedArgs);
}
catch (JavaScriptException ex)
{
ThrowCrossRealmError(_realm, ex.Message);
return default!;
}
return GetWrappedValue(callerRealm, callerRealm, result);
}
}
///
/// If body Contains NewTarget is true, throw a SyntaxError exception.
/// If body Contains SuperProperty is true, throw a SyntaxError exception.
/// If body Contains SuperCall is true, throw a SyntaxError exception.
///
private sealed class ShadowScriptValidator : AstVisitor
{
private readonly Realm _realm;
public ShadowScriptValidator(Realm realm)
{
_realm = realm;
}
protected override object? VisitSuper(Super super)
{
ExceptionHelper.ThrowTypeError(_realm, "Shadow realm code cannot contain super");
return null;
}
}
}