diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs index 563a1edebe..d08afb663a 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorDiagnosticFactory.cs @@ -61,6 +61,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor return RazorDiagnostic.Create(MismatchedClosingTag, span ?? SourceSpan.Undefined, expectedTagName, tagName); } + public static readonly RazorDiagnosticDescriptor UnexpectedClosingTagForVoidElement = new RazorDiagnosticDescriptor( + "BL9983", + () => "Unexpected closing tag '{0}'. The element '{0}' is a void element, and should be used without a closing tag.", + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic Create_UnexpectedClosingTagForVoidElement(SourceSpan? span, string tagName) + { + return RazorDiagnostic.Create(UnexpectedClosingTagForVoidElement, span ?? SourceSpan.Undefined, tagName); + } + public static readonly RazorDiagnosticDescriptor InvalidHtmlContent = new RazorDiagnosticDescriptor( "BL9984", () => "Found invalid HTML content. Text '{0}'", diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs index 1d35f15232..65f8ff77d9 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor { // Per the HTML spec, the following elements are inherently self-closing // For example, is the same as (and therefore it cannot contain descendants) - public readonly static HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + public static readonly HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", }; @@ -182,7 +182,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor } case HtmlTokenType.StartTag: - case HtmlTokenType.EndTag: { var tag = token.AsTag(); @@ -218,48 +217,55 @@ namespace Microsoft.AspNetCore.Blazor.Razor stack.Pop(); } - if (token.Type == HtmlTokenType.EndTag) - { - var popped = stack.Pop(); - if (stack.Count == 0) - { - // If we managed to 'bottom out' the stack then we have an unbalanced end tag. - // Put back the current node so we don't crash. - stack.Push(popped); + break; + } - var tagName = parser.GetTagNameOriginalCasing(token.AsTag()); - var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); - var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(span, tagName); - popped.Children.Add(new HtmlElementIntermediateNode() + case HtmlTokenType.EndTag: + { + var tag = token.AsTag(); + + var popped = stack.Pop(); + if (stack.Count == 0) + { + // If we managed to 'bottom out' the stack then we have an unbalanced end tag. + // Put back the current node so we don't crash. + stack.Push(popped); + + var tagName = parser.GetTagNameOriginalCasing(tag); + var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); + + var diagnostic = VoidElements.Contains(tagName) + ? BlazorDiagnosticFactory.Create_UnexpectedClosingTagForVoidElement(span, tagName) + : BlazorDiagnosticFactory.Create_UnexpectedClosingTag(span, tagName); + popped.Children.Add(new HtmlElementIntermediateNode() + { + Diagnostics = { - Diagnostics = - { - diagnostic, - }, - TagName = tagName, - Source = span, - }); - } - else if (!string.Equals(tag.Name, ((HtmlElementIntermediateNode)popped).TagName, StringComparison.OrdinalIgnoreCase)) - { - var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); - var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(span, ((HtmlElementIntermediateNode)popped).TagName, token.Data); - popped.Diagnostics.Add(diagnostic); - } - else - { - // Happy path. - // - // We need to compute a new source span because when we found the start tag before we knew - // the end poosition of the tag. - var length = end.AbsoluteIndex - popped.Source.Value.AbsoluteIndex; - popped.Source = new SourceSpan( - popped.Source.Value.FilePath, - popped.Source.Value.AbsoluteIndex, - popped.Source.Value.LineIndex, - popped.Source.Value.CharacterIndex, - length); - } + diagnostic, + }, + TagName = tagName, + Source = span, + }); + } + else if (!string.Equals(tag.Name, ((HtmlElementIntermediateNode)popped).TagName, StringComparison.OrdinalIgnoreCase)) + { + var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); + var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(span, ((HtmlElementIntermediateNode)popped).TagName, token.Data); + popped.Diagnostics.Add(diagnostic); + } + else + { + // Happy path. + // + // We need to compute a new source span because when we found the start tag before we knew + // the end poosition of the tag. + var length = end.AbsoluteIndex - popped.Source.Value.AbsoluteIndex; + popped.Source = new SourceSpan( + popped.Source.Value.FilePath, + popped.Source.Value.AbsoluteIndex, + popped.Source.Value.LineIndex, + popped.Source.Value.CharacterIndex, + length); } break; diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/DiagnosticRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/DiagnosticRazorIntegrationTest.cs index e601f8c65e..6cfbf74277 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/DiagnosticRazorIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/DiagnosticRazorIntegrationTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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 Xunit; diff --git a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs index 6faab2841e..b18af937dd 100644 --- a/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Razor.Extensions.Test/ComponentDocumentRewritePassTest.cs @@ -255,6 +255,36 @@ namespace Microsoft.AspNetCore.Blazor.Razor c => NodeAssert.Content(c, "Hello, World!")); } + [Fact] + public void Execute_RewritesHtml_UnbalancedClosing_MisuseOfVoidElement() + { + // Arrange + var document = CreateDocument(@""); + + var documentNode = Lower(document); + + // Act + Pass.Execute(document, documentNode); + + // Assert + var method = documentNode.FindPrimaryMethod(); + Assert.Collection( + method.Children, + c => Assert.IsType(c), + c => NodeAssert.Element(c, "input"), + c => NodeAssert.Element(c, "input")); + + var input2 = NodeAssert.Element(method.Children[2], "input"); + Assert.Equal(7, input2.Source.Value.AbsoluteIndex); + Assert.Equal(0, input2.Source.Value.LineIndex); + Assert.Equal(7, input2.Source.Value.CharacterIndex); + Assert.Equal(8, input2.Source.Value.Length); + + var diagnostic = Assert.Single(input2.Diagnostics); + Assert.Same(BlazorDiagnosticFactory.UnexpectedClosingTagForVoidElement.Id, diagnostic.Id); + Assert.Equal(input2.Source, diagnostic.Span); + } + [Fact] public void Execute_RewritesHtml_UnbalancedClosingTagAtTopLevel() {