In RazorCompiler, reject malformed documents with mismatching tags
This commit is contained in:
parent
95023c0300
commit
0665d30e19
|
|
@ -2,6 +2,7 @@
|
||||||
// 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 Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Razor;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" },
|
new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" },
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private readonly ScopeStack _scopeStack = new ScopeStack();
|
||||||
private string _unconsumedHtml;
|
private string _unconsumedHtml;
|
||||||
private IList<object> _currentAttributeValues;
|
private IList<object> _currentAttributeValues;
|
||||||
private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
|
private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
|
||||||
|
|
@ -75,6 +76,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
{
|
{
|
||||||
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
|
||||||
{
|
{
|
||||||
|
_scopeStack.IncrementCurrentScopeChildCount();
|
||||||
context.CodeWriter.Write(token.Content);
|
context.CodeWriter.Write(token.Content);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -118,6 +120,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|
|
||||||
// Since we're not in the middle of writing an element, this must evaluate as some
|
// Since we're not in the middle of writing an element, this must evaluate as some
|
||||||
// text to display
|
// text to display
|
||||||
|
_scopeStack.IncrementCurrentScopeChildCount();
|
||||||
context.CodeWriter
|
context.CodeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
|
|
@ -207,6 +210,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
case HtmlTokenType.Character:
|
case HtmlTokenType.Character:
|
||||||
{
|
{
|
||||||
// Text node
|
// Text node
|
||||||
|
_scopeStack.IncrementCurrentScopeChildCount();
|
||||||
codeWriter
|
codeWriter
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddContent)}")
|
||||||
.Write((_sourceSequence++).ToString())
|
.Write((_sourceSequence++).ToString())
|
||||||
|
|
@ -225,6 +229,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|
|
||||||
if (nextToken.Type == HtmlTokenType.StartTag)
|
if (nextToken.Type == HtmlTokenType.StartTag)
|
||||||
{
|
{
|
||||||
|
_scopeStack.IncrementCurrentScopeChildCount();
|
||||||
if (isComponent)
|
if (isComponent)
|
||||||
{
|
{
|
||||||
codeWriter
|
codeWriter
|
||||||
|
|
@ -241,40 +246,47 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
.WriteStringLiteral(nextTag.Data)
|
.WriteStringLiteral(nextTag.Data)
|
||||||
.WriteEndMethodInvocation();
|
.WriteEndMethodInvocation();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attribute in nextTag.Attributes)
|
foreach (var attribute in nextTag.Attributes)
|
||||||
{
|
|
||||||
WriteAttribute(codeWriter, attribute.Key, attribute.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_currentElementAttributes.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in _currentElementAttributes)
|
|
||||||
{
|
{
|
||||||
WriteAttribute(codeWriter, pair.Key, pair.Value.AttributeValue);
|
WriteAttribute(codeWriter, attribute.Key, attribute.Value);
|
||||||
}
|
}
|
||||||
_currentElementAttributes.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_currentElementAttributeTokens.Count > 0)
|
if (_currentElementAttributes.Count > 0)
|
||||||
{
|
|
||||||
foreach (var token in _currentElementAttributeTokens)
|
|
||||||
{
|
{
|
||||||
codeWriter
|
foreach (var pair in _currentElementAttributes)
|
||||||
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
{
|
||||||
.Write((_sourceSequence++).ToString())
|
WriteAttribute(codeWriter, pair.Key, pair.Value.AttributeValue);
|
||||||
.WriteParameterSeparator()
|
}
|
||||||
.Write(token.AttributeValue.Content)
|
_currentElementAttributes.Clear();
|
||||||
.WriteEndMethodInvocation();
|
|
||||||
}
|
}
|
||||||
_currentElementAttributeTokens.Clear();
|
|
||||||
|
if (_currentElementAttributeTokens.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var token in _currentElementAttributeTokens)
|
||||||
|
{
|
||||||
|
codeWriter
|
||||||
|
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
|
||||||
|
.Write((_sourceSequence++).ToString())
|
||||||
|
.WriteParameterSeparator()
|
||||||
|
.Write(token.AttributeValue.Content)
|
||||||
|
.WriteEndMethodInvocation();
|
||||||
|
}
|
||||||
|
_currentElementAttributeTokens.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_scopeStack.OpenScope(
|
||||||
|
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
|
||||||
|
isComponent: isComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextToken.Type == HtmlTokenType.EndTag
|
if (nextToken.Type == HtmlTokenType.EndTag
|
||||||
|| nextTag.IsSelfClosing
|
|| nextTag.IsSelfClosing
|
||||||
|| htmlVoidElementsLookup.Contains(nextTag.Data))
|
|| (!isComponent && htmlVoidElementsLookup.Contains(nextTag.Data)))
|
||||||
{
|
{
|
||||||
|
_scopeStack.CloseScope(
|
||||||
|
tagName: isComponent ? tagNameOriginalCase : nextTag.Data,
|
||||||
|
isComponent: isComponent);
|
||||||
var closeMethodName = isComponent
|
var closeMethodName = isComponent
|
||||||
? nameof(RenderTreeBuilder.CloseComponent)
|
? nameof(RenderTreeBuilder.CloseComponent)
|
||||||
: nameof(RenderTreeBuilder.CloseElement);
|
: nameof(RenderTreeBuilder.CloseElement);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// 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.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
|
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
{
|
{
|
||||||
public class RazorCompilerDiagnostic
|
public class RazorCompilerDiagnostic
|
||||||
{
|
{
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
|
||||||
public int Column { get; }
|
public int Column { get; }
|
||||||
public string Message { get; }
|
public string Message { get; }
|
||||||
|
|
||||||
internal RazorCompilerDiagnostic(
|
public RazorCompilerDiagnostic(
|
||||||
DiagnosticType type,
|
DiagnosticType type,
|
||||||
string sourceFilePath,
|
string sourceFilePath,
|
||||||
int line,
|
int line,
|
||||||
|
|
@ -3,15 +3,15 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
|
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a fatal error during the transformation of a Blazor component from
|
/// Represents a fatal error during the transformation of a Blazor component from
|
||||||
/// Razor source code to C# source code.
|
/// Razor source code to C# source code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class RazorCompilerException : Exception
|
public class RazorCompilerException : Exception
|
||||||
{
|
{
|
||||||
public RazorCompilerException(string message): base(message)
|
public RazorCompilerException(string message) : base(message)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps track of the nesting of elements/containers while writing out the C# source code
|
||||||
|
/// for a component. This allows us to detect mismatched start/end tags, as well as inject
|
||||||
|
/// additional C# source to capture component descendants in a lambda.
|
||||||
|
/// </summary>
|
||||||
|
internal class ScopeStack
|
||||||
|
{
|
||||||
|
private readonly Stack<ScopeEntry> _stack = new Stack<ScopeEntry>();
|
||||||
|
|
||||||
|
public void OpenScope(string tagName, bool isComponent)
|
||||||
|
{
|
||||||
|
_stack.Push(new ScopeEntry(tagName, isComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseScope(string tagName, bool isComponent)
|
||||||
|
{
|
||||||
|
if (_stack.Count == 0)
|
||||||
|
{
|
||||||
|
throw new RazorCompilerException(
|
||||||
|
$"Unexpected closing tag '{tagName}' with no matching start tag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = _stack.Pop();
|
||||||
|
if (!tagName.Equals(expected.TagName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new RazorCompilerException(
|
||||||
|
$"Mismatching closing tag. Found '{tagName}' but expected '{expected.TagName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: there's no unit test to cover the following, because there's no known way of
|
||||||
|
// triggering it from user code (i.e., Razor source code). But the test is here anyway
|
||||||
|
// just in case one day it turns out there is some way of causing this error.
|
||||||
|
if (isComponent != expected.IsComponent)
|
||||||
|
{
|
||||||
|
throw new RazorCompilerException(
|
||||||
|
$"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(expected.IsComponent ? "component" : "element")}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IncrementCurrentScopeChildCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScopeEntry
|
||||||
|
{
|
||||||
|
public readonly string TagName;
|
||||||
|
public readonly bool IsComponent;
|
||||||
|
public int ChildCount;
|
||||||
|
|
||||||
|
public ScopeEntry(string tagName, bool isComponent)
|
||||||
|
{
|
||||||
|
TagName = tagName;
|
||||||
|
IsComponent = isComponent;
|
||||||
|
ChildCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
||||||
using Microsoft.AspNetCore.Blazor.Components;
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
using Microsoft.AspNetCore.Blazor.Layouts;
|
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Razor;
|
||||||
using Microsoft.AspNetCore.Blazor.Rendering;
|
using Microsoft.AspNetCore.Blazor.Rendering;
|
||||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||||
|
|
@ -503,6 +504,38 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
diagnostic => Assert.Contains("'invalidVar'", diagnostic.GetMessage()));
|
diagnostic => Assert.Contains("'invalidVar'", diagnostic.GetMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsEndTagWithNoStartTag()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var result = CompileToCSharp(
|
||||||
|
"Line1\nLine2\nLine3</mytag>");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(result.Diagnostics,
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type);
|
||||||
|
Assert.StartsWith("Unexpected closing tag 'mytag' with no matching start tag.", item.Message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsEndTagWithDifferentNameToStartTag()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var result = CompileToCSharp(
|
||||||
|
"<root><other />text<child>more text</root></child>");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(result.Diagnostics,
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type);
|
||||||
|
Assert.StartsWith("Mismatching closing tag. Found 'root' but expected 'child'.", item.Message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static RenderTreeFrame[] GetRenderTree(IComponent component)
|
private static RenderTreeFrame[] GetRenderTree(IComponent component)
|
||||||
{
|
{
|
||||||
var renderer = new TestRenderer();
|
var renderer = new TestRenderer();
|
||||||
|
|
@ -575,6 +608,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CompileToCSharpResult CompileToCSharp(string cshtmlContent)
|
||||||
|
=> CompileToCSharp(
|
||||||
|
GetArbitraryPlatformValidDirectoryPath(),
|
||||||
|
"test.cshtml",
|
||||||
|
cshtmlContent,
|
||||||
|
"TestNamespace");
|
||||||
|
|
||||||
private static CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace)
|
private static CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent, string outputNamespace)
|
||||||
{
|
{
|
||||||
using (var resultStream = new MemoryStream())
|
using (var resultStream = new MemoryStream())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue