< Summary

Information
Class: Elsa.Expressions.JavaScript.Handlers.ConfigureEngineWithVariablesAndInputOutputAccessors
Assembly: Elsa.Expressions.JavaScript
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs
Line coverage
96%
Covered lines: 55
Uncovered lines: 2
Coverable lines: 57
Total lines: 157
Line coverage: 96.4%
Branch coverage
92%
Covered branches: 37
Total branches: 40
Branch coverage: 92.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
HandleAsync()50%2288.88%
GetReferencedGlobalsFilter(...)87.5%8888.88%
CreateVariableAccessors(...)100%66100%
CreateWorkflowInputAccessors(...)90%1010100%
CreateActivityOutputAccessorsAsync()100%1010100%
IsActivityOutputAccessorName(...)100%22100%
IsReferenced(...)100%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs

#LineLine coverage
 1using Elsa.Expressions.Models;
 2using Elsa.Extensions;
 3using Elsa.Expressions.JavaScript.Extensions;
 4using Elsa.Expressions.JavaScript.Notifications;
 5using Elsa.Expressions.JavaScript.Options;
 6using Elsa.Mediator.Contracts;
 7using Humanizer;
 8using JetBrains.Annotations;
 9using Jint;
 10using Microsoft.Extensions.Options;
 11
 12namespace Elsa.Expressions.JavaScript.Handlers;
 13
 14/// <summary>
 15/// A handler that configures the Jint engine with workflow input and output accessors.
 16/// </summary>
 17[UsedImplicitly]
 90918public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions<JintOptions> options) : INotificationHandler<E
 19{
 20    /// <summary>
 21    /// Identifiers whose presence means the expression can reach a global it never names, so that no accessor may
 22    /// be filtered out. See the remarks on <see cref="GetReferencedGlobalsFilter"/>.
 23    /// </summary>
 324    private static readonly string[] DynamicCodeSignals = ["eval", "Function", "globalThis"];
 25
 26    /// <inheritdoc />
 27    public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
 28    {
 28529        if (options.Value.DisableWrappers)
 030            return;
 31
 28532        var engine = notification.Engine;
 28533        var context = notification.Context;
 28534        var referencedGlobals = GetReferencedGlobalsFilter(notification);
 35
 36        // The order of the next 3 lines is important.
 28537        CreateVariableAccessors(engine, context, referencedGlobals);
 28538        CreateWorkflowInputAccessors(engine, context, referencedGlobals);
 28539        await CreateActivityOutputAccessorsAsync(engine, context, referencedGlobals);
 28540    }
 41
 42    /// <summary>
 43    /// Returns the identifiers the expression references, to be used as a filter over the accessors that would
 44    /// otherwise all be registered, or <see langword="null"/> when no filtering may be applied and every accessor
 45    /// has to be registered the way it always was.
 46    /// </summary>
 47    /// <remarks>
 48    /// <para>
 49    /// The filter is sound for an expression that names an accessor the way one is meant to be named, as an
 50    /// identifier. It is not sound for one that builds or reaches a name at run time, which leaves no identifier
 51    /// for the parser to report. Four such forms are detectable and each turns the filter off:
 52    /// </para>
 53    /// <list type="bullet">
 54    /// <item><description>a direct <c>eval</c> call, reported as <see cref="Jint.ReferencedGlobals.HasDirectEvalCall"/>
 55    /// <item><description>an indirect <c>eval</c> call, which is <em>not</em> flagged as a direct one — the identifier 
 56    /// <item><description>the <c>Function</c> constructor, likewise signalled by the identifier <c>Function</c>, and wh
 57    /// <item><description>a reference to <c>globalThis</c>, which reaches a global without naming it.</description></it
 58    /// </list>
 59    /// <para>
 60    /// What stays undetectable is reaching the global object without naming it at all: a sloppy-mode top-level
 61    /// <c>this</c>, or <c>[].constructor.constructor(…)</c>. An expression written that way loses the generated
 62    /// accessor, not the data: <c>getVariable(name)</c>, <c>getInput(name)</c> and
 63    /// <c>getOutputFrom(activityId, outputName)</c> are always registered and reach the same values.
 64    /// </para>
 65    /// </remarks>
 66    private static ReferencedGlobals? GetReferencedGlobalsFilter(EvaluatingJavaScript notification)
 67    {
 28568        var referencedGlobals = notification.ReferencedGlobals;
 69
 28570        if (referencedGlobals is null)
 071            return null;
 72
 28573        if (referencedGlobals.HasDirectEvalCall)
 174            return null;
 75
 225776        foreach (var dynamicCodeSignal in DynamicCodeSignals)
 77        {
 84978            if (referencedGlobals.Contains(dynamicCodeSignal))
 979                return null;
 80        }
 81
 27582        return referencedGlobals;
 83    }
 84
 85    private void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context, ReferencedGlobals? reference
 86    {
 28587        var variableNames = context.GetVariableNamesInScope().FilterInvalidVariableNames().ToList();
 88
 88689        foreach (var variableName in variableNames)
 90        {
 15891            var pascalName = variableName.Pascalize();
 15892            var getterName = $"get{pascalName}";
 15893            var setterName = $"set{pascalName}";
 94
 15895            if (IsReferenced(referencedGlobals, getterName))
 7996                engine.SetValue(getterName, (Func<object?>)(() => context.GetVariableInScope(variableName)));
 97
 15898            if (IsReferenced(referencedGlobals, setterName))
 999                engine.SetValue(setterName, (Action<object?>)(value =>
 9100                {
 4101                    engine.SyncVariablesContainer(options, variableName, value);
 4102                    context.SetVariableInScope(variableName, value);
 13103                }));
 104        }
 285105    }
 106
 107    private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context, ReferencedGlobals? refe
 108    {
 109        // Create workflow input accessors - only if the current activity is not part of a composite activity definition
 110        // Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variab
 285111        if (context.IsContainedWithinCompositeActivity())
 8112            return;
 113
 339114        var inputs = context.GetWorkflowInputs().Where(x => x.Name.IsValidVariableName()).ToDictionary(x => x.Name, Stri
 115
 277116        if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
 123117            return;
 118
 154119        var inputDefinitions = workflowExecutionContext.Workflow.Inputs;
 120
 318121        foreach (var inputDefinition in inputDefinitions)
 122        {
 5123            var accessorName = $"get{inputDefinition.Name}";
 124
 5125            if (!IsReferenced(referencedGlobals, accessorName))
 126                continue;
 127
 4128            var input = inputs.GetValueOrDefault(inputDefinition.Name);
 8129            engine.SetValue(accessorName, (Func<object?>)(() => input?.Value));
 130        }
 154131    }
 132
 133    private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context, Refe
 134    {
 135        // Naming the accessors means walking every node of the enclosing container and resolving each one against
 136        // the activity registry, so this is the one piece of engine setup whose cost grows with the size of the
 137        // workflow rather than the size of the expression. Every name it can produce is get{Output}From{Activity},
 138        // so an expression referencing no identifier of that shape needs none of the walk.
 285139        if (referencedGlobals is not null && !referencedGlobals.Any(IsActivityOutputAccessorName))
 272140            return;
 141
 13142        var activityOutputs = context.GetActivityOutputs();
 143
 28144        await foreach (var activityOutput in activityOutputs)
 4145        foreach (var outputName in activityOutput.OutputNames.FilterInvalidVariableNames())
 146        {
 1147            var accessorName = $"get{outputName}From{activityOutput.ActivityName.Pascalize()}";
 148
 1149            if (IsReferenced(referencedGlobals, accessorName))
 2150                engine.SetValue(accessorName, (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputN
 151        }
 285152    }
 153
 285154    private static bool IsActivityOutputAccessorName(string name) => name.StartsWith("get", StringComparison.Ordinal) &&
 155
 322156    private static bool IsReferenced(ReferencedGlobals? referencedGlobals, string name) => referencedGlobals is null || 
 157}