Make TagHelper & Razor comments locatable at design time.

- Added a step to full fidelity verification that does a `LocateOwner` at every source location. If we find a `null` owner we build a snippet with a detailed response to enable us to diagnose the problem.

Addresses aspnet/AspNetCore#7718
This commit is contained in:
N. Taylor Mullen 2019-02-19 12:06:57 -08:00
Родитель 49f7a26e36
Коммит 08587a30fd
4 изменённых файлов: 179 добавлений и 1 удалений

Просмотреть файл

@ -23,6 +23,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private static readonly SyntaxKind[] CommentSpanKinds = new SyntaxKind[]
{
SyntaxKind.RazorCommentTransition,
SyntaxKind.RazorCommentStar,
SyntaxKind.RazorCommentLiteral,
};
@ -100,7 +102,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
SyntaxNode owner = null;
IEnumerable<SyntaxNode> children = null;
IEnumerable<SyntaxNode> children;
if (node is MarkupStartTagSyntax startTag)
{
children = startTag.Children;
@ -109,6 +111,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
children = endTag.Children;
}
else if (node is MarkupTagHelperStartTagSyntax startTagHelper)
{
children = startTagHelper.Children;
}
else if (node is MarkupTagHelperEndTagSyntax endTagHelper)
{
children = endTagHelper.Children;
}
else
{
children = node.ChildNodes();

Просмотреть файл

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language.Syntax
{
internal partial class MarkupTagHelperEndTagSyntax
{
// Copied directly from MarkupEndTagSyntax Children & GetLegacyChildren.
public SyntaxList<RazorSyntaxNode> Children => GetLegacyChildren();
private SyntaxList<RazorSyntaxNode> GetLegacyChildren()
{
// This method returns the children of this end tag in legacy format.
// This is needed to generate the same classified spans as the legacy syntax tree.
var builder = new SyntaxListBuilder(3);
var tokens = SyntaxListBuilder<SyntaxToken>.Create();
var context = this.GetSpanContext();
if (!OpenAngle.IsMissing)
{
tokens.Add(OpenAngle);
}
if (!ForwardSlash.IsMissing)
{
tokens.Add(ForwardSlash);
}
if (Bang != null)
{
// The prefix of an end tag(E.g '|</|!foo>') will have 'Any' accepted characters if a bang exists.
var acceptsAnyContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault());
acceptsAnyContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(acceptsAnyContext));
tokens.Add(Bang);
var acceptsNoneContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault());
acceptsNoneContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
builder.Add(SyntaxFactory.RazorMetaCode(tokens.Consume()).WithSpanContext(acceptsNoneContext));
}
if (!Name.IsMissing)
{
tokens.Add(Name);
}
if (MiscAttributeContent?.Children != null && MiscAttributeContent.Children.Count > 0)
{
foreach (var content in MiscAttributeContent.Children)
{
tokens.AddRange(((MarkupTextLiteralSyntax)content).LiteralTokens);
}
}
if (!CloseAngle.IsMissing)
{
tokens.Add(CloseAngle);
}
builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(context));
return new SyntaxList<RazorSyntaxNode>(builder.ToListNode().CreateRed(this, Position));
}
}
}

Просмотреть файл

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.AspNetCore.Razor.Language.Syntax
{
internal partial class MarkupTagHelperStartTagSyntax
{
// Copied directly from MarkupStartTagSyntax Children & GetLegacyChildren.
public SyntaxList<RazorSyntaxNode> Children => GetLegacyChildren();
private SyntaxList<RazorSyntaxNode> GetLegacyChildren()
{
// This method returns the children of this start tag in legacy format.
// This is needed to generate the same classified spans as the legacy syntax tree.
var builder = new SyntaxListBuilder(5);
var tokens = SyntaxListBuilder<SyntaxToken>.Create();
var context = this.GetSpanContext();
// We want to know if this tag contains non-whitespace attribute content to set the appropriate AcceptedCharacters.
// The prefix of a start tag(E.g '|<foo| attr>') will have 'Any' accepted characters if non-whitespace attribute content exists.
var acceptsAnyContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault());
acceptsAnyContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any;
var containsAttributesContent = false;
foreach (var attribute in Attributes)
{
if (!string.IsNullOrWhiteSpace(attribute.GetContent()))
{
containsAttributesContent = true;
break;
}
}
if (!OpenAngle.IsMissing)
{
tokens.Add(OpenAngle);
}
if (Bang != null)
{
builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(acceptsAnyContext));
tokens.Add(Bang);
var acceptsNoneContext = new SpanContext(context.ChunkGenerator, SpanEditHandler.CreateDefault());
acceptsNoneContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
builder.Add(SyntaxFactory.RazorMetaCode(tokens.Consume()).WithSpanContext(acceptsNoneContext));
}
if (!Name.IsMissing)
{
tokens.Add(Name);
}
builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(containsAttributesContent ? acceptsAnyContext : context));
builder.AddRange(Attributes);
if (ForwardSlash != null)
{
tokens.Add(ForwardSlash);
}
if (!CloseAngle.IsMissing)
{
tokens.Add(CloseAngle);
}
if (tokens.Count > 0)
{
builder.Add(SyntaxFactory.MarkupTextLiteral(tokens.Consume()).WithSpanContext(context));
}
return new SyntaxList<RazorSyntaxNode>(builder.ToListNode().CreateRed(this, Position));
}
}
}

Просмотреть файл

@ -6,6 +6,7 @@ using System.Text;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Razor.Language
{
@ -29,6 +30,38 @@ namespace Microsoft.AspNetCore.Razor.Language
// Make sure the syntax tree contains all of the text in the document.
Assert.Equal(sourceString, syntaxTreeString);
// Ensure all source is locatable
for (var i = 0; i < syntaxTree.Source.Length; i++)
{
var span = new SourceSpan(i, 0);
var location = new SourceChange(span, string.Empty);
var owner = syntaxTree.Root.LocateOwner(location);
if (owner == null)
{
var snippetStartIndex = Math.Max(0, i - 10);
var snippetStartLength = i - snippetStartIndex;
var snippetStart = new char[snippetStartLength];
syntaxTree.Source.CopyTo(snippetStartIndex, snippetStart, 0, snippetStartLength);
var snippetEndIndex = Math.Min(syntaxTree.Source.Length - 1, i + 10);
var snippetEndLength = snippetEndIndex - i;
var snippetEnd = new char[snippetEndLength];
syntaxTree.Source.CopyTo(i, snippetEnd, 0, snippetEndLength);
var snippet = new char[snippetStart.Length + snippetEnd.Length + 1];
snippetStart.CopyTo(snippet, 0);
snippet[snippetStart.Length] = '|';
snippetEnd.CopyTo(snippet, snippetStart.Length + 1);
var snippetString = new string(snippet);
throw new XunitException(
$@"Could not locate Syntax Node owner at position '{i}':
{snippetString}");
}
}
}
}