diff --git a/source/Handlebars.Test/ExceptionTests.cs b/source/Handlebars.Test/ExceptionTests.cs index 5aa3f6f0..434dc999 100644 --- a/source/Handlebars.Test/ExceptionTests.cs +++ b/source/Handlebars.Test/ExceptionTests.cs @@ -12,5 +12,50 @@ public void TestNonClosingBlockExpressionException() Handlebars.Compile("{{#if 0}}test")(new { }); }); } - } + + [Fact] + public void TestLooseClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 0}}test{{/if}}{{/unless}}")(new { }); + }); + } + + [Fact] + public void TestNestedLooseClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 1}}{{#unless 0}}test{{/if}}{{/unless}}{{/if}}")(new { }); + }); + } + + [Fact] + public void TestUnmatchedClosingBlockExpressionException() + { + Assert.Throws(() => + { + Handlebars.Compile("{{#if 0}}test{{/unless}}")(new { }); + }); + } + + [Fact] + public void TestLooseClosingBlockInIteratorExpressionException() + { + var data = new + { + enumerateMe = new + { + foo = "hello", + bar = "world" + } + }; + + Assert.Throws(() => + { + Handlebars.Compile("{{#each enumerateMe}}test{{/if}}{{/each}}")(data); + }); + } + } } diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs index d252974e..40068729 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulator.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -27,10 +26,10 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) while (enumerator.MoveNext()) { var item = (Expression)enumerator.Current; - var context = BlockAccumulatorContext.Create(item, _configuration); + var context = BlockAccumulatorContext.Create(item, null, _configuration); if (context != null) { - yield return AccumulateBlock(enumerator, context); + yield return AccumulateBlock(item, enumerator, context); } else { @@ -40,16 +39,17 @@ public override IEnumerable ConvertTokens(IEnumerable sequence) } private Expression AccumulateBlock( + Expression parentItem, IEnumerator enumerator, BlockAccumulatorContext context) { while (enumerator.MoveNext()) { var item = (Expression)enumerator.Current; - var innerContext = BlockAccumulatorContext.Create(item, _configuration); + var innerContext = BlockAccumulatorContext.Create(item, parentItem, _configuration); if (innerContext != null) { - context.HandleElement(AccumulateBlock(enumerator, innerContext)); + context.HandleElement(AccumulateBlock(item, enumerator, innerContext)); } else if (context.IsClosingElement(item)) { diff --git a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs index 195e8c6a..8ec0682e 100644 --- a/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs +++ b/source/Handlebars/Compiler/Lexer/Converter/BlockAccumulators/BlockAccumulatorContext.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.StringUtils; namespace HandlebarsDotNet.Compiler { @@ -10,7 +11,7 @@ internal abstract class BlockAccumulatorContext private static readonly HashSet ConditionHelpers = new HashSet(StringComparer.OrdinalIgnoreCase){ "#if", "#unless", "^if", "^unless" }; private static readonly HashSet IteratorHelpers = new HashSet(StringComparer.OrdinalIgnoreCase){ "#each", "^each" }; - public static BlockAccumulatorContext Create(Expression item, ICompiledHandlebarsConfiguration configuration) + public static BlockAccumulatorContext Create(Expression item, Expression parentItem, ICompiledHandlebarsConfiguration configuration) { BlockAccumulatorContext context = null; if (IsConditionalBlock(item)) @@ -29,6 +30,10 @@ public static BlockAccumulatorContext Create(Expression item, ICompiledHandlebar { context = new BlockHelperAccumulatorContext(item); } + else if (IsDetachedClosingElement(item, parentItem, out var closingElement)) + { + throw new HandlebarsCompilerException($"A closing element '{closingElement}' was found without a matching open element"); + } return context; } @@ -64,17 +69,55 @@ private static bool IsIteratorBlock(Expression item) private static bool IsPartialBlock (Expression item) { item = UnwrapStatement (item); - switch (item) + return item switch + { + PathExpression expression => expression.Path.StartsWith("#>"), + HelperExpression helperExpression => helperExpression.HelperName.StartsWith("#>"), + _ => false, + }; + } + + private static bool IsDetachedClosingElement(Expression item, Expression parentItem, out string closingElement) + { + closingElement = null; + + var itemElement = GetItemElement(item); + + if (itemElement == null) return false; + + var parentItemElement = GetItemElement(parentItem); + + if (!itemElement.StartsWith("/")) return false; + + if (parentItemElement == null || IsClosingElementNotMatchOpenElement(itemElement, parentItemElement)) { - case PathExpression expression: - return expression.Path.StartsWith("#>"); - - case HelperExpression helperExpression: - return helperExpression.HelperName.StartsWith("#>"); - - default: - return false; + closingElement = itemElement; + + return true; } + + return false; + } + + private static bool IsClosingElementNotMatchOpenElement(string closingElement, string openElement) + { + if (closingElement == null) throw new ArgumentNullException(nameof(closingElement)); + if (openElement == null) throw new ArgumentNullException(nameof(openElement)); + + if (!openElement.StartsWith("#") || openElement.StartsWith("#>") || openElement.StartsWith("#*")) return false; + + return new Substring(openElement, 1) != new Substring(closingElement, 1); + } + + private static string GetItemElement(Expression item) + { + item = UnwrapStatement(item); + return item switch + { + PathExpression pathExpression => pathExpression.Path, + HelperExpression helperExpression => helperExpression.HelperName, + _ => null, + }; } protected static Expression UnwrapStatement(Expression item)