Skip to content

Commit

Permalink
Implemented parsing option to enforce that an end-statement must matc…
Browse files Browse the repository at this point in the history
…h a block begin.
  • Loading branch information
Chris Wiitamaki committed Oct 19, 2022
1 parent dbcd324 commit 2b96fac
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/Scriban.Tests/TestParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,48 @@ public void EnsureStackOverflowCanBeAvoidedForSelfReferentialObjectGraphs()
Assert.Throws<ScriptRuntimeException>(() => template.Render(context));
}

[TestCase(null, false)]
[TestCase(false, false)]
[TestCase(true, true)]
public void TestEnforceEndStatementMustMatchBlockBegin(bool? enforceOption, bool expectErrors)
{
// test three conditions of the new parser option: default (not supplied), false, and true
ParserOptions? options = null;
if (enforceOption != null)
{
options = new ParserOptions() { EnforceEndStatementMustMatchBlockBegin = enforceOption.Value };
}

// variations on nesting levels, each with an unmatched end-statement
var inputsToTest = new[]
{
@"ab{{end}}c", // no blocks
@"a{{if true}}b{{end}}{{end}}c", // one-level block
@"a{{if true}}{{for i in 0..1}}b{{end}}{{end}}{{end}}c", // two-level block (nested)
};

// test each variation
foreach(var input in inputsToTest)
{
var template = Template.Parse(input, parserOptions: options);
Assert.AreEqual(expectErrors, template.HasErrors);

if (expectErrors)
{
Assert.AreEqual(1, template.Messages.Count);
StringAssert.Contains("Found <end> statement without a corresponding beginning of a block", template.Messages[0].ToString());
}
else
{
Assert.AreEqual(0, template.Messages.Count);

// no errors expected, so now render and ensure the parsing stopped after the extra end-statement
var result = template.Render();
StringAssert.EndsWith("b", result);
}
}
}

private static void TestFile(string inputName, bool testASTInstead = false)
{
var filename = Path.GetFileName(inputName);
Expand Down
12 changes: 12 additions & 0 deletions src/Scriban/Parsing/Parser.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ private ScriptBlockStatement ParseBlockStatement(ScriptNode parentStatement, boo

if (hasEnd)
{
if (_blockLevel == 1 && Options.EnforceEndStatementMustMatchBlockBegin)
{
if (_isLiquid)
{
var syntax = ScriptSyntaxAttribute.Get(parentStatement);
LogError(parentStatement, parentStatement?.Span ?? CurrentSpan, $"Found <end> statement `end{syntax.TypeName}` without a corresponding beginning of a block");
}
else
{
LogError(parentStatement, GetSpanForToken(Previous), $"Found <end> statement without a corresponding beginning of a block");
}
}
break;
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Scriban/Parsing/ParserOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@ struct ParserOptions
/// If the number cannot be represented to a decimal, it will fall back to a double.
/// </summary>
public bool ParseFloatAsDecimal { get; set; }

/// <summary>
/// <c>true</c> to return a parse error if an end statement does not correspond to a block (e.g. if-end, while-end, etc.).
/// Default is <c>false</c>: an end statement that is not part of a block is allowed, but will prematurely end the parsing.
/// </summary>
public bool EnforceEndStatementMustMatchBlockBegin { get; set; }
}
}

0 comments on commit 2b96fac

Please sign in to comment.