Fix #1399 - crash on start-end syntax for void element
We weren't correctly recovering when a void element is written as a start-end pair. This change cleans up some of the plumbing around end-tag handling and adds recognition for this case. Added a new bespoke diagnostic for the void element case.
This commit is contained in:
parent
f601b6d3d2
commit
4f51d90157
|
|
@ -61,6 +61,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
return RazorDiagnostic.Create(MismatchedClosingTag, span ?? SourceSpan.Undefined, expectedTagName, tagName);
|
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(
|
public static readonly RazorDiagnosticDescriptor InvalidHtmlContent = new RazorDiagnosticDescriptor(
|
||||||
"BL9984",
|
"BL9984",
|
||||||
() => "Found invalid HTML content. Text '{0}'",
|
() => "Found invalid HTML content. Text '{0}'",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
{
|
{
|
||||||
// Per the HTML spec, the following elements are inherently self-closing
|
// Per the HTML spec, the following elements are inherently self-closing
|
||||||
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
|
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
|
||||||
public readonly static HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
public static readonly HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr",
|
"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.StartTag:
|
||||||
case HtmlTokenType.EndTag:
|
|
||||||
{
|
{
|
||||||
var tag = token.AsTag();
|
var tag = token.AsTag();
|
||||||
|
|
||||||
|
|
@ -218,48 +217,55 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
stack.Pop();
|
stack.Pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.Type == HtmlTokenType.EndTag)
|
break;
|
||||||
{
|
}
|
||||||
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(token.AsTag());
|
case HtmlTokenType.EndTag:
|
||||||
var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex);
|
{
|
||||||
var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(span, tagName);
|
var tag = token.AsTag();
|
||||||
popped.Children.Add(new HtmlElementIntermediateNode()
|
|
||||||
|
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,
|
||||||
{
|
},
|
||||||
diagnostic,
|
TagName = tagName,
|
||||||
},
|
Source = span,
|
||||||
TagName = tagName,
|
});
|
||||||
Source = span,
|
}
|
||||||
});
|
else if (!string.Equals(tag.Name, ((HtmlElementIntermediateNode)popped).TagName, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
{
|
||||||
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);
|
||||||
var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex);
|
popped.Diagnostics.Add(diagnostic);
|
||||||
var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(span, ((HtmlElementIntermediateNode)popped).TagName, token.Data);
|
}
|
||||||
popped.Diagnostics.Add(diagnostic);
|
else
|
||||||
}
|
{
|
||||||
else
|
// Happy path.
|
||||||
{
|
//
|
||||||
// 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.
|
||||||
// We need to compute a new source span because when we found the start tag before we knew
|
var length = end.AbsoluteIndex - popped.Source.Value.AbsoluteIndex;
|
||||||
// the end poosition of the tag.
|
popped.Source = new SourceSpan(
|
||||||
var length = end.AbsoluteIndex - popped.Source.Value.AbsoluteIndex;
|
popped.Source.Value.FilePath,
|
||||||
popped.Source = new SourceSpan(
|
popped.Source.Value.AbsoluteIndex,
|
||||||
popped.Source.Value.FilePath,
|
popped.Source.Value.LineIndex,
|
||||||
popped.Source.Value.AbsoluteIndex,
|
popped.Source.Value.CharacterIndex,
|
||||||
popped.Source.Value.LineIndex,
|
length);
|
||||||
popped.Source.Value.CharacterIndex,
|
|
||||||
length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,36 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
c => NodeAssert.Content(c, "Hello, World!"));
|
c => NodeAssert.Content(c, "Hello, World!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Execute_RewritesHtml_UnbalancedClosing_MisuseOfVoidElement()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var document = CreateDocument(@"<input></input>");
|
||||||
|
|
||||||
|
var documentNode = Lower(document);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Pass.Execute(document, documentNode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var method = documentNode.FindPrimaryMethod();
|
||||||
|
Assert.Collection(
|
||||||
|
method.Children,
|
||||||
|
c => Assert.IsType<CSharpCodeIntermediateNode>(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]
|
[Fact]
|
||||||
public void Execute_RewritesHtml_UnbalancedClosingTagAtTopLevel()
|
public void Execute_RewritesHtml_UnbalancedClosingTagAtTopLevel()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue