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()
{