< Summary

Information
Class: Elsa.Expressions.JavaScript.Services.JintJavaScriptEvaluator
Assembly: Elsa.Expressions.JavaScript
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions.JavaScript/Services/JintJavaScriptEvaluator.cs
Line coverage
97%
Covered lines: 75
Uncovered lines: 2
Coverable lines: 77
Total lines: 185
Line coverage: 97.4%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
EvaluateAsync()100%11100%
GetConfiguredEngine()100%44100%
ConfigureClrAccess(...)100%22100%
ConfigureExecutionConstraints(...)100%88100%
ConfigureObjectConverters(...)100%11100%
ConfigureArgumentGetters(...)100%22100%
ConfigureConfigurationAccess(...)50%2266.66%
ExecuteExpressionAndGetResultAsync()100%11100%
GetOrCreatePrepareScript(...)100%44100%
PrepareScript(...)100%11100%
get_Expression()100%210%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions.JavaScript/Services/JintJavaScriptEvaluator.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json;
 3using Acornima.Ast;
 4using Elsa.Expressions.Helpers;
 5using Elsa.Expressions.Models;
 6using Elsa.Expressions.JavaScript.Contracts;
 7using Elsa.Expressions.JavaScript.Notifications;
 8using Elsa.Expressions.JavaScript.ObjectConverters;
 9using Elsa.Expressions.JavaScript.Options;
 10using Elsa.Mediator.Contracts;
 11using Jint;
 12using Microsoft.Extensions.Caching.Memory;
 13using Microsoft.Extensions.Configuration;
 14using Microsoft.Extensions.Options;
 15
 16// ReSharper disable ConvertClosureToMethodGroup
 17namespace Elsa.Expressions.JavaScript.Services;
 18
 19/// <summary>
 20/// Provides a JavaScript evaluator using Jint.
 21/// </summary>
 20822public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> s
 23    : IJavaScriptEvaluator
 24{
 20825    private readonly JintOptions _jintOptions = scriptOptions.Value;
 26
 27    /// <inheritdoc />
 28    [RequiresUnreferencedCode("The Jint library uses reflection and can't be statically analyzed.")]
 29    public async Task<object?> EvaluateAsync(string expression,
 30        Type returnType,
 31        ExpressionExecutionContext context,
 32        ExpressionEvaluatorOptions? options = null,
 33        Action<Engine>? configureEngine = null,
 34        CancellationToken cancellationToken = default)
 35    {
 36        // The script is prepared before the engine is configured so that the identifiers the expression
 37        // actually references are known while the globals are being installed. Handlers that would
 38        // otherwise have to build a global speculatively can then skip the ones the expression cannot read.
 30939        var preparedScript = GetOrCreatePrepareScript(expression);
 30540        var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
 30541        await mediator.SendAsync(new EvaluatingJavaScript(engine, context, expression, preparedScript.ReferencedGlobals)
 30542        var result = await ExecuteExpressionAndGetResultAsync(engine, preparedScript, cancellationToken);
 29843        await mediator.SendAsync(new EvaluatedJavaScript(engine, context, expression, result), cancellationToken);
 44
 29845        return result.ConvertTo(returnType);
 29846    }
 47
 48    private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, 
 49    {
 30550        options ??= new();
 51
 30552        var engineOptions = new Jint.Options
 30553        {
 30554            ExperimentalFeatures = ExperimentalFeature.TaskInterop
 30555        };
 56
 57        // Jint 4.14 changed this default to LiveView, which exposes a CLR array to script as a live view over
 58        // the original array rather than as a copy. Keeping the copy semantics means a script that mutates an
 59        // array does not reach back into the workflow's own data, and that an array survives a round trip as
 60        // object[] the way it always has. Hosts that want the live view can opt in via ConfigureEngineOptions.
 30561        engineOptions.Interop.ArrayConversion = ArrayConversionMode.Copy;
 62
 63        // Expose CLR enums to script as their member name. This is what EnumToStringConverter used to do by
 64        // hand for values crossing the boundary; the built-in switch also covers the direction that converter
 65        // could not reach, a constant read off a registered enum type such as LogPersistenceMode.Include, which
 66        // used to produce a number and therefore never compared equal to the same value held in a variable.
 67        // Values going back to the CLR keep accepting both the name and the number.
 30568        engineOptions.Interop.EnumConversion = EnumConversionMode.String;
 69
 30570        ConfigureClrAccess(engineOptions);
 30571        ConfigureObjectConverters(engineOptions);
 30572        ConfigureExecutionConstraints(engineOptions, cancellationToken);
 73
 30574        await mediator.SendAsync(new CreatingJavaScriptEngine(engineOptions, context), cancellationToken);
 30575        _jintOptions.ConfigureEngineOptionsCallback(engineOptions, context);
 76
 30577        var engine = new Engine(engineOptions);
 78
 30579        configureEngine?.Invoke(engine);
 30580        ConfigureArgumentGetters(engine, options);
 30581        ConfigureConfigurationAccess(engine);
 30582        _jintOptions.ConfigureEngineCallback(engine, context);
 83
 30584        return engine;
 30585    }
 86
 87    private void ConfigureClrAccess(Jint.Options options)
 88    {
 30589        if (_jintOptions.AllowClrAccess)
 1990            options.AllowClr();
 30591    }
 92
 93    private void ConfigureExecutionConstraints(Jint.Options options, CancellationToken cancellationToken)
 94    {
 95        // An expression that never returns would otherwise occupy the calling thread forever.
 30596        if (_jintOptions.ExecutionTimeout is { } executionTimeout)
 30597            options.TimeoutInterval(executionTimeout);
 98
 30599        if (_jintOptions.MaxStatements is { } maxStatements)
 1100            options.MaxStatements(maxStatements);
 101
 305102        if (_jintOptions.MemoryLimit is { } memoryLimit)
 1103            options.LimitMemory(memoryLimit);
 104
 305105        if (_jintOptions.MaxRecursionDepth is { } maxRecursionDepth)
 1106            options.LimitRecursion(maxRecursionDepth);
 107
 108        // Cancelling the workflow should also abort a script that is still running.
 305109        options.CancellationToken(cancellationToken);
 305110    }
 111
 112    private void ConfigureObjectConverters(Jint.Options options)
 113    {
 114        // Each converter declares the CLR types it handles. A converter registered without them has to be
 115        // offered every value crossing the boundary, which costs Jint its compiled member-read and
 116        // method-invoker lanes for every wrapped .NET object in the engine; declaring the types keeps those
 117        // lanes for the members and methods no converter can observe.
 305118        options.AddObjectConverter(new ByteArrayConverter(), typeof(byte[]));
 305119        options.AddObjectConverter(new JsonElementConverter(), typeof(JsonElement));
 305120    }
 121
 122    private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options)
 123    {
 618124        foreach (var argument in options.Arguments)
 7125            engine.SetValue($"get{argument.Key}", (Func<object?>)(() => argument.Value));
 305126    }
 127
 128    private void ConfigureConfigurationAccess(Engine engine)
 129    {
 305130        if (_jintOptions.AllowConfigurationAccess)
 0131            engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
 305132    }
 133
 134    private async Task<object?> ExecuteExpressionAndGetResultAsync(Engine engine, Prepared<Script> preparedScript, Cance
 135    {
 136        // EvaluateAsync awaits a returned promise instead of blocking the calling thread on it, which matters
 137        // for expressions that await a .NET Task, such as the ones calling getSecret().
 305138        var result = await engine.EvaluateAsync(preparedScript, cancellationToken);
 298139        return result.ToObject();
 298140    }
 141
 142    private Prepared<Script> GetOrCreatePrepareScript(string expression)
 143    {
 144        // The key type keeps these entries distinct from any other consumer of the shared cache, so the
 145        // expression itself can be used as the key. A cache hit then costs a dictionary lookup rather than
 146        // a hash of the entire expression plus the allocations needed to render that hash as a string.
 309147        var cacheKey = new ScriptCacheKey(expression);
 148
 149        // Looking the entry up directly rather than through GetOrCreate keeps the factory closure off the
 150        // hot path: it is only needed on a miss.
 309151        if (memoryCache.TryGetValue(cacheKey, out Prepared<Script> cachedScript))
 63152            return cachedScript;
 153
 246154        using var entry = memoryCache.CreateEntry(cacheKey);
 155
 246156        if (_jintOptions.ScriptCacheTimeout.HasValue)
 246157            entry.SetSlidingExpiration(_jintOptions.ScriptCacheTimeout.Value);
 158
 246159        var preparedScript = PrepareScript(expression);
 242160        entry.Value = preparedScript;
 242161        return preparedScript;
 242162    }
 163
 164    private Prepared<Script> PrepareScript(string expression)
 165    {
 246166        var prepareOptions = new ScriptPreparationOptions
 246167        {
 246168            ParsingOptions = new()
 246169            {
 246170                AllowReturnOutsideFunction = true
 246171            },
 246172
 246173            // Collected once per distinct expression, alongside the parse that is already cached, and read by
 246174            // the handlers that would otherwise register a global for every variable, workflow input and
 246175            // activity output in scope.
 246176            CollectReferencedGlobals = true
 246177        };
 246178        return Engine.PrepareScript(expression, options: prepareOptions);
 179    }
 180
 181    /// <summary>
 182    /// Identifies a prepared script in the shared memory cache.
 183    /// </summary>
 0184    private readonly record struct ScriptCacheKey(string Expression);
 185}