Add error mechanism for TagHelperParseTreeRewriter.

- Replaced customer facing Debug.Assert with a new error mechanism to surface errors to GenerateCode callers.
- The new mechanism is a general purpose way for ISyntaxTreeRewriters to add errors to the parsing phase.

#174
This commit is contained in:
NTaylorMullen 2014-10-04 23:03:23 -07:00 committed by N. Taylor Mullen
parent 72c449bf86
commit 74974d371c
12 changed files with 270 additions and 32 deletions

View File

@ -6,19 +6,17 @@ using Microsoft.AspNet.Razor.Parser.SyntaxTree;
namespace Microsoft.AspNet.Razor.Parser
{
/// <summary>
/// Defines the contract for rewriting a syntax tree.
/// Contract for rewriting a syntax tree.
/// </summary>
public interface ISyntaxTreeRewriter
{
/// <summary>
/// Rewrites the provided <paramref name="input"/> syntax tree.
/// Rewrites the provided <paramref name="context"/>s <see cref= "RewritingContext.SyntaxTree" />.
/// </summary>
/// <param name="input">The current syntax tree.</param>
/// <returns>The <paramref name="input"/> syntax tree or a syntax tree to be used instead of the
/// <paramref name="input"/> tree.</returns>
/// <param name="context">Contains information on the rewriting of the syntax tree.</param>
/// <remarks>
/// If you choose not to modify the syntax tree you can always return <paramref name="input"/>.
/// To modify the syntax tree replace the <paramref name="context"/>s <see cref="RewritingContext.SyntaxTree"/>.
/// </remarks>
Block Rewrite(Block input);
void Rewrite(RewritingContext context);
}
}

View File

@ -28,11 +28,11 @@ namespace Microsoft.AspNet.Razor.Parser
get { return _blocks.Count > 0 ? _blocks.Peek() : null; }
}
public virtual Block Rewrite(Block input)
public virtual void Rewrite(RewritingContext context)
{
input.Accept(this);
context.SyntaxTree.Accept(this);
Debug.Assert(_blocks.Count == 1);
return _blocks.Pop().Build();
context.SyntaxTree = _blocks.Pop().Build();
}
public override void VisitBlock(Block block)

View File

@ -136,25 +136,27 @@ namespace Microsoft.AspNet.Razor.Parser
ParserResults results = context.CompleteParse();
// Rewrite whitespace if supported
Block current = results.Document;
var rewritingContext = new RewritingContext(results.Document);
foreach (ISyntaxTreeRewriter rewriter in Optimizers)
{
current = rewriter.Rewrite(current);
rewriter.Rewrite(rewritingContext);
}
if (_tagHelperDescriptorResolver != null)
{
var tagHelperRegistrationVisitor = new TagHelperRegistrationVisitor(_tagHelperDescriptorResolver);
var tagHelperProvider = tagHelperRegistrationVisitor.CreateProvider(current);
var tagHelperProvider = tagHelperRegistrationVisitor.CreateProvider(rewritingContext.SyntaxTree);
var tagHelperParseTreeRewriter = new TagHelperParseTreeRewriter(tagHelperProvider);
// Rewrite the document to utilize tag helpers
current = tagHelperParseTreeRewriter.Rewrite(current);
tagHelperParseTreeRewriter.Rewrite(rewritingContext);
}
var syntaxTree = rewritingContext.SyntaxTree;
// Link the leaf nodes into a chain
Span prev = null;
foreach (Span node in current.Flatten())
foreach (Span node in syntaxTree.Flatten())
{
node.Previous = prev;
if (prev != null)
@ -164,8 +166,12 @@ namespace Microsoft.AspNet.Razor.Parser
prev = node;
}
// We want to surface both the parsing and rewriting errors as one unified list of errors because
// both parsing and rewriting errors affect the end users Razor page.
var errors = results.ParserErrors.Concat(rewritingContext.Errors).ToList();
// Return the new result
return new ParserResults(current, results.ParserErrors);
return new ParserResults(syntaxTree, errors);
}
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
namespace Microsoft.AspNet.Razor.Parser
{
/// <summary>
/// Informational class for rewriting a syntax tree.
/// </summary>
public class RewritingContext
{
private readonly List<RazorError> _errors;
/// <summary>
/// Instantiates a new <see cref="RewritingContext"/>.
/// </summary>
public RewritingContext(Block syntaxTree)
{
_errors = new List<RazorError>();
SyntaxTree = syntaxTree;
}
/// <summary>
/// The documents syntax tree.
/// </summary>
public Block SyntaxTree { get; set; }
/// <summary>
/// <see cref="RazorError"/>s collected.
/// </summary>
public IEnumerable<RazorError> Errors
{
get
{
return _errors;
}
}
/// <summary>
/// Creates and tracks a new <see cref="RazorError"/>.
/// </summary>
/// <param name="location"><see cref="SourceLocation"/> of the error.</param>
/// <param name="message">A message describing the error.</param>
public void OnError(SourceLocation location, string message)
{
_errors.Add(new RazorError(message, location));
}
}
}

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
using Microsoft.Internal.Web.Utils;
namespace Microsoft.AspNet.Razor.Parser.TagHelpers
@ -16,6 +17,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// </summary>
public class TagHelperBlock : Block, IEquatable<TagHelperBlock>
{
private readonly SourceLocation _start;
/// <summary>
/// Instantiates a new instance of a <see cref="TagHelperBlock"/>.
/// </summary>
@ -26,6 +29,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
{
TagName = source.TagName;
Attributes = new Dictionary<string, SyntaxTreeNode>(source.Attributes);
_start = source.Start;
source.Reset();
@ -40,6 +44,15 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// </summary>
public IDictionary<string, SyntaxTreeNode> Attributes { get; private set; }
/// <inheritdoc />
public override SourceLocation Start
{
get
{
return _start;
}
}
/// <summary>
/// The HTML tag name.
/// </summary>

View File

@ -7,6 +7,7 @@ using System.Linq;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Text;
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
namespace Microsoft.AspNet.Razor.Parser.TagHelpers
@ -44,6 +45,9 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
CodeGenerator = new TagHelperCodeGenerator(descriptors);
Type = startTag.Type;
Attributes = GetTagAttributes(startTag);
// There will always be at least one child for the '<'.
Start = startTag.Children.First().Start;
}
// Internal for testing
@ -98,6 +102,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
base.Reset();
}
/// <summary>
/// The starting <see cref="SourceLocation"/> of the tag helper.
/// </summary>
public SourceLocation Start { get; private set; }
private static IDictionary<string, SyntaxTreeNode> GetTagAttributes(Block tagBlock)
{
var attributes = new Dictionary<string, SyntaxTreeNode>(StringComparer.OrdinalIgnoreCase);

View File

@ -25,13 +25,13 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
_blockStack = new Stack<BlockBuilder>();
}
public Block Rewrite(Block input)
public void Rewrite(RewritingContext context)
{
RewriteTags(input);
RewriteTags(context.SyntaxTree);
Debug.Assert(_blockStack.Count == 0);
ValidateRewrittenSyntaxTree(context);
return _currentBlock.Build();
context.SyntaxTree = _currentBlock.Build();
}
private void RewriteTags(Block input)
@ -122,6 +122,37 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal
BuildCurrentlyTrackedBlock();
}
private void ValidateRewrittenSyntaxTree(RewritingContext context)
{
// If the blockStack still has elements in it that means there's at least one malformed TagHelper block in
// the document, that's the only way we can have a non-zero _blockStack.Count.
if (_blockStack.Count != 0)
{
// We reverse the children so we can search from the back to the front for the TagHelper that is
// malformed.
var candidateChildren = _currentBlock.Children.Reverse();
var malformedTagHelper = candidateChildren.OfType<TagHelperBlock>().FirstOrDefault();
// If the malformed tag helper is null that means something other than a TagHelper caused the
// unbalancing of the syntax tree (should never happen).
Debug.Assert(malformedTagHelper != null);
// We only create a single error because we can't reasonably determine other invalid tag helpers in the
// document; having one malformed tag helper puts the document into an invalid state.
context.OnError(
malformedTagHelper.Start,
RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(
malformedTagHelper.TagName));
// We need to build the remaining blocks in the stack to ensure we don't return an invalid syntax tree.
do
{
BuildCurrentlyTrackedTagHelperBlock();
}
while (_blockStack.Count != 0);
}
}
private void BuildCurrentlyTrackedBlock()
{
// Going to remove the current BlockBuilder from the stack because it's complete.

View File

@ -1542,6 +1542,22 @@ namespace Microsoft.AspNet.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotUseDirectiveWithNoTagHelperDescriptorResolver"), p0, p1, p2);
}
/// <summary>
/// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
/// </summary>
internal static string TagHelpersParseTreeRewriter_FoundMalformedTagHelper
{
get { return GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"); }
}
/// <summary>
/// Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.
/// </summary>
internal static string FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpersParseTreeRewriter_FoundMalformedTagHelper"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -424,4 +424,7 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TagHelpers_CannotUseDirectiveWithNoTagHelperDescriptorResolver" xml:space="preserve">
<value>Cannot use directive '{0}' when a {1} has not been provided to the {2}.</value>
</data>
<data name="TagHelpersParseTreeRewriter_FoundMalformedTagHelper" xml:space="preserve">
<value>Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing.</value>
</data>
</root>

View File

@ -185,8 +185,10 @@ namespace Microsoft.AspNet.Razor.Test.Parser.Html
{
// Act
ParserResults results = ParseDocument("<a href='~/Foo/Bar' />");
Block rewritten = new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(results.Document);
rewritten = new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritten);
var rewritingContext = new RewritingContext(results.Document);
new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritingContext);
new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritingContext);
var rewritten = rewritingContext.SyntaxTree;
// Assert
Assert.Equal(0, results.ParserErrors.Count);
@ -270,8 +272,10 @@ namespace Microsoft.AspNet.Razor.Test.Parser.Html
// Act
ParserResults results = ParseDocument(code);
Block rewritten = new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(results.Document);
rewritten = new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritten);
var rewritingContext = new RewritingContext(results.Document);
new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritingContext);
new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritingContext);
var rewritten = rewritingContext.SyntaxTree;
// Assert
Assert.Equal(0, results.ParserErrors.Count);

View File

@ -32,14 +32,15 @@ namespace Microsoft.AspNet.Razor.Test.Parser
factory.Markup("test")
);
WhiteSpaceRewriter rewriter = new WhiteSpaceRewriter(new HtmlMarkupParser().BuildSpan);
var rewritingContext = new RewritingContext(start);
// Act
Block actual = rewriter.Rewrite(start);
rewriter.Rewrite(rewritingContext);
factory.Reset();
// Assert
ParserTestBase.EvaluateParseTree(actual, new MarkupBlock(
ParserTestBase.EvaluateParseTree(rewritingContext.SyntaxTree, new MarkupBlock(
factory.Markup("test"),
factory.Markup(" "),
new ExpressionBlock(

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
@ -16,6 +18,90 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : CsHtmlMarkupParserTestBase
{
public static IEnumerable<object[]> IncompleteHelperBlockData
{
get
{
var factory = CreateDefaultSpanFactory();
var blockFactory = new BlockFactory(factory);
var errorFormat = "Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or " +
"be self closing.";
var dateTimeNow = new MarkupBlock(
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)));
yield return new object[] {
"<p class=foo dynamic=@DateTime.Now style=color:red;><strong></p></strong>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) },
{ "dynamic", new MarkupBlock(dateTimeNow) },
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
},
new MarkupTagHelperBlock("strong",
blockFactory.MarkupTagBlock("</p>")))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"),
SourceLocation.Zero)
};
yield return new object[] {
"<div><p>Hello <strong>World</strong></div>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<div>"),
new MarkupTagHelperBlock("p",
factory.Markup("Hello "),
new MarkupTagHelperBlock("strong",
factory.Markup("World")),
blockFactory.MarkupTagBlock("</div>"))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"),
absoluteIndex: 5, lineIndex: 0, columnIndex: 5)
};
yield return new object[] {
"<div><p>Hello <strong>World</div>",
new MarkupBlock(
blockFactory.MarkupTagBlock("<div>"),
new MarkupTagHelperBlock("p",
factory.Markup("Hello "),
new MarkupTagHelperBlock("strong",
factory.Markup("World"),
blockFactory.MarkupTagBlock("</div>")))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "strong"),
absoluteIndex: 14, lineIndex: 0, columnIndex: 14)
};
yield return new object[] {
"<p class=\"foo\">Hello <p style=\"color:red;\">World</p>",
new MarkupBlock(
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "class", new MarkupBlock(factory.Markup("foo")) }
},
factory.Markup("Hello "),
new MarkupTagHelperBlock("p",
new Dictionary<string, SyntaxTreeNode>
{
{ "style", new MarkupBlock(factory.Markup("color:red;")) }
},
factory.Markup("World")))),
new RazorError(string.Format(CultureInfo.InvariantCulture, errorFormat, "p"),
SourceLocation.Zero)
};
}
}
[Theory]
[MemberData(nameof(IncompleteHelperBlockData))]
public void TagHelperParseTreeRewriter_CreatesErrorForIncompleteTagHelper(
string documentContent,
MarkupBlock expectedOutput,
RazorError expectedError)
{
RunParseTreeRewriterTest(documentContent, expectedOutput, new[] { expectedError }, "strong", "p");
}
public static IEnumerable<object[]> TextTagsBlockData
{
get
@ -938,14 +1024,25 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
}
private void RunParseTreeRewriterTest(string documentContent,
MarkupBlock expectedOutput,
params string[] tagNames)
MarkupBlock expectedOutput,
params string[] tagNames)
{
RunParseTreeRewriterTest(documentContent,
expectedOutput,
errors: Enumerable.Empty<RazorError>(),
tagNames: tagNames);
}
private void RunParseTreeRewriterTest(string documentContent,
MarkupBlock expectedOutput,
IEnumerable<RazorError> errors,
params string[] tagNames)
{
// Arrange
var providerContext = BuildProviderContext(tagNames);
// Act & Assert
EvaluateData(providerContext, documentContent, expectedOutput);
EvaluateData(providerContext, documentContent, expectedOutput, errors);
}
private TagHelperDescriptorProvider BuildProviderContext(params string[] tagNames)
@ -961,12 +1058,20 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
return new TagHelperDescriptorProvider(descriptors);
}
private void EvaluateData(TagHelperDescriptorProvider provider, string documentContent, MarkupBlock expectedOutput)
private void EvaluateData(TagHelperDescriptorProvider provider,
string documentContent,
MarkupBlock expectedOutput,
IEnumerable<RazorError> expectedErrors)
{
var results = ParseDocument(documentContent);
var rewritten = new TagHelperParseTreeRewriter(provider).Rewrite(results.Document);
var rewritingContext = new RewritingContext(results.Document);
new TagHelperParseTreeRewriter(provider).Rewrite(rewritingContext);
var rewritten = rewritingContext.SyntaxTree;
Assert.Empty(results.ParserErrors);
// Combine the parser errors and the rewriter errors. Normally the RazorParser does this.
var errors = results.ParserErrors.Concat(rewritingContext.Errors).ToList();
EvaluateRazorErrors(errors, expectedErrors.ToList());
EvaluateParseTree(rewritten, expectedOutput);
}