< Summary

Information
Class: Elsa.Dsl.ElsaScript.Parser.ParseException
Assembly: Elsa.Dsl.ElsaScript
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Dsl.ElsaScript/Parser/ElsaScriptParser.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 2
Coverable lines: 2
Total lines: 630
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Dsl.ElsaScript/Parser/ElsaScriptParser.cs

#LineLine coverage
 1using Elsa.Dsl.ElsaScript.Ast;
 2using Elsa.Dsl.ElsaScript.Contracts;
 3using Parlot;
 4using Parlot.Fluent;
 5
 6namespace Elsa.Dsl.ElsaScript.Parser;
 7
 8/// <summary>
 9/// ElsaScript parser using Parlot for robust parsing.
 10/// </summary>
 11public class ElsaScriptParser : IElsaScriptParser
 12{
 13    private static readonly Parser<ProgramNode> ProgramParser;
 14
 15    static ElsaScriptParser()
 16    {
 17        // Keywords
 18        var useKeyword = Terms.Text("use");
 19        var workflowKeyword = Terms.Text("workflow");
 20        var expressionsKeyword = Terms.Text("expressions");
 21        var listenKeyword = Terms.Text("listen");
 22        var varKeyword = Terms.Text("var");
 23        var constKeyword = Terms.Text("const");
 24        var forKeyword = Terms.Text("for");
 25        var foreachKeyword = Terms.Text("foreach");
 26        var inKeyword = Terms.Text("in");
 27        var toKeyword = Terms.Text("to");
 28        var throughKeyword = Terms.Text("through");
 29        var stepKeyword = Terms.Text("step");
 30        var flowchartKeyword = Terms.Text("flowchart");
 31        var entryKeyword = Terms.Text("entry");
 32
 33        // Basic tokens
 34        var identifier = Terms.Identifier();
 35        var stringLiteral = Terms.String();
 36        var integerLiteral = Terms.Integer();
 37        var decimalLiteral = Terms.Decimal();
 38
 39        // Punctuation
 40        var semicolon = Terms.Char(';');
 41        var comma = Terms.Char(',');
 42        var colon = Terms.Char(':');
 43        var leftParen = Terms.Char('(');
 44        var rightParen = Terms.Char(')');
 45        var leftBrace = Terms.Char('{');
 46        var rightBrace = Terms.Char('}');
 47        var leftBracket = Terms.Char('[');
 48        var rightBracket = Terms.Char(']');
 49        var dot = Terms.Char('.');
 50        var arrow = Terms.Text("=>");
 51        var rightArrow = Terms.Text("->");
 52        var equals = Terms.Char('=');
 53
 54        // Deferred parsers for recursive structures
 55        var expression = Deferred<ExpressionNode>();
 56        var statement = Deferred<StatementNode>();
 57
 58        // Expression parsers
 59        var booleanLiteral = Terms.Text("true").Or(Terms.Text("false"))
 60            .Then<ExpressionNode>(x => new LiteralNode { Value = x.ToString() == "true" });
 61
 62        var numberLiteral = decimalLiteral
 63            .Then<ExpressionNode>(x => new LiteralNode { Value = x });
 64
 65        var intLiteral = integerLiteral
 66            .Then<ExpressionNode>(x => new LiteralNode { Value = (long)x });
 67
 68        var stringExpr = stringLiteral
 69            .Then<ExpressionNode>(x => new LiteralNode { Value = x.ToString() });
 70
 71        var identifierExpr = identifier
 72            .Then<ExpressionNode>(x => new IdentifierNode { Name = x.ToString() });
 73
 74        // Array literal: [expr, expr, ...]
 75        var commaSeparatedExpression = expression.And(ZeroOrOne(comma)).Then(x => x.Item1);
 76        var arrayLiteral = Between(leftBracket, ZeroOrMany(commaSeparatedExpression), rightBracket)
 77            .Then<ExpressionNode>(elements => new ArrayLiteralNode { Elements = elements.ToList() });
 78
 79        // Elsa expression: lang => <raw text until matching )>
 80        // We need to capture raw text after => up to the closing parenthesis
 81        // This supports nested parentheses by counting depth
 82        // Use a custom scanner-based parser wrapped in RawExpressionParser
 83        var rawExpressionText = new RawExpressionParser();
 84
 85        var elsaExpressionWithLang = identifier
 86            .And(arrow)
 87            .And(rawExpressionText)
 88            .Then<ExpressionNode>(x => new ElsaExpressionNode
 89            {
 90                Language = x.Item1.ToString(),
 91                Expression = x.Item3.ToString().Trim()
 92            });
 93
 94        var elsaExpressionWithoutLang = arrow
 95            .And(rawExpressionText)
 96            .Then<ExpressionNode>(x => new ElsaExpressionNode
 97            {
 98                Language = null,
 99                Expression = x.Item2.ToString().Trim()
 100            });
 101
 102        var elsaExpression = elsaExpressionWithLang.Or(elsaExpressionWithoutLang);
 103
 104        // Expression priority: try most specific first
 105        expression.Parser = elsaExpression
 106            .Or(arrayLiteral)
 107            .Or(booleanLiteral)
 108            .Or(numberLiteral)
 109            .Or(intLiteral)
 110            .Or(stringExpr)
 111            .Or(identifierExpr);
 112
 113        // Argument parser: name: value or just value
 114        var namedArgument = identifier
 115            .And(colon)
 116            .And(expression)
 117            .Then(x => new ArgumentNode { Name = x.Item1.ToString(), Value = x.Item3 });
 118
 119        var positionalArgument = expression
 120            .Then(x => new ArgumentNode { Value = x });
 121
 122        var argument = namedArgument.Or(positionalArgument);
 123
 124        var commaSeparatedArgument = argument.And(ZeroOrOne(comma)).Then(x => x.Item1);
 125        var arguments = ZeroOrMany(commaSeparatedArgument);
 126
 127        // Activity invocation: ActivityName(args) - with or without arguments
 128        var activityInvocationWithArgs = identifier
 129            .And(leftParen)
 130            .And(arguments)
 131            .And(rightParen)
 132            .Then(x => new ActivityInvocationNode
 133            {
 134                ActivityName = x.Item1.ToString(),
 135                Arguments = x.Item3.ToList()
 136            });
 137
 138        var activityInvocationNoArgs = identifier
 139            .And(leftParen)
 140            .And(rightParen)
 141            .Then(x => new ActivityInvocationNode
 142            {
 143                ActivityName = x.Item1.ToString(),
 144                Arguments = []
 145            });
 146
 147        var activityInvocation = activityInvocationWithArgs.Or(activityInvocationNoArgs);
 148
 149        // Variable declaration: var/const name = expr
 150        var variableKindParser = varKeyword.Or(constKeyword);
 151
 152        var variableDeclaration = variableKindParser
 153            .And(identifier)
 154            .And(equals)
 155            .And(expression)
 156            .Then<StatementNode>(x =>
 157            {
 158                // x is a flat tuple (kind, identifier, equals, expression)
 159                var kind = x.Item1.ToString() switch
 160                {
 161                    "var" => VariableKind.Var,
 162                    "const" => VariableKind.Const,
 163                    _ => VariableKind.Var
 164                };
 165
 166                return new VariableDeclarationNode
 167                {
 168                    Kind = kind,
 169                    Name = x.Item2.ToString(),
 170                    Value = x.Item4
 171                };
 172            });
 173
 174        // Listen statement: listen ActivityName(args)
 175        var listenStatement = listenKeyword
 176            .And(activityInvocation)
 177            .Then<StatementNode>(x => new ListenNode { Activity = x.Item2 });
 178
 179        // Statement: variable declaration, listen, or activity invocation
 180        var activityStatement = activityInvocation
 181            .Then<StatementNode>(x => x);
 182
 183        // Declare deferred for loop, foreach, and flowchart parsers
 184        var forStatement = Deferred<StatementNode>();
 185        var foreachStatement = Deferred<StatementNode>();
 186        var flowchartStatement = Deferred<StatementNode>();
 187
 188        statement.Parser = variableDeclaration
 189            .Or(listenStatement)
 190            .Or(forStatement)
 191            .Or(foreachStatement)
 192            .Or(flowchartStatement)
 193            .Or(activityStatement);
 194
 195        // Statement with optional semicolon
 196        var statementWithSemicolon = statement.And(ZeroOrOne(semicolon)).Then(x => x.Item1);
 197
 198        // For loop statement: for (var i = 0 to 10 step 1) { body } or for (i = 0 to 10) statement
 199        // Must be defined after statementWithSemicolon
 200        var rangeOperator = toKeyword.Or(throughKeyword);
 201
 202        // For body can be either a block or a single statement
 203        var forBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
 204            .Then(statements => (StatementNode)(statements.Count == 1
 205                ? statements.First()
 206                : new BlockNode { Statements = statements.ToList() }));
 207        var forSingleStatementBody = statement;
 208        var forBody = forBlockBody.Or(forSingleStatementBody);
 209
 210        // For header with optional var: (var i = start to/through end step stepValue)
 211        // or (i = start to/through end step stepValue)
 212        // Step clause is optional
 213        var optionalVarKeyword = ZeroOrOne(varKeyword);
 214        var optionalStepClause = ZeroOrOne(stepKeyword.And(expression).Then(x => x.Item2));
 215
 216        var forHeader = Between(leftParen,
 217            optionalVarKeyword
 218                .And(identifier)
 219                .And(equals)
 220                .And(expression)
 221                .And(rangeOperator)
 222                .And(expression)
 223                .And(optionalStepClause)
 224                .Then(x => (
 225                    HasVar: x.Item1 != null,
 226                    VarName: x.Item2.ToString(),
 227                    Start: x.Item4,
 228                    RangeOp: x.Item5.ToString(),
 229                    End: x.Item6,
 230                    Step: x.Item7
 231                )),
 232            rightParen);
 233
 234        var forStatementParser = forKeyword
 235            .And(forHeader)
 236            .And(forBody)
 237            .Then<StatementNode>(result =>
 238            {
 239                var header = result.Item2;
 240                var body = result.Item3;
 241
 242                // Default step to 1 if not specified
 243                var stepExpr = header.Step ?? new LiteralNode { Value = 1 };
 244
 245                return new ForNode
 246                {
 247                    DeclaresVariable = header.HasVar,
 248                    VariableName = header.VarName,
 249                    Start = header.Start,
 250                    End = header.End,
 251                    Step = stepExpr,
 252                    IsInclusive = header.RangeOp == "through",
 253                    Body = body
 254                };
 255            });
 256
 257        forStatement.Parser = forStatementParser;
 258
 259        // ForEach statement: foreach (var item in collection) { body } or foreach (item in collection) statement
 260        // Must be defined after statementWithSemicolon
 261        // ForEach body can be either a block or a single statement
 262        var foreachBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
 263            .Then(statements => (StatementNode)(statements.Count == 1
 264                ? statements.First()
 265                : new BlockNode { Statements = statements.ToList() }));
 266        var foreachSingleStatementBody = statement;
 267        var foreachBody = foreachBlockBody.Or(foreachSingleStatementBody);
 268
 269        // ForEach header with optional var: (var item in collection) or (item in collection)
 270        var foreachOptionalVarKeyword = ZeroOrOne(varKeyword);
 271
 272        var foreachHeader = Between(leftParen,
 273            foreachOptionalVarKeyword
 274                .And(identifier)
 275                .And(inKeyword)
 276                .And(expression)
 277                .Then(x => (
 278                    HasVar: x.Item1 != null,
 279                    VarName: x.Item2.ToString(),
 280                    Collection: x.Item4
 281                )),
 282            rightParen);
 283
 284        var foreachStatementParser = foreachKeyword
 285            .And(foreachHeader)
 286            .And(foreachBody)
 287            .Then<StatementNode>(result =>
 288            {
 289                var header = result.Item2;
 290                var body = result.Item3;
 291
 292                return new ForEachNode
 293                {
 294                    DeclaresVariable = header.HasVar,
 295                    VariableName = header.VarName,
 296                    Collection = header.Collection,
 297                    Body = body
 298                };
 299            });
 300
 301        foreachStatement.Parser = foreachStatementParser;
 302
 303        // Flowchart statement: flowchart { [variables] [nodes] [connections] [entry] }
 304        // Node declaration: label: statement;
 305        // Entry declaration: entry label;
 306        // Connection declaration: source -> target; or source.Outcome -> target;
 307
 308        // Flowchart body element can be:
 309        // 1. Variable declaration
 310        // 2. Node declaration (label: statement)
 311        // 3. Entry declaration (entry label)
 312        // 4. Connection declaration (source -> target or source.Outcome -> target)
 313
 314        // Node declaration: label: activityInvocation; or label: { block }
 315        // Note: We use activityInvocation directly (not statement) to avoid circular dependency
 316        // since statement includes flowchart which would include node declarations
 317        var nodeBlock = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
 318            .Then<StatementNode>(statements => statements.Count == 1
 319                ? statements.First()
 320                : new BlockNode { Statements = statements.ToList() });
 321
 322        var nodeActivityStatement = activityInvocation.Then<StatementNode>(s => s);
 323
 324        var nodeDeclaration = identifier
 325            .And(colon)
 326            .And(nodeBlock.Or(nodeActivityStatement))
 327            .And(ZeroOrOne(semicolon))
 328            .Then(x => new LabeledActivityNode
 329            {
 330                Label = x.Item1.ToString(),
 331                Activity = x.Item3
 332            });
 333
 334        // Entry declaration: entry label;
 335        var entryDeclaration = entryKeyword
 336            .And(identifier)
 337            .And(ZeroOrOne(semicolon))
 338            .Then(x => x.Item2.ToString());
 339
 340        // Connection declaration: source -> target; or source.Outcome -> target;
 341        // Source can be: identifier or identifier.identifier (with outcome)
 342        var optionalOutcome = ZeroOrOne(dot.And(identifier).Then(x => x.Item2.ToString()));
 343
 344        var connectionSource = identifier
 345            .And(optionalOutcome)
 346            .Then(x => (
 347                SourceLabel: x.Item1.ToString(),
 348                Outcome: x.Item2
 349            ));
 350
 351        var connectionTarget = identifier;
 352
 353        var connectionDeclaration = connectionSource
 354            .And(rightArrow)
 355            .And(connectionTarget)
 356            .And(ZeroOrOne(semicolon))
 357            .Then(x => new ConnectionNode
 358            {
 359                Source = x.Item1.SourceLabel,
 360                Outcome = x.Item1.Outcome,
 361                Target = x.Item3.ToString()
 362            });
 363
 364        // Flowchart body element type - try each parser in order
 365        var flowchartBodyElement = variableDeclaration.Then<object>(v => v)
 366            .Or(entryDeclaration.Then<object>(e => e))
 367            .Or(nodeDeclaration.Then<object>(n => n))
 368            .Or(connectionDeclaration.Then<object>(c => c));
 369
 370        var flowchartBody = Between(leftBrace, ZeroOrMany(flowchartBodyElement), rightBrace);
 371
 372        var flowchartStatementParser = flowchartKeyword
 373            .And(flowchartBody)
 374            .Then<StatementNode>(result =>
 375            {
 376                var bodyElements = result.Item2;
 377
 378                var variables = new List<VariableDeclarationNode>();
 379                var nodes = new List<LabeledActivityNode>();
 380                var connections = new List<ConnectionNode>();
 381                string? entryPoint = null;
 382
 383                foreach (var element in bodyElements)
 384                {
 385                    if (element is VariableDeclarationNode varDecl)
 386                        variables.Add(varDecl);
 387                    else if (element is LabeledActivityNode node)
 388                        nodes.Add(node);
 389                    else if (element is ConnectionNode conn)
 390                        connections.Add(conn);
 391                    else if (element is string entry)
 392                        entryPoint = entry;
 393                }
 394
 395                return new FlowchartNode
 396                {
 397                    Variables = variables,
 398                    Activities = nodes,
 399                    Connections = connections,
 400                    EntryPoint = entryPoint
 401                };
 402            });
 403
 404        flowchartStatement.Parser = flowchartStatementParser;
 405
 406        // Use statement: use Namespace; or use expressions lang;
 407        var namespaceUse = identifier
 408            .And(ZeroOrMany(dot.And(identifier)))
 409            .Then(x =>
 410            {
 411                var ns = x.Item1.ToString();
 412                foreach (var part in x.Item2)
 413                {
 414                    ns += "." + part.Item2.ToString();
 415                }
 416                return new UseNode { Type = UseType.Namespace, Value = ns };
 417            });
 418
 419        var expressionUse = expressionsKeyword
 420            .And(identifier)
 421            .Then(x => new UseNode { Type = UseType.Expressions, Value = x.Item2.ToString() });
 422
 423        var useStatement = useKeyword
 424            .And(expressionUse.Or(namespaceUse))
 425            .And(ZeroOrOne(semicolon))
 426            .Then(x => x.Item2);
 427
 428        // Workflow metadata: name: value
 429        var metadataEntry = identifier
 430            .And(colon)
 431            .And(expression)
 432            .Then(x => (Name: x.Item1.ToString(), Value: EvaluateConstantExpressionStatic(x.Item3)));
 433
 434        var commaSeparatedMetadata = metadataEntry.And(ZeroOrOne(comma)).Then(x => x.Item1);
 435        var metadataList = ZeroOrMany(commaSeparatedMetadata);
 436
 437        // Workflow declaration: workflow Identifier [(metadata)] { [use statements] [statements] }
 438        var workflowMetadata = Between(leftParen, metadataList, rightParen);
 439
 440        // Workflow body can contain use statements and regular statements
 441        var workflowUseStatement = useStatement;
 442
 443        var workflowBodyElement = Deferred<object>();
 444        workflowBodyElement.Parser = workflowUseStatement
 445            .Then<object>(u => u)
 446            .Or(statementWithSemicolon.Then<object>(s => s));
 447
 448        var workflowBody = Between(leftBrace, ZeroOrMany(workflowBodyElement), rightBrace);
 449
 450        var workflowWithMetadata = workflowKeyword
 451            .And(identifier)
 452            .And(workflowMetadata)
 453            .Then(x => (WorkflowId: x.Item2.ToString(), Metadata: x.Item3));
 454
 455        var workflowWithoutMetadata = workflowKeyword
 456            .And(identifier)
 457            .Then(x => (WorkflowId: x.Item2.ToString(), Metadata: (IReadOnlyList<(string Name, object Value)>?)null));
 458
 459        var workflowHeader = workflowWithMetadata.Or(workflowWithoutMetadata);
 460
 461        var workflowDeclaration = workflowHeader
 462            .And(workflowBody)
 463            .Then(x =>
 464            {
 465                var header = x.Item1;
 466                var bodyElements = x.Item2;
 467
 468                var metadataDict = new Dictionary<string, object>();
 469                if (header.Metadata != null)
 470                {
 471                    foreach (var entry in header.Metadata)
 472                    {
 473                        metadataDict[entry.Name] = entry.Value;
 474                    }
 475                }
 476
 477                // Separate use statements from regular statements in body
 478                var workflowUses = new List<UseNode>();
 479                var statements = new List<StatementNode>();
 480
 481                foreach (var element in bodyElements)
 482                {
 483                    if (element is UseNode useNode)
 484                        workflowUses.Add(useNode);
 485                    else if (element is StatementNode stmt)
 486                        statements.Add(stmt);
 487                }
 488
 489                return new WorkflowNode
 490                {
 491                    Id = header.WorkflowId,
 492                    Metadata = metadataDict,
 493                    UseStatements = workflowUses,
 494                    Body = statements
 495                };
 496            });
 497
 498        // Program with single workflow: [global use statements] [workflow declaration]
 499        var programWithWorkflow = ZeroOrMany(useStatement)
 500            .And(workflowDeclaration)
 501            .Then(x =>
 502            {
 503                var globalUses = x.Item1.Select(u => (UseNode)u).ToList();
 504                var workflow = x.Item2;
 505
 506                return new ProgramNode
 507                {
 508                    GlobalUseStatements = globalUses,
 509                    Workflows = new List<WorkflowNode> { workflow }
 510                };
 511            });
 512
 513        // Fallback: raw statements without workflow keyword (backward compatibility)
 514        // Only match if there are actual statements (OneOrMany)
 515        var programWithStatements = ZeroOrMany(useStatement)
 516            .And(OneOrMany(statementWithSemicolon))
 517            .Then(x =>
 518            {
 519                var globalUses = x.Item1.Select(u => (UseNode)u).ToList();
 520                var statements = x.Item2.ToList();
 521
 522                return new ProgramNode
 523                {
 524                    GlobalUseStatements = globalUses,
 525                    Workflows = new List<WorkflowNode>
 526                    {
 527                        new WorkflowNode
 528                        {
 529                            Id = "DefaultWorkflow",
 530                            UseStatements = new List<UseNode>(),
 531                            Body = statements
 532                        }
 533                    }
 534                };
 535            });
 536
 537        var programParser = programWithWorkflow.Or(programWithStatements);
 538
 539        ProgramParser = programParser;
 540    }
 541
 542    /// <inheritdoc />
 543    public ProgramNode Parse(string source)
 544    {
 545        if (!ProgramParser.TryParse(source, out var result, out var error))
 546        {
 547            var errorMessage = error != null
 548                ? $"{error.Message} at {error.Position}"
 549                : "Unknown parse error";
 550            throw new ParseException($"Failed to parse ElsaScript: {errorMessage}");
 551        }
 552        return result;
 553    }
 554
 555    /// <summary>
 556    /// Static helper to evaluate constant expressions during parsing.
 557    /// </summary>
 558    private static object EvaluateConstantExpressionStatic(ExpressionNode exprNode)
 559    {
 560        return exprNode switch
 561        {
 562            LiteralNode literal => literal.Value ?? string.Empty,
 563            IdentifierNode identifier => identifier.Name,
 564            _ => string.Empty
 565        };
 566    }
 567}
 568
 569/// <summary>
 570/// Exception thrown when parsing fails.
 571/// </summary>
 572public class ParseException : Exception
 573{
 0574    public ParseException(string message) : base(message)
 575    {
 0576    }
 577}
 578
 579/// <summary>
 580/// Custom parser that captures raw text after => until the matching closing parenthesis.
 581/// Supports nested parentheses.
 582/// </summary>
 583internal sealed class RawExpressionParser : Parser<TextSpan>
 584{
 585    public override bool Parse(ParseContext context, ref ParseResult<TextSpan> result)
 586    {
 587        context.EnterParser(this);
 588
 589        var scanner = context.Scanner;
 590        var start = scanner.Cursor.Offset;
 591        var depth = 0;
 592
 593        while (!scanner.Cursor.Eof)
 594        {
 595            var ch = scanner.Cursor.Current;
 596
 597            if (ch == '(')
 598            {
 599                depth++;
 600                scanner.Cursor.Advance();
 601            }
 602            else if (ch == ')')
 603            {
 604                if (depth == 0)
 605                {
 606                    // This is the closing paren for the activity invocation
 607                    break;
 608                }
 609                depth--;
 610                scanner.Cursor.Advance();
 611            }
 612            else
 613            {
 614                scanner.Cursor.Advance();
 615            }
 616        }
 617
 618        var length = scanner.Cursor.Offset - start;
 619        if (length == 0)
 620        {
 621            context.ExitParser(this);
 622            return false;
 623        }
 624
 625        var text = new TextSpan(scanner.Buffer, start, length);
 626        result.Set(start, scanner.Cursor.Offset, text);
 627        context.ExitParser(this);
 628        return true;
 629    }
 630}

Methods/Properties

.ctor(System.String)