From 1243e07e5659f317309a70c7a6758cab5e72a126 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 19 Feb 2019 12:06:57 -0800 Subject: [PATCH] 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/AspNetCoredotnet/aspnetcore-tooling#7718 \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/08587a30fd75f5392ece1a56ee23d36a22a8bc66 --- .../src/Legacy/LegacySyntaxNodeExtensions.cs | 12 ++- .../src/Syntax/MarkupTagHelperEndTagSyntax.cs | 60 +++++++++++++++ .../Syntax/MarkupTagHelperStartTagSyntax.cs | 75 +++++++++++++++++++ .../Language/SyntaxTreeVerifier.cs | 33 ++++++++ 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperEndTagSyntax.cs create mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperStartTagSyntax.cs diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LegacySyntaxNodeExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LegacySyntaxNodeExtensions.cs index 9949ce8a62..bdfbb27c43 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LegacySyntaxNodeExtensions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LegacySyntaxNodeExtensions.cs @@ -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 children = null; + IEnumerable 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(); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperEndTagSyntax.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperEndTagSyntax.cs new file mode 100644 index 0000000000..2536f5ae32 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperEndTagSyntax.cs @@ -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 Children => GetLegacyChildren(); + + private SyntaxList 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.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 '|') 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(builder.ToListNode().CreateRed(this, Position)); + } + } +} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperStartTagSyntax.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperStartTagSyntax.cs new file mode 100644 index 0000000000..bebec4f425 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/MarkupTagHelperStartTagSyntax.cs @@ -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 Children => GetLegacyChildren(); + + private SyntaxList 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.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 '|') 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(builder.ToListNode().CreateRed(this, Position)); + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs index f4f416616c..e92858cc9a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs @@ -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}"); + } + } } }