From 7408bcd025ca76aec28a65e7883f1f73d54293b1 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 3 Jul 2017 15:25:34 -0700 Subject: [PATCH] Rewrite of code rendering context --- .../CodeGeneration/CodeRenderingContext.cs | 48 +--- .../DefaultCodeRenderingContext.cs | 15 +- .../CodeGeneration/DefaultDocumentWriter.cs | 7 +- .../DefaultTagHelperTargetExtension.cs | 126 +++++++--- .../PreallocatedAttributeTargetExtension.cs | 61 +++-- .../Properties/Resources.Designer.cs | 14 ++ .../Resources.resx | 3 + .../DefaultTagHelperTargetExtensionTest.cs | 222 +++++++++++++++++- ...reallocatedAttributeTargetExtensionTest.cs | 93 +++++++- ...ixedAttributeTagHelpers_Runtime.codegen.cs | 8 + 10 files changed, 489 insertions(+), 108 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeRenderingContext.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeRenderingContext.cs index c0a8ac49ef..40bd7c3cd3 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeRenderingContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/CodeRenderingContext.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration @@ -18,11 +19,15 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration public abstract string DocumentKind { get; } public abstract ItemCollection Items { get; } + + public abstract IEnumerable Ancestors { get; } public abstract IntermediateNodeWriter NodeWriter { get; } public abstract RazorCodeGenerationOptions Options { get; } + public abstract IntermediateNode Parent { get; } + public abstract RazorSourceDocument SourceDocument { get; } public abstract Scope CreateScope(); @@ -56,48 +61,5 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration _context.EndScope(); } } - - - // All bits below here are temporary - - #region Temporary TagHelper bits - - internal TagHelperRenderingContext TagHelperRenderingContext { get; set; } - - internal TagHelperRenderingContextScope Push(TagHelperRenderingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var scope = new TagHelperRenderingContextScope(this, TagHelperRenderingContext); - TagHelperRenderingContext = context; - return scope; - } - - internal struct TagHelperRenderingContextScope : IDisposable - { - private readonly CodeRenderingContext _context; - private readonly TagHelperRenderingContext _renderingContext; - - public TagHelperRenderingContextScope(CodeRenderingContext context, TagHelperRenderingContext renderingContext) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - _context = context; - _renderingContext = renderingContext; - } - - public void Dispose() - { - _context.TagHelperRenderingContext = _renderingContext; - } - } - - #endregion } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultCodeRenderingContext.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultCodeRenderingContext.cs index ae0691e7f2..f6efde9236 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultCodeRenderingContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultCodeRenderingContext.cs @@ -10,9 +10,9 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration { internal class DefaultCodeRenderingContext : CodeRenderingContext { + private readonly Stack _ancestors; private readonly RazorCodeDocument _codeDocument; private readonly DocumentIntermediateNode _documentNode; - private readonly List _scopes; public DefaultCodeRenderingContext( @@ -52,12 +52,11 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration _documentNode = documentNode; Options = options; + _ancestors = new Stack(); Diagnostics = new RazorDiagnosticCollection(); Items = new ItemCollection(); LineMappings = new List(); - TagHelperRenderingContext = new TagHelperRenderingContext(); - var diagnostics = _documentNode.GetAllDiagnostics(); for (var i = 0; i < diagnostics.Count; i++) { @@ -81,6 +80,10 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration // This will be initialized by the document writer when the context is 'live'. public IntermediateNodeVisitor Visitor { get; set; } + public override IEnumerable Ancestors => _ancestors; + + internal Stack AncestorsInternal => _ancestors; + public override CodeWriter CodeWriter { get; } public override RazorDiagnosticCollection Diagnostics { get; } @@ -95,6 +98,8 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration public override RazorCodeGenerationOptions Options { get; } + public override IntermediateNode Parent => _ancestors.Count == 0 ? null : _ancestors.Peek(); + public override RazorSourceDocument SourceDocument => _codeDocument.Source; private ScopeInternal Current => _scopes[_scopes.Count - 1]; @@ -154,10 +159,14 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration throw new ArgumentNullException(nameof(node)); } + _ancestors.Push(node); + for (var i = 0; i < node.Children.Count; i++) { Visitor.Visit(node.Children[i]); } + + _ancestors.Pop(); } public override void RenderNode(IntermediateNode node) diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs index fc65f6533f..b358ee956d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs @@ -200,12 +200,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration public override void VisitTagHelper(TagHelperIntermediateNode node) { - var tagHelperRenderingContext = new TagHelperRenderingContext(); - - using (Context.Push(tagHelperRenderingContext)) - { - VisitDefault(node); - } + VisitDefault(node); } public override void VisitDefault(IntermediateNode node) diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs index 83d33d4120..f583a96821 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/DefaultTagHelperTargetExtension.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -75,6 +76,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperBody(CodeRenderingContext context, DefaultTagHelperBodyIntermediateNode node) { + if (context.Parent as TagHelperIntermediateNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } + if (DesignTime) { context.RenderChildren(node); @@ -121,6 +128,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperCreate(CodeRenderingContext context, DefaultTagHelperCreateIntermediateNode node) { + if (context.Parent as TagHelperIntermediateNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } + context.CodeWriter .WriteStartAssignment(node.Field) .Write(CreateTagHelperMethodName) @@ -137,6 +150,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperExecute(CodeRenderingContext context, DefaultTagHelperExecuteIntermediateNode node) { + if (context.Parent as TagHelperIntermediateNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } + if (!DesignTime) { context.CodeWriter @@ -178,6 +197,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperHtmlAttribute(CodeRenderingContext context, DefaultTagHelperHtmlAttributeIntermediateNode node) { + if (context.Parent as TagHelperIntermediateNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } + if (DesignTime) { context.RenderChildren(node); @@ -261,22 +286,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperProperty(CodeRenderingContext context, DefaultTagHelperPropertyIntermediateNode node) { - var tagHelperRenderingContext = context.TagHelperRenderingContext; - var propertyName = node.BoundAttribute.GetPropertyName(); - var propertyValueAccessor = GetTagHelperPropertyAccessor(node.IsIndexerNameMatch, node.Field, node.AttributeName, node.BoundAttribute); + var tagHelperNode = context.Parent as TagHelperIntermediateNode; + if (context.Parent == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } if (!DesignTime) { // Ensure that the property we're trying to set has initialized its dictionary bound properties. if (node.IsIndexerNameMatch && - tagHelperRenderingContext.VerifiedPropertyDictionaries.Add($"{node.TagHelper.GetTypeName()}.{propertyName}")) + object.ReferenceEquals(FindFirstUseOfIndexer(tagHelperNode, node), node)) { // Throw a reasonable Exception at runtime if the dictionary property is null. context.CodeWriter .Write("if (") .Write(node.Field) .Write(".") - .Write(propertyName) + .Write(node.Property) .WriteLine(" == null)"); using (context.CodeWriter.BuildScope()) { @@ -290,34 +318,40 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions .WriteParameterSeparator() .WriteStringLiteral(node.TagHelper.GetTypeName()) .WriteParameterSeparator() - .WriteStringLiteral(propertyName) + .WriteStringLiteral(node.Property) .WriteEndMethodInvocation(endLine: false) // End of method call .WriteEndMethodInvocation(); // End of new expression / throw statement } } } - if (tagHelperRenderingContext.RenderedBoundAttributes.TryGetValue(node.AttributeName, out var previousValueAccessor)) + // If this is not the first use of the attribute value, we need to evaluate the expression and assign it to + // the tag helper property. + // + // Otherwise, the value has already been computed and assigned to another tag helper. We just need to + // copy from that tag helper to this one. + // + // This is important because we can't evaluate the expression twice due to side-effects. + var firstUseOfAttribute = FindFirstUseOfAttribute(tagHelperNode, node); + if (!object.ReferenceEquals(firstUseOfAttribute, node)) { + // If we get here, this value has already been used. We just need to copy the value. context.CodeWriter - .WriteStartAssignment(propertyValueAccessor) - .Write(previousValueAccessor) + .WriteStartAssignment(GetPropertyAccessor(node)) + .Write(GetPropertyAccessor(firstUseOfAttribute)) .WriteLine(";"); return; } - else - { - tagHelperRenderingContext.RenderedBoundAttributes[node.AttributeName] = propertyValueAccessor; - } + // If we get there, this is the first time seeing this property so we need to evaluate the expression. if (node.BoundAttribute.IsStringProperty || (node.IsIndexerNameMatch && node.BoundAttribute.IsIndexerStringProperty)) { if (DesignTime) { context.RenderChildren(node); - context.CodeWriter.WriteStartAssignment(propertyValueAccessor); + context.CodeWriter.WriteStartAssignment(GetPropertyAccessor(node)); if (node.Children.Count == 1 && node.Children.First() is HtmlContentIntermediateNode htmlNode) { var content = GetContent(htmlNode); @@ -341,7 +375,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions context.CodeWriter .WriteStartAssignment(StringValueBufferVariableName) .WriteMethodInvocation(EndWriteTagHelperAttributeMethodName) - .WriteStartAssignment(propertyValueAccessor) + .WriteStartAssignment(GetPropertyAccessor(node)) .Write(StringValueBufferVariableName) .WriteLine(";"); } @@ -355,7 +389,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions using (context.CodeWriter.BuildLinePragma(node.Source)) { - var assignmentPrefixLength = propertyValueAccessor.Length + " = ".Length; + var accessor = GetPropertyAccessor(node); + var assignmentPrefixLength = accessor.Length + " = ".Length; if (node.BoundAttribute.IsEnum && node.Children.Count == 1 && node.Children.First() is IntermediateToken token && @@ -369,7 +404,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } context.CodeWriter - .WriteStartAssignment(propertyValueAccessor) + .WriteStartAssignment(accessor) .Write("global::") .Write(node.BoundAttribute.TypeName) .Write("."); @@ -381,7 +416,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions context.CodeWriter.WritePadding(assignmentPrefixLength, node.Source, context); } - context.CodeWriter.WriteStartAssignment(propertyValueAccessor); + context.CodeWriter.WriteStartAssignment(GetPropertyAccessor(node)); } RenderTagHelperAttributeInline(context, node, node.Source); @@ -393,7 +428,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions { using (context.CodeWriter.BuildLinePragma(node.Source)) { - context.CodeWriter.WriteStartAssignment(propertyValueAccessor); + context.CodeWriter.WriteStartAssignment(GetPropertyAccessor(node)); if (node.BoundAttribute.IsEnum && node.Children.Count == 1 && @@ -422,7 +457,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions ExecutionContextAddTagHelperAttributeMethodName) .WriteStringLiteral(node.AttributeName) .WriteParameterSeparator() - .Write(propertyValueAccessor) + .Write(GetPropertyAccessor(node)) .WriteParameterSeparator() .Write($"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}") .WriteEndMethodInvocation(); @@ -548,6 +583,45 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } } + private static DefaultTagHelperPropertyIntermediateNode FindFirstUseOfIndexer( + TagHelperIntermediateNode tagHelperNode, + DefaultTagHelperPropertyIntermediateNode propertyNode) + { + Debug.Assert(tagHelperNode.Children.Contains(propertyNode)); + Debug.Assert(propertyNode.IsIndexerNameMatch); + + for (var i = 0; i < tagHelperNode.Children.Count; i++) + { + if (tagHelperNode.Children[i] is DefaultTagHelperPropertyIntermediateNode otherPropertyNode && + otherPropertyNode.TagHelper.Equals(propertyNode.TagHelper) && + otherPropertyNode.BoundAttribute.Equals(propertyNode.BoundAttribute) && + otherPropertyNode.IsIndexerNameMatch) + { + return otherPropertyNode; + } + } + + // This is unreachable, we should find 'propertyNode' in the list of children. + throw new InvalidOperationException(); + } + + private static DefaultTagHelperPropertyIntermediateNode FindFirstUseOfAttribute( + TagHelperIntermediateNode tagHelperNode, + DefaultTagHelperPropertyIntermediateNode propertyNode) + { + for (var i = 0; i < tagHelperNode.Children.Count; i++) + { + if (tagHelperNode.Children[i] is DefaultTagHelperPropertyIntermediateNode otherPropertyNode && + string.Equals(otherPropertyNode.AttributeName, propertyNode.AttributeName, StringComparison.Ordinal)) + { + return otherPropertyNode; + } + } + + // This is unreachable, we should find 'propertyNode' in the list of children. + throw new InvalidOperationException(); + } + private string GetContent(HtmlContentIntermediateNode node) { var builder = new StringBuilder(); @@ -562,17 +636,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions return builder.ToString(); } - private static string GetTagHelperPropertyAccessor( - bool isIndexerNameMatch, - string tagHelperVariableName, - string attributeName, - BoundAttributeDescriptor descriptor) + private static string GetPropertyAccessor(DefaultTagHelperPropertyIntermediateNode node) { - var propertyAccessor = $"{tagHelperVariableName}.{descriptor.GetPropertyName()}"; + var propertyAccessor = $"{node.Field}.{node.Property}"; - if (isIndexerNameMatch) + if (node.IsIndexerNameMatch) { - var dictionaryKey = attributeName.Substring(descriptor.IndexerNamePrefix.Length); + var dictionaryKey = node.AttributeName.Substring(node.BoundAttribute.IndexerNamePrefix.Length); propertyAccessor += $"[\"{dictionaryKey}\"]"; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedAttributeTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedAttributeTargetExtension.cs index ded37ecce3..fe3249e7e0 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedAttributeTargetExtension.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/PreallocatedAttributeTargetExtension.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language.Extensions { @@ -50,6 +52,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperHtmlAttribute(CodeRenderingContext context, PreallocatedTagHelperHtmlAttributeIntermediateNode node) { + if (context.Parent as TagHelperIntermediateNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } + context.CodeWriter .WriteStartInstanceMethodInvocation(ExecutionContextVariableName, ExecutionContextAddHtmlAttributeMethodName) .Write(node.VariableName) @@ -75,20 +83,23 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public void WriteTagHelperProperty(CodeRenderingContext context, PreallocatedTagHelperPropertyIntermediateNode node) { - var propertyName = node.BoundAttribute.GetPropertyName(); - var propertyValueAccessor = GetTagHelperPropertyAccessor(node.IsIndexerNameMatch, node.Field, node.AttributeName, node.BoundAttribute); - var attributeValueAccessor = $"{node.VariableName}.Value" /* ORIGINAL: TagHelperAttributeValuePropertyName */; + var tagHelperNode = context.Parent as TagHelperIntermediateNode; + if (tagHelperNode == null) + { + var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode)); + throw new InvalidOperationException(message); + } // Ensure that the property we're trying to set has initialized its dictionary bound properties. if (node.IsIndexerNameMatch && - context.TagHelperRenderingContext.VerifiedPropertyDictionaries.Add($"{node.TagHelper.GetTypeName()}.{propertyName}")) + object.ReferenceEquals(FindFirstUseOfIndexer(tagHelperNode, node), node)) { // Throw a reasonable Exception at runtime if the dictionary property is null. context.CodeWriter .Write("if (") .Write(node.Field) .Write(".") - .Write(propertyName) + .Write(node.Property) .WriteLine(" == null)"); using (context.CodeWriter.BuildScope()) { @@ -102,33 +113,51 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions .WriteParameterSeparator() .WriteStringLiteral(node.TagHelper.GetTypeName()) .WriteParameterSeparator() - .WriteStringLiteral(propertyName) + .WriteStringLiteral(node.Property) .WriteEndMethodInvocation(endLine: false) // End of method call .WriteEndMethodInvocation(); // End of new expression / throw statement } } context.CodeWriter - .WriteStartAssignment(propertyValueAccessor) + .WriteStartAssignment(GetPropertyAccessor(node)) .Write("(string)") - .Write(attributeValueAccessor) + .Write($"{node.VariableName}.Value") .WriteLine(";") .WriteStartInstanceMethodInvocation(ExecutionContextVariableName, ExecutionContextAddTagHelperAttributeMethodName) .Write(node.VariableName) .WriteEndMethodInvocation(); } - private static string GetTagHelperPropertyAccessor( - bool isIndexerNameMatch, - string tagHelperVariableName, - string attributeName, - BoundAttributeDescriptor descriptor) + private static PreallocatedTagHelperPropertyIntermediateNode FindFirstUseOfIndexer( + TagHelperIntermediateNode tagHelperNode, + PreallocatedTagHelperPropertyIntermediateNode propertyNode) { - var propertyAccessor = $"{tagHelperVariableName}.{descriptor.GetPropertyName()}"; + Debug.Assert(tagHelperNode.Children.Contains(propertyNode)); + Debug.Assert(propertyNode.IsIndexerNameMatch); - if (isIndexerNameMatch) + for (var i = 0; i < tagHelperNode.Children.Count; i++) { - var dictionaryKey = attributeName.Substring(descriptor.IndexerNamePrefix.Length); + if (tagHelperNode.Children[i] is PreallocatedTagHelperPropertyIntermediateNode otherPropertyNode && + otherPropertyNode.TagHelper.Equals(propertyNode.TagHelper) && + otherPropertyNode.BoundAttribute.Equals(propertyNode.BoundAttribute) && + otherPropertyNode.IsIndexerNameMatch) + { + return otherPropertyNode; + } + } + + // This is unreachable, we should find 'propertyNode' in the list of children. + throw new InvalidOperationException(); + } + + private static string GetPropertyAccessor(PreallocatedTagHelperPropertyIntermediateNode node) + { + var propertyAccessor = $"{node.Field}.{node.Property}"; + + if (node.IsIndexerNameMatch) + { + var dictionaryKey = node.AttributeName.Substring(node.BoundAttribute.IndexerNamePrefix.Length); propertyAccessor += $"[\"{dictionaryKey}\"]"; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 3850448e4e..89a137d0f8 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -584,6 +584,20 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatRenderingContextRequiresDelegate(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("RenderingContextRequiresDelegate"), p0, p1); + /// + /// The '{0}' node type can only be used as a direct child of a '{1}' node. + /// + internal static string IntermediateNodes_InvalidParentNode + { + get => GetString("IntermediateNodes_InvalidParentNode"); + } + + /// + /// The '{0}' node type can only be used as a direct child of a '{1}' node. + /// + internal static string FormatIntermediateNodes_InvalidParentNode(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("IntermediateNodes_InvalidParentNode"), p0, p1); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index fc64c848ea..15d914463d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -240,4 +240,7 @@ The '{0}' requires a '{1}' delegate to be set. + + The '{0}' node type can only be used as a direct child of a '{1}' node. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs index 99e0bea81f..18c9673777 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/DefaultTagHelperTargetExtensionTest.cs @@ -71,6 +71,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperBodyIntermediateNode() { Children = @@ -78,6 +79,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions new CSharpExpressionIntermediateNode(), } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperBody(context, node); @@ -98,6 +101,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperBodyIntermediateNode() { Children = @@ -107,6 +111,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions TagMode = TagMode.SelfClosing, TagName = "p", }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperBody(context, node); @@ -130,11 +136,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperCreateIntermediateNode() { Field = "__TestNamespace_MyTagHelper", Type = "TestNamespace.MyTagHelper", }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperCreate(context, node); @@ -155,11 +164,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperCreateIntermediateNode() { Field = "__TestNamespace_MyTagHelper", Type = "TestNamespace.MyTagHelper", }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperCreate(context, node); @@ -181,7 +193,10 @@ __tagHelperExecutionContext.Add(__TestNamespace_MyTagHelper); var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperExecuteIntermediateNode(); + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperExecute(context, node); @@ -201,7 +216,10 @@ __tagHelperExecutionContext.Add(__TestNamespace_MyTagHelper); var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperExecuteIntermediateNode(); + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperExecute(context, node); @@ -228,6 +246,7 @@ __tagHelperExecutionContext = __tagHelperScopeManager.End(); var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -244,6 +263,8 @@ __tagHelperExecutionContext = __tagHelperScopeManager.End(); } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperHtmlAttribute(context, node); @@ -265,6 +286,7 @@ Render Children var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -277,6 +299,8 @@ Render Children } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperHtmlAttribute(context, node); @@ -300,6 +324,7 @@ __tagHelperExecutionContext.AddHtmlAttribute(""name"", Html.Raw(__tagHelperStrin var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -316,6 +341,8 @@ __tagHelperExecutionContext.AddHtmlAttribute(""name"", Html.Raw(__tagHelperStrin } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperHtmlAttribute(context, node); @@ -339,6 +366,7 @@ EndAddHtmlAttributeValues(__tagHelperExecutionContext); var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -356,6 +384,8 @@ EndAddHtmlAttributeValues(__tagHelperExecutionContext); } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -377,6 +407,7 @@ __InputTagHelper.StringProp = ""value""; var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -394,6 +425,8 @@ __InputTagHelper.StringProp = ""value""; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -415,6 +448,7 @@ __InputTagHelper.StringProp = string.Empty; var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -433,6 +467,8 @@ __InputTagHelper.StringProp = string.Empty; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -450,6 +486,50 @@ __InputTagHelper.IntProp = 32; ignoreLineEndingDifferences: true); } + // If a value is bound to multiple tag helpers, we want to make sure to only render the first + // occurrence of the expression due to side-effects. + [Fact] + public void WriteTagHelperProperty_DesignTime_NonStringProperty_SecondUseOfAttribute() + { + // Arrange + var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; + var context = TestCodeRenderingContext.CreateDesignTime(); + + var tagHelperNode = new TagHelperIntermediateNode(); + var node1 = new DefaultTagHelperPropertyIntermediateNode() + { + // We only look at the attribute name here. + AttributeName = "bound", + Field = "__OtherTagHelper", + Property = "IntProp", + }; + var node2 = new DefaultTagHelperPropertyIntermediateNode() + { + AttributeName = "bound", + AttributeStructure = AttributeStructure.DoubleQuotes, + BoundAttribute = IntPropertyTagHelper.BoundAttributes.Single(), + Field = "__InputTagHelper", + IsIndexerNameMatch = false, + Property = "IntProp", + TagHelper = IntPropertyTagHelper, + Source = Span, + }; + tagHelperNode.Children.Add(node1); + tagHelperNode.Children.Add(node2); + Push(context, tagHelperNode); + + // Act + extension.WriteTagHelperProperty(context, node2); + + // Assert + var csharp = context.CodeWriter.Builder.ToString(); + Assert.Equal( +@"__InputTagHelper.IntProp = __OtherTagHelper.IntProp; +", + csharp, + ignoreLineEndingDifferences: true); + } + [Fact] public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly_WithoutLocation() { @@ -457,6 +537,7 @@ __InputTagHelper.IntProp = 32; var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -474,6 +555,8 @@ __InputTagHelper.IntProp = 32; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -494,6 +577,7 @@ __InputTagHelper.IntProp = 32; var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -512,6 +596,8 @@ __InputTagHelper.IntProp = 32; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -536,6 +622,7 @@ __InputTagHelper.IntIndexer[""bound""] = 32; var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -553,6 +640,8 @@ __InputTagHelper.IntIndexer[""bound""] = 32; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -573,6 +662,7 @@ __InputTagHelper.IntIndexer[""bound""] = 32; var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -590,6 +680,8 @@ __InputTagHelper.IntIndexer[""bound""] = 32; } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -616,6 +708,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.St var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -634,6 +727,8 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.St } }, }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -652,6 +747,50 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In ignoreLineEndingDifferences: true); } + // If a value is bound to multiple tag helpers, we want to make sure to only render the first + // occurrence of the expression due to side-effects. + [Fact] + public void WriteTagHelperProperty_Runtime_NonStringProperty_SecondUseOfAttribute() + { + // Arrange + var extension = new DefaultTagHelperTargetExtension(); + var context = TestCodeRenderingContext.CreateRuntime(); + + var tagHelperNode = new TagHelperIntermediateNode(); + var node1 = new DefaultTagHelperPropertyIntermediateNode() + { + // We only look at the attribute name here. + AttributeName = "bound", + Field = "__OtherTagHelper", + Property = "IntProp", + }; + var node2 = new DefaultTagHelperPropertyIntermediateNode() + { + AttributeName = "bound", + AttributeStructure = AttributeStructure.DoubleQuotes, + BoundAttribute = IntPropertyTagHelper.BoundAttributes.Single(), + Field = "__InputTagHelper", + IsIndexerNameMatch = false, + Property = "IntProp", + TagHelper = IntPropertyTagHelper, + Source = Span, + }; + tagHelperNode.Children.Add(node1); + tagHelperNode.Children.Add(node2); + Push(context, tagHelperNode); + + // Act + extension.WriteTagHelperProperty(context, node2); + + // Assert + var csharp = context.CodeWriter.Builder.ToString(); + Assert.Equal( +@"__InputTagHelper.IntProp = __OtherTagHelper.IntProp; +", + csharp, + ignoreLineEndingDifferences: true); + } + [Fact] public void WriteTagHelperProperty_Runtime_NonStringProperty_RendersCorrectly_WithoutLocation() { @@ -659,6 +798,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -676,6 +816,8 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -697,6 +839,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -715,6 +858,8 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -729,6 +874,71 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""bound"", __InputTagHelper.In #line 3 ""test.cshtml"" __InputTagHelper.IntIndexer[""bound""] = 32; +#line default +#line hidden +__tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelper.IntIndexer[""bound""], global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes); +", + csharp, + ignoreLineEndingDifferences: true); + } + + [Fact] // We should only emit the validation code for the first use of an indexer property. + public void WriteTagHelperProperty_Runtime_NonStringIndexer_MultipleValues() + { + // Arrange + var extension = new DefaultTagHelperTargetExtension(); + var context = TestCodeRenderingContext.CreateRuntime(); + + var tagHelperNode = new TagHelperIntermediateNode(); + var node1 = new DefaultTagHelperPropertyIntermediateNode() + { + AttributeName = "foo-first", + AttributeStructure = AttributeStructure.DoubleQuotes, + BoundAttribute = IntIndexerTagHelper.BoundAttributes.Single(), + Field = "__InputTagHelper", + IsIndexerNameMatch = true, + Property = "IntIndexer", + TagHelper = IntIndexerTagHelper, + Source = Span, + Children = + { + new CSharpExpressionIntermediateNode() + { + Children = { new IntermediateToken { Kind = IntermediateToken.TokenKind.CSharp, Content = "17", } }, + } + } + }; + var node2 = new DefaultTagHelperPropertyIntermediateNode() + { + AttributeName = "foo-bound", + AttributeStructure = AttributeStructure.DoubleQuotes, + BoundAttribute = IntIndexerTagHelper.BoundAttributes.Single(), + Field = "__InputTagHelper", + IsIndexerNameMatch = true, + Property = "IntIndexer", + TagHelper = IntIndexerTagHelper, + Source = Span, + Children = + { + new CSharpExpressionIntermediateNode() + { + Children = { new IntermediateToken { Kind = IntermediateToken.TokenKind.CSharp, Content = "32", } }, + } + } + }; + tagHelperNode.Children.Add(node1); + tagHelperNode.Children.Add(node2); + Push(context, tagHelperNode); + + // Act + extension.WriteTagHelperProperty(context, node2); + + // Assert + var csharp = context.CodeWriter.Builder.ToString(); + Assert.Equal( +@"#line 3 ""test.cshtml"" +__InputTagHelper.IntIndexer[""bound""] = 32; + #line default #line hidden __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelper.IntIndexer[""bound""], global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes); @@ -744,6 +954,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelpe var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -761,6 +972,8 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelpe } } }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -785,7 +998,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelpe // Arrange var extension = new DefaultTagHelperTargetExtension() { DesignTime = true }; var context = TestCodeRenderingContext.CreateDesignTime(); - + var node = new DefaultTagHelperRuntimeIntermediateNode(); // Act @@ -805,7 +1018,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(""foo-bound"", __InputTagHelpe // Arrange var extension = new DefaultTagHelperTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); - + var node = new DefaultTagHelperRuntimeIntermediateNode(); // Act @@ -837,6 +1050,11 @@ private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeMana ignoreLineEndingDifferences: true); } + private static void Push(CodeRenderingContext context, TagHelperIntermediateNode node) + { + ((DefaultCodeRenderingContext)context).AncestorsInternal.Push(node); + } + private static DocumentIntermediateNode Lower(RazorCodeDocument codeDocument) { var engine = RazorEngine.Create(); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/PreallocatedAttributeTargetExtensionTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/PreallocatedAttributeTargetExtensionTest.cs index b0d79ed67e..72d306791b 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/PreallocatedAttributeTargetExtensionTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Extensions/PreallocatedAttributeTargetExtensionTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; using Xunit; namespace Microsoft.AspNetCore.Razor.Language.Extensions @@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public class PreallocatedAttributeTargetExtensionTest { [Fact] - public void WriteDeclarePreallocatedTagHelperHtmlAttribute_RendersCorrectly() + public void WriteTagHelperHtmlAttributeValue_RendersCorrectly() { // Arrange var extension = new PreallocatedAttributeTargetExtension(); @@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } [Fact] - public void WriteDeclarePreallocatedTagHelperHtmlAttribute_Minimized_RendersCorrectly() + public void WriteTagHelperHtmlAttributeValue_Minimized_RendersCorrectly() { // Arrange var extension = new PreallocatedAttributeTargetExtension(); @@ -63,16 +64,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } [Fact] - public void WriteAddPreallocatedTagHelperHtmlAttribute_RendersCorrectly() + public void WriteTagHelperHtmlAttribute_RendersCorrectly() { // Arrange var extension = new PreallocatedAttributeTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new PreallocatedTagHelperHtmlAttributeIntermediateNode() { VariableName = "_tagHelper1" }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperHtmlAttribute(context, node); @@ -87,12 +91,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } [Fact] - public void WriteDeclarePreallocatedTagHelperAttribute_RendersCorrectly() + public void WriteTagHelperPropertyValue_RendersCorrectly() { // Arrange var extension = new PreallocatedAttributeTargetExtension(); var context = TestCodeRenderingContext.CreateRuntime(); - + var node = new PreallocatedTagHelperPropertyValueIntermediateNode() { AttributeName = "Foo", @@ -114,7 +118,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } [Fact] - public void WriteSetPreallocatedTagHelperProperty_RendersCorrectly() + public void WriteTagHelperProperty_RendersCorrectly() { // Arrange var extension = new PreallocatedAttributeTargetExtension(); @@ -124,7 +128,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions tagHelperBuilder.TypeName("FooTagHelper"); var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind); - builder .Name("Foo") .TypeName("System.String") @@ -132,13 +135,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions var descriptor = builder.Build(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = descriptor.Name, - Field = "__FooTagHelper", - VariableName = "_tagHelper1", BoundAttribute = descriptor, + Field = "__FooTagHelper", + Property = "FooProp", + VariableName = "_tagHelper1", }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -164,7 +171,6 @@ __tagHelperExecutionContext.AddTagHelperAttribute(_tagHelper1); tagHelperBuilder.TypeName("FooTagHelper"); var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind); - builder .Name("Foo") .TypeName("System.Collections.Generic.Dictionary") @@ -173,6 +179,7 @@ __tagHelperExecutionContext.AddTagHelperAttribute(_tagHelper1); var descriptor = builder.Build(); + var tagHelperNode = new TagHelperIntermediateNode(); var node = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = "pre-Foo", @@ -180,8 +187,11 @@ __tagHelperExecutionContext.AddTagHelperAttribute(_tagHelper1); VariableName = "_tagHelper1", BoundAttribute = descriptor, IsIndexerNameMatch = true, + Property = "FooProp", TagHelper = tagHelperBuilder.Build(), }; + tagHelperNode.Children.Add(node); + Push(context, tagHelperNode); // Act extension.WriteTagHelperProperty(context, node); @@ -199,5 +209,68 @@ __tagHelperExecutionContext.AddTagHelperAttribute(_tagHelper1); csharp, ignoreLineEndingDifferences: true); } + + [Fact] + public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_MultipleValues() + { + // Arrange + var extension = new PreallocatedAttributeTargetExtension(); + var context = TestCodeRenderingContext.CreateRuntime(); + + var tagHelperBuilder = new DefaultTagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "FooTagHelper", "Test"); + tagHelperBuilder.TypeName("FooTagHelper"); + + var builder = new DefaultBoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind); + builder + .Name("Foo") + .TypeName("System.Collections.Generic.Dictionary") + .AsDictionaryAttribute("pre-", "System.String") + .PropertyName("FooProp"); + + var boundAttribute = builder.Build(); + var tagHelper = tagHelperBuilder.Build(); + + var tagHelperNode = new TagHelperIntermediateNode(); + var node1 = new PreallocatedTagHelperPropertyIntermediateNode() + { + AttributeName = "pre-Bar", + Field = "__FooTagHelper", + VariableName = "_tagHelper0s", + BoundAttribute = boundAttribute, + IsIndexerNameMatch = true, + Property = "FooProp", + TagHelper = tagHelper, + }; + var node2 = new PreallocatedTagHelperPropertyIntermediateNode() + { + AttributeName = "pre-Foo", + Field = "__FooTagHelper", + VariableName = "_tagHelper1", + BoundAttribute = boundAttribute, + IsIndexerNameMatch = true, + Property = "FooProp", + TagHelper = tagHelper, + }; + tagHelperNode.Children.Add(node1); + tagHelperNode.Children.Add(node2); + Push(context, tagHelperNode); + + // Act + extension.WriteTagHelperProperty(context, node2); + + // Assert + var csharp = context.CodeWriter.Builder.ToString(); + Assert.Equal( +@"__FooTagHelper.FooProp[""Foo""] = (string)_tagHelper1.Value; +__tagHelperExecutionContext.AddTagHelperAttribute(_tagHelper1); +", + csharp, + ignoreLineEndingDifferences: true); + } + + private static void Push(CodeRenderingContext context, TagHelperIntermediateNode node) + { + ((DefaultCodeRenderingContext)context).AncestorsInternal.Push(node); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/PrefixedAttributeTagHelpers_Runtime.codegen.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/PrefixedAttributeTagHelpers_Runtime.codegen.cs index cd5c2cc959..9b37c7dd64 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/PrefixedAttributeTagHelpers_Runtime.codegen.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/PrefixedAttributeTagHelpers_Runtime.codegen.cs @@ -181,6 +181,10 @@ __TestNamespace_InputTagHelper1.IntDictionaryProperty["pepper"] = 98; __tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_5); __TestNamespace_InputTagHelper2.StringDictionaryProperty["paprika"] = (string)__tagHelperAttribute_5.Value; __tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_5); + if (__TestNamespace_InputTagHelper1.StringDictionaryProperty == null) + { + throw new InvalidOperationException(InvalidTagHelperIndexerAssignment("string-prefix-cumin", "TestNamespace.InputTagHelper1", "StringDictionaryProperty")); + } BeginWriteTagHelperAttribute(); WriteLiteral("literate "); #line 21 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/PrefixedAttributeTagHelpers.cshtml" @@ -192,6 +196,10 @@ __TestNamespace_InputTagHelper1.IntDictionaryProperty["pepper"] = 98; __tagHelperStringValueBuffer = EndWriteTagHelperAttribute(); __TestNamespace_InputTagHelper1.StringDictionaryProperty["cumin"] = __tagHelperStringValueBuffer; __tagHelperExecutionContext.AddTagHelperAttribute("string-prefix-cumin", __TestNamespace_InputTagHelper1.StringDictionaryProperty["cumin"], global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes); + if (__TestNamespace_InputTagHelper2.StringDictionaryProperty == null) + { + throw new InvalidOperationException(InvalidTagHelperIndexerAssignment("string-prefix-cumin", "TestNamespace.InputTagHelper2", "StringDictionaryProperty")); + } __TestNamespace_InputTagHelper2.StringDictionaryProperty["cumin"] = __TestNamespace_InputTagHelper1.StringDictionaryProperty["cumin"]; await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); if (!__tagHelperExecutionContext.Output.IsContentModified)