From 8ea4877d472ec03b7c2e17c101d16c52ecdc8438 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 08:38:54 -0700 Subject: [PATCH 01/13] Change TagHelperIntermediateNode.TagHelpers property to ImmutableArray It's unnecessary to create a new List for the TagHelperIntermediateNode.TagHelpers property and add tag helpers to it. Since TagHelperBinding.Descriptors is already an ImmutableArray, it can just be assigned to TagHelperIntermediateNode.TagHelpers. --- .../Language/Components/ComponentLoweringPass.cs | 3 +-- .../DefaultRazorIntermediateNodeLoweringPhase.cs | 16 ++++------------ .../Intermediate/TagHelperIntermediateNode.cs | 3 ++- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs index 7ecfeb47f1b..ab161e20175 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs @@ -89,9 +89,8 @@ static TagHelperDescriptor GetTagHelperOrAddDiagnostic(TagHelperIntermediateNode { TagHelperDescriptor candidate = null; List matched = null; - for (var i = 0; i < node.TagHelpers.Count; i++) + foreach (var tagHelper in node.TagHelpers) { - var tagHelper = node.TagHelpers[i]; if (!tagHelper.IsComponentTagHelper) { continue; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index a22fe867146..83ddcbab59e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1032,14 +1032,10 @@ public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax no { TagName = tagName, TagMode = info.TagMode, - Source = BuildSourceSpanFromNode(node) + Source = BuildSourceSpanFromNode(node), + TagHelpers = info.BindingResult.Descriptors }; - foreach (var tagHelper in info.BindingResult.Descriptors) - { - tagHelperNode.TagHelpers.Add(tagHelper); - } - _builder.Push(tagHelperNode); _builder.Push(new TagHelperBodyIntermediateNode()); @@ -1775,14 +1771,10 @@ public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax no { TagName = tagName, TagMode = info.TagMode, - Source = BuildSourceSpanFromNode(node) + Source = BuildSourceSpanFromNode(node), + TagHelpers = info.BindingResult.Descriptors }; - foreach (var tagHelper in info.BindingResult.Descriptors) - { - tagHelperNode.TagHelpers.Add(tagHelper); - } - if (node.StartTag != null && // We only want this error during the second phase of the two phase compilation. !_document.Options.SuppressPrimaryMethodBody && diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs index 42ca1805bca..ac550fb7092 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -17,7 +18,7 @@ public sealed class TagHelperIntermediateNode : IntermediateNode public string TagName { get; set; } - public IList TagHelpers { get; } = new List(); + public ImmutableArray TagHelpers { get; init => field = value.NullToEmpty(); } = []; public TagHelperBodyIntermediateNode Body => Children.OfType().SingleOrDefault(); From 08e4731d4ae23c19be6e1519374a09218d5baf81 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 09:16:03 -0700 Subject: [PATCH 02/13] Only store one tag helper object in an intermediate node Many intermediate nodes store several related tag helper objects. For example, TagHelperPropertyIntermediateNode stores both a BoundAttributeDesccriptor and a TagHelperDescriptor. However, for all intermediate nodes, the tag helper objects are always have a parent-child relationship. Now that tag helper objects actually provide a Parent property, we can use that and avoid extra fields. --- .../DefaultTagHelperTargetExtensionTest.cs | 15 ---- ...reallocatedAttributeTargetExtensionTest.cs | 77 +++++++++---------- .../Components/ComponentBindLoweringPass.cs | 3 - ...faultRazorIntermediateNodeLoweringPhase.cs | 12 +-- ...efaultTagHelperPropertyIntermediateNode.cs | 3 +- ...ocatedTagHelperPropertyIntermediateNode.cs | 3 +- .../ComponentAttributeIntermediateNode.cs | 5 +- .../ComponentTypeArgumentIntermediateNode.cs | 3 +- ...elperDirectiveAttributeIntermediateNode.cs | 2 +- ...ctiveAttributeParameterIntermediateNode.cs | 4 +- .../TagHelperPropertyIntermediateNode.cs | 2 +- 11 files changed, 46 insertions(+), 83 deletions(-) diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs index aa2a5757908..79587345197 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs @@ -434,7 +434,6 @@ public void WriteTagHelperProperty_DesignTime_StringProperty_HtmlContent_Renders FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "StringProp", - TagHelper = StringPropertyTagHelper, Children = { new HtmlContentIntermediateNode() @@ -475,7 +474,6 @@ public void WriteTagHelperProperty_DesignTime_StringProperty_NonHtmlContent_Rend FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "StringProp", - TagHelper = StringPropertyTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -516,7 +514,6 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Source = Span, Children = { @@ -573,7 +570,6 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_SecondUseOfAttri FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Source = Span, }; tagHelperNode.Children.Add(node1); @@ -608,7 +604,6 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -648,7 +643,6 @@ public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly( FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Source = Span, Children = { @@ -696,7 +690,6 @@ public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly_ FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -736,7 +729,6 @@ public void WriteTagHelperProperty_Runtime_StringProperty_HtmlContent_RendersCor FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "StringProp", - TagHelper = StringPropertyTagHelper, Children = { new HtmlContentIntermediateNode() @@ -782,7 +774,6 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_RendersCorrectly() FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -840,7 +831,6 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_SecondUseOfAttribut FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Source = Span, }; tagHelperNode.Children.Add(node1); @@ -875,7 +865,6 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_RendersCorrectly_Wi FieldName = "__InputTagHelper", IsIndexerNameMatch = false, PropertyName = "IntProp", - TagHelper = IntPropertyTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -916,7 +905,6 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_RendersCorrectly() FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -969,7 +957,6 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_MultipleValues() FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -986,7 +973,6 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_MultipleValues() FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Children = { new CSharpExpressionIntermediateNode() @@ -1036,7 +1022,6 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_RendersCorrectly_Wit FieldName = "__InputTagHelper", IsIndexerNameMatch = true, PropertyName = "IntIndexer", - TagHelper = IntIndexerTagHelper, Children = { new CSharpExpressionIntermediateNode() diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs index 962fba9f110..efac52ca6f8 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs @@ -127,26 +127,26 @@ public void WriteTagHelperProperty_RendersCorrectly() var extension = new PreallocatedAttributeTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "FooTagHelper", "Test"); - tagHelperBuilder.Metadata(TypeName("FooTagHelper")); + var tagHelper = TagHelperDescriptorBuilder.Create("FooTagHelper", "Test") + .Metadata(TypeName("FooTagHelper")) + .BoundAttributeDescriptor(builder => builder + .Name("Foo") + .TypeName("System.String") + .PropertyName("FooProp")) + .Build(); - var builder = new BoundAttributeDescriptorBuilder(tagHelperBuilder); - builder - .Name("Foo") - .TypeName("System.String") - .PropertyName("FooProp"); - - var descriptor = builder.Build(); + var attribute = tagHelper.BoundAttributes[0]; var tagHelperNode = new TagHelperIntermediateNode(); var node = new PreallocatedTagHelperPropertyIntermediateNode() { - AttributeName = descriptor.Name, - BoundAttribute = descriptor, + AttributeName = attribute.Name, + BoundAttribute = attribute, FieldName = "__FooTagHelper", PropertyName = "FooProp", VariableName = "_tagHelper1", }; + tagHelperNode.Children.Add(node); Push(context, tagHelperNode); @@ -170,17 +170,16 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_RendersCorrec var extension = new PreallocatedAttributeTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "FooTagHelper", "Test"); - tagHelperBuilder.Metadata(TypeName("FooTagHelper")); - - var builder = new BoundAttributeDescriptorBuilder(tagHelperBuilder); - builder - .Name("Foo") - .TypeName("System.Collections.Generic.Dictionary") - .AsDictionaryAttribute("pre-", "System.String") - .PropertyName("FooProp"); + var tagHelper = TagHelperDescriptorBuilder.Create("FooTagHelper", "Test") + .Metadata(TypeName("FooTagHelper")) + .BoundAttributeDescriptor(builder => builder + .Name("Foo") + .TypeName("System.Collections.Generic.Dictionary") + .AsDictionaryAttribute("pre-", "System.String") + .PropertyName("FooProp")) + .Build(); - var descriptor = builder.Build(); + var attribute = tagHelper.BoundAttributes[0]; var tagHelperNode = new TagHelperIntermediateNode(); var node = new PreallocatedTagHelperPropertyIntermediateNode() @@ -188,11 +187,11 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_RendersCorrec AttributeName = "pre-Foo", FieldName = "__FooTagHelper", VariableName = "_tagHelper1", - BoundAttribute = descriptor, + BoundAttribute = attribute, IsIndexerNameMatch = true, PropertyName = "FooProp", - TagHelper = tagHelperBuilder.Build(), }; + tagHelperNode.Children.Add(node); Push(context, tagHelperNode); @@ -220,18 +219,16 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_MultipleValue var extension = new PreallocatedAttributeTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "FooTagHelper", "Test"); - tagHelperBuilder.Metadata(TypeName("FooTagHelper")); - - var builder = new BoundAttributeDescriptorBuilder(tagHelperBuilder); - builder - .Name("Foo") - .TypeName("System.Collections.Generic.Dictionary") - .AsDictionaryAttribute("pre-", "System.String") - .PropertyName("FooProp"); + var tagHelper = TagHelperDescriptorBuilder.Create("FooTagHelper", "Test") + .Metadata(TypeName("FooTagHelper")) + .BoundAttributeDescriptor(builder => builder + .Name("Foo") + .TypeName("System.Collections.Generic.Dictionary") + .AsDictionaryAttribute("pre-", "System.String") + .PropertyName("FooProp")) + .Build(); - var boundAttribute = builder.Build(); - var tagHelper = tagHelperBuilder.Build(); + var attribute = tagHelper.BoundAttributes[0]; var tagHelperNode = new TagHelperIntermediateNode(); var node1 = new PreallocatedTagHelperPropertyIntermediateNode() @@ -239,21 +236,21 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_MultipleValue AttributeName = "pre-Bar", FieldName = "__FooTagHelper", VariableName = "_tagHelper0s", - BoundAttribute = boundAttribute, + BoundAttribute = attribute, IsIndexerNameMatch = true, - PropertyName = "FooProp", - TagHelper = tagHelper, + PropertyName = "FooProp" }; + var node2 = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = "pre-Foo", FieldName = "__FooTagHelper", VariableName = "_tagHelper1", - BoundAttribute = boundAttribute, + BoundAttribute = attribute, IsIndexerNameMatch = true, - PropertyName = "FooProp", - TagHelper = tagHelper, + PropertyName = "FooProp" }; + tagHelperNode.Children.Add(node1); tagHelperNode.Children.Add(node2); Push(context, tagHelperNode); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentBindLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentBindLoweringPass.cs index 70af0af9fb7..aa390a8e044 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentBindLoweringPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentBindLoweringPass.cs @@ -523,7 +523,6 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE valueNode.AttributeName = valueAttributeName; valueNode.BoundAttribute = valueAttribute; // Might be null if it doesn't match a component attribute valueNode.PropertyName = valuePropertyName; - valueNode.TagHelper = valueAttribute == null ? null : bindEntry.GetEffectiveNodeTagHelperDescriptor(); valueNode.TypeName = valueAttribute?.IsWeaklyTyped == false ? valueAttribute.TypeName : null; valueNode.Children.Clear(); @@ -542,7 +541,6 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE changeNode.AttributeName = changeAttributeName; changeNode.BoundAttribute = changeAttribute; // Might be null if it doesn't match a component attribute changeNode.PropertyName = changeAttribute?.PropertyName; - changeNode.TagHelper = changeAttribute == null ? null : bindEntry.GetEffectiveNodeTagHelperDescriptor(); changeNode.TypeName = changeAttribute?.IsWeaklyTyped == false ? changeAttribute.TypeName : null; changeNode.Children.Clear(); @@ -565,7 +563,6 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE expressionNode.AttributeName = expressionAttributeName; expressionNode.BoundAttribute = expressionAttribute; expressionNode.PropertyName = expressionAttribute.PropertyName; - expressionNode.TagHelper = bindEntry.GetEffectiveNodeTagHelperDescriptor(); expressionNode.TypeName = expressionAttribute.IsWeaklyTyped ? null : expressionAttribute.TypeName; expressionNode.Children.Clear(); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 83ddcbab59e..5b025d05b69 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1104,7 +1104,6 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe { AttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(associatedAttributeDescriptor, attributeName.AsSpan()), @@ -1147,13 +1146,12 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta if (associatedDescriptorsWithSatisfyingAttribute.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var (associatedDescriptor, associatedAttributeDescriptor) in associatedDescriptorsWithSatisfyingAttribute) + foreach (var (_, associatedAttributeDescriptor) in associatedDescriptorsWithSatisfyingAttribute) { var setTagHelperProperty = new TagHelperPropertyIntermediateNode() { AttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(associatedAttributeDescriptor, attributeName.AsSpan()), @@ -1875,7 +1873,6 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe { AttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, IsIndexerNameMatch = indexerMatch, @@ -1947,8 +1944,6 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), OriginalAttributeName = attributeName, BoundAttributeParameter = associatedAttributeParameterDescriptor, - BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, IsIndexerNameMatch = indexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, @@ -1968,7 +1963,6 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim AttributeName = actualAttributeName, OriginalAttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, IsIndexerNameMatch = indexerMatch, @@ -2016,7 +2010,6 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta { AttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), IsIndexerNameMatch = indexerMatch, @@ -2080,8 +2073,6 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), OriginalAttributeName = attributeName, BoundAttributeParameter = associatedAttributeParameterDescriptor, - BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, IsIndexerNameMatch = indexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), @@ -2095,7 +2086,6 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec AttributeName = actualAttributeName, OriginalAttributeName = attributeName, BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), IsIndexerNameMatch = indexerMatch, diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperPropertyIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperPropertyIntermediateNode.cs index 824262a23a4..1766a8ebcfc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperPropertyIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperPropertyIntermediateNode.cs @@ -27,7 +27,6 @@ public DefaultTagHelperPropertyIntermediateNode(TagHelperPropertyIntermediateNod BoundAttribute = propertyNode.BoundAttribute; IsIndexerNameMatch = propertyNode.IsIndexerNameMatch; Source = propertyNode.Source; - TagHelper = propertyNode.TagHelper; for (var i = 0; i < propertyNode.Children.Count; i++) { @@ -51,7 +50,7 @@ public DefaultTagHelperPropertyIntermediateNode(TagHelperPropertyIntermediateNod public string PropertyName { get; set; } - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public override void Accept(IntermediateNodeVisitor visitor) { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/PreallocatedTagHelperPropertyIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/PreallocatedTagHelperPropertyIntermediateNode.cs index 5bb8905eb16..53e44525d26 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/PreallocatedTagHelperPropertyIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/PreallocatedTagHelperPropertyIntermediateNode.cs @@ -29,7 +29,6 @@ public PreallocatedTagHelperPropertyIntermediateNode(DefaultTagHelperPropertyInt IsIndexerNameMatch = propertyNode.IsIndexerNameMatch; PropertyName = propertyNode.PropertyName; Source = propertyNode.Source; - TagHelper = propertyNode.TagHelper; } public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; @@ -46,7 +45,7 @@ public PreallocatedTagHelperPropertyIntermediateNode(DefaultTagHelperPropertyInt public string PropertyName { get; set; } - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public string VariableName { get; set; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentAttributeIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentAttributeIntermediateNode.cs index 39dd3e815a2..4cfd89ac090 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentAttributeIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentAttributeIntermediateNode.cs @@ -83,7 +83,6 @@ public ComponentAttributeIntermediateNode(TagHelperPropertyIntermediateNode prop OriginalAttributeSpan = propertyNode.OriginalAttributeSpan; PropertyName = propertyNode.BoundAttribute.PropertyName; Source = propertyNode.Source; - TagHelper = propertyNode.TagHelper; TypeName = propertyNode.BoundAttribute.IsWeaklyTyped ? null : propertyNode.BoundAttribute.TypeName; for (var i = 0; i < propertyNode.Children.Count; i++) @@ -107,7 +106,6 @@ public ComponentAttributeIntermediateNode(TagHelperDirectiveAttributeIntermediat OriginalAttributeSpan = directiveAttributeNode.OriginalAttributeSpan; PropertyName = directiveAttributeNode.BoundAttribute.PropertyName; Source = directiveAttributeNode.Source; - TagHelper = directiveAttributeNode.TagHelper; TypeName = directiveAttributeNode.BoundAttribute.IsWeaklyTyped ? null : directiveAttributeNode.BoundAttribute.TypeName; for (var i = 0; i < directiveAttributeNode.Children.Count; i++) @@ -131,7 +129,6 @@ public ComponentAttributeIntermediateNode(TagHelperDirectiveAttributeParameterIn OriginalAttributeSpan = directiveAttributeParameterNode.OriginalAttributeSpan; PropertyName = directiveAttributeParameterNode.BoundAttributeParameter.PropertyName; Source = directiveAttributeParameterNode.Source; - TagHelper = directiveAttributeParameterNode.TagHelper; TypeName = directiveAttributeParameterNode.BoundAttributeParameter.TypeName; for (var i = 0; i < directiveAttributeParameterNode.Children.Count; i++) @@ -152,7 +149,7 @@ public ComponentAttributeIntermediateNode(TagHelperDirectiveAttributeParameterIn public string PropertyName { get; set; } - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute?.Parent; public string TypeName { get; set; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs index 8b6160e3d11..7d7e2a0605c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs @@ -19,7 +19,6 @@ public ComponentTypeArgumentIntermediateNode(TagHelperPropertyIntermediateNode p BoundAttribute = propertyNode.BoundAttribute; Source = propertyNode.Source; - TagHelper = propertyNode.TagHelper; Debug.Assert(propertyNode.Children.Count == 1); Value = propertyNode.Children[0] switch @@ -39,7 +38,7 @@ public ComponentTypeArgumentIntermediateNode(TagHelperPropertyIntermediateNode p public string TypeParameterName => BoundAttribute.Name; - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public CSharpIntermediateToken Value { get; set; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs index 78c28d7bdda..156ac499888 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs @@ -21,7 +21,7 @@ public sealed class TagHelperDirectiveAttributeIntermediateNode : IntermediateNo public BoundAttributeDescriptor BoundAttribute { get; set; } - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public bool IsIndexerNameMatch { get; set; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs index 3dee9d71539..ae0c4f1f8fe 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs @@ -21,9 +21,9 @@ public sealed class TagHelperDirectiveAttributeParameterIntermediateNode : Inter public BoundAttributeParameterDescriptor BoundAttributeParameter { get; set; } - public BoundAttributeDescriptor BoundAttribute { get; set; } + public BoundAttributeDescriptor BoundAttribute => BoundAttributeParameter.Parent; - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public bool IsIndexerNameMatch { get; set; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs index b2f740ef678..f3bd028bbdc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs @@ -17,7 +17,7 @@ public sealed class TagHelperPropertyIntermediateNode : IntermediateNode public BoundAttributeDescriptor BoundAttribute { get; set; } - public TagHelperDescriptor TagHelper { get; set; } + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public bool IsIndexerNameMatch { get; set; } From c30deddb67c4097b45525fe35bca79ac2f3ae7c0 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 10:50:29 -0700 Subject: [PATCH 03/13] Consolidate out parameters into struct result TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(...) has four out parameters. This change consolidates them into a single out parameter of type TagHelperAttributeMatch. This allows some logic based on the out parameters to be encapsulated, such as whether a match expects a boolean or string value. --- ...faultRazorIntermediateNodeLoweringPhase.cs | 96 ++++++------------- .../Language/Legacy/TagHelperBlockRewriter.cs | 39 +++----- .../src/Language/TagHelperAttributeMatch.cs | 51 ++++++++++ .../Language/TagHelperMatchingConventions.cs | 26 ++--- 4 files changed, 98 insertions(+), 114 deletions(-) create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperAttributeMatch.cs diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 5b025d05b69..795d5b48bca 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1853,33 +1853,24 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe { foreach (var associatedDescriptor in associatedDescriptors) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch( - associatedDescriptor, - attributeName, - out var associatedAttributeDescriptor, - out var indexerMatch, - out _, - out _)) + if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) { - var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attributeName); - - if (!expectsBooleanValue) + if (!match.ExpectsBooleanValue) { // We do not allow minimized non-boolean bound attributes. return; } - var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + var setTagHelperProperty = new TagHelperPropertyIntermediateNode { AttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = indexerMatch, + IsIndexerNameMatch = match.IsIndexerMatch, + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; - setTagHelperProperty.OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name); - _builder.Add(setTagHelperProperty); } } @@ -1915,57 +1906,43 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim { foreach (var associatedDescriptor in associatedDescriptors) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch( - associatedDescriptor, - attributeName, - out var associatedAttributeDescriptor, - out var indexerMatch, - out var parameterMatch, - out var associatedAttributeParameterDescriptor)) + if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) { + if (!match.ExpectsBooleanValue) + { + // We do not allow minimized non-boolean bound attributes. + return; + } + // Directive attributes should start with '@' unless the descriptors are misconfigured. // In that case, we would have already logged an error. var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; IntermediateNode attributeNode; - if (parameterMatch && + if (match.IsParameterMatch && TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) { - var expectsBooleanValue = associatedAttributeParameterDescriptor.IsBooleanProperty; - if (!expectsBooleanValue) - { - // We do not allow minimized non-boolean bound attributes. - return; - } - attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() { AttributeName = actualAttributeName, AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), OriginalAttributeName = attributeName, - BoundAttributeParameter = associatedAttributeParameterDescriptor, - IsIndexerNameMatch = indexerMatch, + BoundAttributeParameter = match.Parameter, + IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = null, + Source = null }; } else { - var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attributeName); - if (!expectsBooleanValue) - { - // We do not allow minimized non-boolean bound attributes. - return; - } - attributeNode = new TagHelperDirectiveAttributeIntermediateNode() { AttributeName = actualAttributeName, OriginalAttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = indexerMatch, + IsIndexerNameMatch = match.IsIndexerMatch }; } @@ -1998,25 +1975,18 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta { foreach (var associatedDescriptor in associatedDescriptors) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch( - associatedDescriptor, - attributeName, - out var associatedAttributeDescriptor, - out var indexerMatch, - out _, - out _)) + if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) { - var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + var setTagHelperProperty = new TagHelperPropertyIntermediateNode { AttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = indexerMatch, + IsIndexerNameMatch = match.IsIndexerMatch, + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; - setTagHelperProperty.OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name); - _builder.Push(setTagHelperProperty); VisitAttributeValue(attributeValueNode); _builder.Pop(); @@ -2051,20 +2021,14 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec { foreach (var associatedDescriptor in associatedDescriptors) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch( - associatedDescriptor, - attributeName, - out var associatedAttributeDescriptor, - out var indexerMatch, - out var parameterMatch, - out var associatedAttributeParameterDescriptor)) + if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) { // Directive attributes should start with '@' unless the descriptors are misconfigured. // In that case, we would have already logged an error. var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; IntermediateNode attributeNode; - if (parameterMatch && + if (match.IsParameterMatch && TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) { attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() @@ -2072,8 +2036,8 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec AttributeName = actualAttributeName, AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), OriginalAttributeName = attributeName, - BoundAttributeParameter = associatedAttributeParameterDescriptor, - IsIndexerNameMatch = indexerMatch, + BoundAttributeParameter = match.Parameter, + IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) @@ -2085,10 +2049,10 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec { AttributeName = actualAttributeName, OriginalAttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = indexerMatch, + IsIndexerNameMatch = match.IsIndexerMatch, OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs index c731b873077..4dac8eec19c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs @@ -438,16 +438,11 @@ private static string GetPropertyType(string name, IEnumerable _isIndexerMatch ??= TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(Attribute, Name.AsSpan()); + + [MemberNotNullWhen(true, nameof(Parameter))] + public readonly bool IsParameterMatch => Parameter is not null; + + public bool ExpectsStringValue + { + get + { + if (IsParameterMatch) + { + return Parameter.IsStringProperty; + } + + return Attribute.IsStringProperty || (IsIndexerMatch && Attribute.IsIndexerStringProperty); + } + } + + public bool ExpectsBooleanValue + { + get + { + if (IsParameterMatch) + { + return Parameter.IsBooleanProperty; + } + + return Attribute.IsBooleanProperty || (IsIndexerMatch && Attribute.IsIndexerBooleanProperty); + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs index ab907a0833e..70369495e3c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs @@ -175,34 +175,20 @@ private static bool TryGetBoundAttributeParameter(string fullAttributeName, out return true; } - public static bool TryGetFirstBoundAttributeMatch( - TagHelperDescriptor descriptor, - string name, - out BoundAttributeDescriptor? boundAttribute, - out bool indexerMatch, - out bool parameterMatch, - out BoundAttributeParameterDescriptor? boundAttributeParameter) + public static bool TryGetFirstBoundAttributeMatch(TagHelperDescriptor descriptor, string name, out TagHelperAttributeMatch match) { - indexerMatch = false; - parameterMatch = false; - boundAttribute = null; - boundAttributeParameter = null; - if (descriptor == null || name.IsNullOrEmpty()) { + match = default; return false; } // First, check if we have a bound attribute descriptor that matches the parameter if it exists. foreach (var attribute in descriptor.BoundAttributes) { - boundAttributeParameter = GetSatisfyingBoundAttributeWithParameter(attribute, name); - - if (boundAttributeParameter != null) + if (GetSatisfyingBoundAttributeWithParameter(attribute, name) is { } parameter) { - boundAttribute = attribute; - indexerMatch = SatisfiesBoundAttributeIndexer(attribute, name.AsSpan()); - parameterMatch = true; + match = new(name, attribute, parameter); return true; } } @@ -213,13 +199,13 @@ public static bool TryGetFirstBoundAttributeMatch( { if (CanSatisfyBoundAttribute(name, attribute)) { - boundAttribute = attribute; - indexerMatch = SatisfiesBoundAttributeIndexer(attribute, name.AsSpan()); + match = new(name, attribute, parameter: null); return true; } } // No matches found. + match = default; return false; } From 351310778782f8b0661d27d218ce4aa0606a818f Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 11:52:49 -0700 Subject: [PATCH 04/13] Add TagHelperMatchingConventions.GetAttributeMatches helper Add helper method to collect matches and use it throughout DefaultRazorIntermediateNodeLoweringPhase to reduce LINQ-related allocations. --- ...faultRazorIntermediateNodeLoweringPhase.cs | 273 ++++++++---------- .../Language/TagHelperMatchingConventions.cs | 25 ++ 2 files changed, 149 insertions(+), 149 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 795d5b48bca..dffbf439c7d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1080,21 +1080,15 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe var element = node.FirstAncestorOrSelf(); var descriptors = element.TagHelperInfo.BindingResult.Descriptors; var attributeName = node.Name.GetContent(); - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); - if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); + + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var associatedDescriptor in associatedDescriptors) + foreach (var match in matches) { - var associatedAttributeDescriptor = associatedDescriptor.BoundAttributes.First(a => - { - return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, a); - }); - - var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attributeName); - - if (!expectsBooleanValue) + if (!match.ExpectsBooleanValue) { // We do not allow minimized non-boolean bound attributes. return; @@ -1103,10 +1097,10 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe var setTagHelperProperty = new TagHelperPropertyIntermediateNode() { AttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(associatedAttributeDescriptor, attributeName.AsSpan()), + IsIndexerNameMatch = match.IsIndexerMatch }; _builder.Add(setTagHelperProperty); @@ -1131,30 +1125,20 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta var attributeName = node.Name.GetContent(); var attributeValueNode = node.Value; - using var associatedDescriptorsWithSatisfyingAttribute = new PooledArrayBuilder<(TagHelperDescriptor, BoundAttributeDescriptor)>(); - foreach (var descriptor in descriptors) - { - foreach (var attributeDescriptor in descriptor.BoundAttributes) - { - if (TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor)) - { - associatedDescriptorsWithSatisfyingAttribute.Add((descriptor, attributeDescriptor)); - break; - } - } - } + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); - if (associatedDescriptorsWithSatisfyingAttribute.Any() && _renderedBoundAttributeNames.Add(attributeName)) + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var (_, associatedAttributeDescriptor) in associatedDescriptorsWithSatisfyingAttribute) + foreach (var match in matches) { var setTagHelperProperty = new TagHelperPropertyIntermediateNode() { AttributeName = attributeName, - BoundAttribute = associatedAttributeDescriptor, + BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(associatedAttributeDescriptor, attributeName.AsSpan()), + IsIndexerNameMatch = match.IsIndexerMatch }; _builder.Push(setTagHelperProperty); @@ -1846,33 +1830,31 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe var element = node.FirstAncestorOrSelf(); var descriptors = element.TagHelperInfo.BindingResult.Descriptors; var attributeName = node.Name.GetContent(); - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); - if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); + + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var associatedDescriptor in associatedDescriptors) + foreach (var match in matches) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) + if (!match.ExpectsBooleanValue) { - if (!match.ExpectsBooleanValue) - { - // We do not allow minimized non-boolean bound attributes. - return; - } + // We do not allow minimized non-boolean bound attributes. + return; + } - var setTagHelperProperty = new TagHelperPropertyIntermediateNode - { - AttributeName = attributeName, - BoundAttribute = match.Attribute, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = null, - IsIndexerNameMatch = match.IsIndexerMatch, - OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) - }; + var setTagHelperProperty = new TagHelperPropertyIntermediateNode + { + AttributeName = attributeName, + BoundAttribute = match.Attribute, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = null, + IsIndexerNameMatch = match.IsIndexerMatch, + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) + }; - _builder.Add(setTagHelperProperty); - } + _builder.Add(setTagHelperProperty); } } else @@ -1899,55 +1881,53 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim var element = node.FirstAncestorOrSelf(); var descriptors = element.TagHelperInfo.BindingResult.Descriptors; var attributeName = node.FullName; - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); - if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); + + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var associatedDescriptor in associatedDescriptors) + foreach (var match in matches) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) + if (!match.ExpectsBooleanValue) { - if (!match.ExpectsBooleanValue) - { - // We do not allow minimized non-boolean bound attributes. - return; - } + // We do not allow minimized non-boolean bound attributes. + return; + } - // Directive attributes should start with '@' unless the descriptors are misconfigured. - // In that case, we would have already logged an error. - var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; + // Directive attributes should start with '@' unless the descriptors are misconfigured. + // In that case, we would have already logged an error. + var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; - IntermediateNode attributeNode; - if (match.IsParameterMatch && - TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) + IntermediateNode attributeNode; + if (match.IsParameterMatch && + TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) + { + attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() { - attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() - { - AttributeName = actualAttributeName, - AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), - OriginalAttributeName = attributeName, - BoundAttributeParameter = match.Parameter, - IsIndexerNameMatch = match.IsIndexerMatch, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = null - }; - } - else + AttributeName = actualAttributeName, + AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), + OriginalAttributeName = attributeName, + BoundAttributeParameter = match.Parameter, + IsIndexerNameMatch = match.IsIndexerMatch, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = null + }; + } + else + { + attributeNode = new TagHelperDirectiveAttributeIntermediateNode() { - attributeNode = new TagHelperDirectiveAttributeIntermediateNode() - { - AttributeName = actualAttributeName, - OriginalAttributeName = attributeName, - BoundAttribute = match.Attribute, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = null, - IsIndexerNameMatch = match.IsIndexerMatch - }; - } - - _builder.Add(attributeNode); + AttributeName = actualAttributeName, + OriginalAttributeName = attributeName, + BoundAttribute = match.Attribute, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = null, + IsIndexerNameMatch = match.IsIndexerMatch + }; } + + _builder.Add(attributeNode); } } else @@ -1968,29 +1948,27 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta var descriptors = element.TagHelperInfo.BindingResult.Descriptors; var attributeName = node.Name.GetContent(); var attributeValueNode = node.Value; - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); - if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); + + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var associatedDescriptor in associatedDescriptors) + foreach (var match in matches) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) + var setTagHelperProperty = new TagHelperPropertyIntermediateNode { - var setTagHelperProperty = new TagHelperPropertyIntermediateNode - { - AttributeName = attributeName, - BoundAttribute = match.Attribute, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = match.IsIndexerMatch, - OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) - }; + AttributeName = attributeName, + BoundAttribute = match.Attribute, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = BuildSourceSpanFromNode(attributeValueNode), + IsIndexerNameMatch = match.IsIndexerMatch, + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) + }; - _builder.Push(setTagHelperProperty); - VisitAttributeValue(attributeValueNode); - _builder.Pop(); - } + _builder.Push(setTagHelperProperty); + VisitAttributeValue(attributeValueNode); + _builder.Pop(); } } else @@ -2014,53 +1992,50 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec var attributeName = node.FullName; var attributeValueNode = node.Value; - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); + using var matches = new PooledArrayBuilder(); + TagHelperMatchingConventions.GetAttributeMatches(descriptors, attributeName, ref matches.AsRef()); - if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { - foreach (var associatedDescriptor in associatedDescriptors) + foreach (var match in matches) { - if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(associatedDescriptor, attributeName, out var match)) - { - // Directive attributes should start with '@' unless the descriptors are misconfigured. - // In that case, we would have already logged an error. - var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; + // Directive attributes should start with '@' unless the descriptors are misconfigured. + // In that case, we would have already logged an error. + var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; - IntermediateNode attributeNode; - if (match.IsParameterMatch && - TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) + IntermediateNode attributeNode; + if (match.IsParameterMatch && + TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) + { + attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() { - attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() - { - AttributeName = actualAttributeName, - AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), - OriginalAttributeName = attributeName, - BoundAttributeParameter = match.Parameter, - IsIndexerNameMatch = match.IsIndexerMatch, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = BuildSourceSpanFromNode(attributeValueNode), - OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) - }; - } - else + AttributeName = actualAttributeName, + AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), + OriginalAttributeName = attributeName, + BoundAttributeParameter = match.Parameter, + IsIndexerNameMatch = match.IsIndexerMatch, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = BuildSourceSpanFromNode(attributeValueNode), + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) + }; + } + else + { + attributeNode = new TagHelperDirectiveAttributeIntermediateNode() { - attributeNode = new TagHelperDirectiveAttributeIntermediateNode() - { - AttributeName = actualAttributeName, - OriginalAttributeName = attributeName, - BoundAttribute = match.Attribute, - AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, - Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = match.IsIndexerMatch, - OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) - }; - } - - _builder.Push(attributeNode); - VisitAttributeValue(attributeValueNode); - _builder.Pop(); + AttributeName = actualAttributeName, + OriginalAttributeName = attributeName, + BoundAttribute = match.Attribute, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = BuildSourceSpanFromNode(attributeValueNode), + IsIndexerNameMatch = match.IsIndexerMatch, + OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) + }; } + + _builder.Push(attributeNode); + VisitAttributeValue(attributeValueNode); + _builder.Pop(); } } else diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs index 70369495e3c..3d6e23a4b48 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperMatchingConventions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language; @@ -175,6 +176,30 @@ private static bool TryGetBoundAttributeParameter(string fullAttributeName, out return true; } + /// + /// Gets all attribute matches from the specified tag helpers for the given attribute name. + /// + /// The collection of tag helper descriptors to search through. + /// The attribute name to match against. + /// A pooled array builder that will be populated with matching attribute descriptors. + /// + /// This method iterates through all provided tag helpers and attempts to find bound attribute matches + /// for the specified attribute name. Each successful match is added to the provided matches collection. + /// + public static void GetAttributeMatches( + ImmutableArray tagHelpers, + string name, + ref PooledArrayBuilder matches) + { + foreach (var tagHelper in tagHelpers) + { + if (TryGetFirstBoundAttributeMatch(tagHelper, name, out var match)) + { + matches.Add(match); + } + } + } + public static bool TryGetFirstBoundAttributeMatch(TagHelperDescriptor descriptor, string name, out TagHelperAttributeMatch match) { if (descriptor == null || name.IsNullOrEmpty()) From 8232fb838715d55e3d920e1660c164bf66835877 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 12:51:40 -0700 Subject: [PATCH 05/13] Fix #if..#endif definitions in SpanExtensions The polyfill extension method in SpanExtensions were causing ambiguities on .NET builds. --- ...irectiveAttributeCompletionItemProvider.cs | 2 +- .../SpanExtensions.cs | 20 +++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeCompletionItemProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeCompletionItemProvider.cs index ce867a17a2c..06b42fe17f2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeCompletionItemProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeCompletionItemProvider.cs @@ -128,7 +128,7 @@ internal static ImmutableArray GetAttributeCompletions( // Strip off the @ from the insertion text. This change is here to align the insertion text with the // completion hooks into VS and VSCode. Basically, completion triggers when `@` is typed so we don't // want to insert `@bind` because `@` already exists. - if (SpanExtensions.StartsWith(insertTextSpan, '@')) + if (insertTextSpan.StartsWith('@')) { insertTextSpan = insertTextSpan[1..]; } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/SpanExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/SpanExtensions.cs index 4a484c646ed..152829d3fc6 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/SpanExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/SpanExtensions.cs @@ -5,17 +5,17 @@ using System.Runtime.InteropServices; #endif +#if !NET9_0_OR_GREATER using System.Runtime.CompilerServices; +#endif namespace System; internal static class SpanExtensions { +#if !NET8_0_OR_GREATER public static unsafe void Replace(this ReadOnlySpan source, Span destination, char oldValue, char newValue) { -#if NET8_0_OR_GREATER - source.Replace(destination, oldValue, newValue); -#else var length = source.Length; if (length == 0) { @@ -35,14 +35,10 @@ public static unsafe void Replace(this ReadOnlySpan source, Span des var original = Unsafe.Add(ref src, i); Unsafe.Add(ref dst, i) = original == oldValue ? newValue : original; } -#endif } public static unsafe void Replace(this Span span, char oldValue, char newValue) { -#if NET8_0_OR_GREATER - span.Replace(oldValue, newValue); -#else var length = span.Length; if (length == 0) { @@ -60,9 +56,10 @@ public static unsafe void Replace(this Span span, char oldValue, char newV slot = newValue; } } -#endif } +#endif +#if !NET9_0_OR_GREATER /// /// Determines whether the specified value appears at the start of the span. /// @@ -71,11 +68,7 @@ public static unsafe void Replace(this Span span, char oldValue, char newV [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool StartsWith(this ReadOnlySpan span, T value) where T : IEquatable? => -#if NET9_0_OR_GREATER - MemoryExtensions.StartsWith(span, value); -#else span.Length != 0 && (span[0]?.Equals(value) ?? (object?)value is null); -#endif /// /// Determines whether the specified value appears at the end of the span. @@ -85,9 +78,6 @@ public static bool StartsWith(this ReadOnlySpan span, T value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWith(this ReadOnlySpan span, T value) where T : IEquatable? => -#if NET9_0_OR_GREATER - MemoryExtensions.EndsWith(span, value); -#else span.Length != 0 && (span[^1]?.Equals(value) ?? (object?)value is null); #endif } From 80b6e0db42cc7561833dec19cd463da55db9c995 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 13:00:56 -0700 Subject: [PATCH 06/13] Introduce DirectiveAttributeName helper struct Introduce DirectiveAttributeName helper struct that uses spans to defer creating strings until necessary. --- ...faultRazorIntermediateNodeLoweringPhase.cs | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index dffbf439c7d..f826c14b509 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1848,9 +1848,9 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe { AttributeName = attributeName, BoundAttribute = match.Attribute, + IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = match.IsIndexerMatch, OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; @@ -1887,6 +1887,8 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { + var directiveAttributeName = new DirectiveAttributeName(attributeName); + foreach (var match in matches) { if (!match.ExpectsBooleanValue) @@ -1895,37 +1897,26 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim return; } - // Directive attributes should start with '@' unless the descriptors are misconfigured. - // In that case, we would have already logged an error. - var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; - - IntermediateNode attributeNode; - if (match.IsParameterMatch && - TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) - { - attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() + IntermediateNode attributeNode = match.IsParameterMatch && directiveAttributeName.HasParameter + ? new TagHelperDirectiveAttributeParameterIntermediateNode() { - AttributeName = actualAttributeName, - AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), + AttributeName = directiveAttributeName.Text, + AttributeNameWithoutParameter = directiveAttributeName.TextWithoutParameter, OriginalAttributeName = attributeName, BoundAttributeParameter = match.Parameter, IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null - }; - } - else - { - attributeNode = new TagHelperDirectiveAttributeIntermediateNode() + } + : new TagHelperDirectiveAttributeIntermediateNode() { - AttributeName = actualAttributeName, + AttributeName = directiveAttributeName.Text, OriginalAttributeName = attributeName, BoundAttribute = match.Attribute, + IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = match.IsIndexerMatch }; - } _builder.Add(attributeNode); } @@ -1997,41 +1988,32 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec if (matches.Any() && _renderedBoundAttributeNames.Add(attributeName)) { + var directiveAttributeName = new DirectiveAttributeName(attributeName); + foreach (var match in matches) { - // Directive attributes should start with '@' unless the descriptors are misconfigured. - // In that case, we would have already logged an error. - var actualAttributeName = attributeName.StartsWith("@", StringComparison.Ordinal) ? attributeName.Substring(1) : attributeName; - - IntermediateNode attributeNode; - if (match.IsParameterMatch && - TagHelperMatchingConventions.TryGetBoundAttributeParameter(actualAttributeName, out var attributeNameWithoutParameter)) - { - attributeNode = new TagHelperDirectiveAttributeParameterIntermediateNode() + IntermediateNode attributeNode = match.IsParameterMatch && directiveAttributeName.HasParameter + ? new TagHelperDirectiveAttributeParameterIntermediateNode() { - AttributeName = actualAttributeName, - AttributeNameWithoutParameter = attributeNameWithoutParameter.ToString(), + AttributeName = directiveAttributeName.Text, + AttributeNameWithoutParameter = directiveAttributeName.TextWithoutParameter, OriginalAttributeName = attributeName, BoundAttributeParameter = match.Parameter, IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) - }; - } - else - { - attributeNode = new TagHelperDirectiveAttributeIntermediateNode() + } + : new TagHelperDirectiveAttributeIntermediateNode() { - AttributeName = actualAttributeName, + AttributeName = directiveAttributeName.Text, OriginalAttributeName = attributeName, BoundAttribute = match.Attribute, + IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = match.IsIndexerMatch, OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; - } _builder.Push(attributeNode); VisitAttributeValue(attributeValueNode); @@ -2146,6 +2128,33 @@ private void Combine(HtmlContentIntermediateNode node, SyntaxNode item) } } + private ref struct DirectiveAttributeName(ReadOnlySpan original) + { + // Directive attributes should start with '@' unless the descriptors are misconfigured. + // In that case, we would have already logged an error. + public readonly ReadOnlySpan Span = original.StartsWith('@') ? original[1..] : original; + + private bool? _hasParameter; + + public string Text => field ??= Span.ToString(); + + public bool HasParameter => _hasParameter ??= Span.IndexOf(':') >= 0; + + public string TextWithoutParameter + { + get + { + return field ??= GetWithoutParameter(Span); + + static string GetWithoutParameter(ReadOnlySpan span) + { + var index = span.IndexOf(':'); + return index >= 0 ? span[..index].ToString() : span.ToString(); + } + } + } + } + private class ComponentImportFileKindVisitor : LoweringVisitor { public ComponentImportFileKindVisitor( From 4399da3e432a10847221a06b4adb668cceef165e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 13:50:38 -0700 Subject: [PATCH 07/13] Clean up TagHelperDirectiveAttributeParameterIntermediateNode - Enable nullability - Make Children property lazy - Remove unnecessary argument-null checks - Add constructor that takes an TagHelperAttributeMatch and verifies that its a parameter match. - Change IsIndexerNameMatch and BoundAttributeParameter properties to computed properties that return values from TagHelperAttributeMatch. - Change other properties to init-only - Make AttributeName, AttributeNameWithoutParameter, OriginalAttributeName, and AttributeStructure as required. --- .../ComponentEventHandlerLoweringPass.cs | 2 +- ...faultRazorIntermediateNodeLoweringPhase.cs | 8 +--- ...ctiveAttributeParameterIntermediateNode.cs | 42 +++++++++---------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentEventHandlerLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentEventHandlerLoweringPass.cs index 28dbff3e4f4..59ac54adf8d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentEventHandlerLoweringPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentEventHandlerLoweringPass.cs @@ -136,7 +136,7 @@ private static void ProcessDuplicates(IntermediateNode parent) var parameterDuplicates = parent.Children .OfType() - .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false) + .Where(p => p.TagHelper.IsEventHandlerTagHelper()) .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index f826c14b509..4e65216a377 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1898,13 +1898,11 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim } IntermediateNode attributeNode = match.IsParameterMatch && directiveAttributeName.HasParameter - ? new TagHelperDirectiveAttributeParameterIntermediateNode() + ? new TagHelperDirectiveAttributeParameterIntermediateNode(match) { AttributeName = directiveAttributeName.Text, AttributeNameWithoutParameter = directiveAttributeName.TextWithoutParameter, OriginalAttributeName = attributeName, - BoundAttributeParameter = match.Parameter, - IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null } @@ -1993,13 +1991,11 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec foreach (var match in matches) { IntermediateNode attributeNode = match.IsParameterMatch && directiveAttributeName.HasParameter - ? new TagHelperDirectiveAttributeParameterIntermediateNode() + ? new TagHelperDirectiveAttributeParameterIntermediateNode(match) { AttributeName = directiveAttributeName.Text, AttributeNameWithoutParameter = directiveAttributeName.TextWithoutParameter, OriginalAttributeName = attributeName, - BoundAttributeParameter = match.Parameter, - IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs index ae0c4f1f8fe..34f36bd217a 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs @@ -1,44 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; +using System.Diagnostics; namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperDirectiveAttributeParameterIntermediateNode : IntermediateNode { - public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); - - public string AttributeName { get; set; } + private readonly TagHelperAttributeMatch _match; - public string AttributeNameWithoutParameter { get; set; } + public required string AttributeName { get; init; } + public required string AttributeNameWithoutParameter { get; init; } - public string OriginalAttributeName { get; set; } + public required string OriginalAttributeName { get; init; } + public SourceSpan? OriginalAttributeSpan { get; init; } - public AttributeStructure AttributeStructure { get; set; } + public required AttributeStructure AttributeStructure { get; init; } - public BoundAttributeParameterDescriptor BoundAttributeParameter { get; set; } + public bool IsIndexerNameMatch => _match.IsIndexerMatch; + public BoundAttributeParameterDescriptor BoundAttributeParameter => _match.Parameter.AssumeNotNull(); public BoundAttributeDescriptor BoundAttribute => BoundAttributeParameter.Parent; - public TagHelperDescriptor TagHelper => BoundAttribute.Parent; - public bool IsIndexerNameMatch { get; set; } - - public SourceSpan? OriginalAttributeSpan { get; set; } + public override IntermediateNodeCollection Children { get => field ??= []; } - public override void Accept(IntermediateNodeVisitor visitor) + internal TagHelperDirectiveAttributeParameterIntermediateNode(TagHelperAttributeMatch match) { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } + Debug.Assert(match.IsParameterMatch); - visitor.VisitTagHelperDirectiveAttributeParameter(this); + _match = match; } + public override void Accept(IntermediateNodeVisitor visitor) + => visitor.VisitTagHelperDirectiveAttributeParameter(this); + public override void FormatNode(IntermediateNodeFormatter formatter) { formatter.WriteContent(AttributeName); @@ -46,8 +42,8 @@ public override void FormatNode(IntermediateNodeFormatter formatter) formatter.WriteProperty(nameof(AttributeName), AttributeName); formatter.WriteProperty(nameof(OriginalAttributeName), OriginalAttributeName); formatter.WriteProperty(nameof(AttributeStructure), AttributeStructure.ToString()); - formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute?.DisplayName); - formatter.WriteProperty(nameof(BoundAttributeParameter), BoundAttributeParameter?.DisplayName); - formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); + formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute.DisplayName); + formatter.WriteProperty(nameof(BoundAttributeParameter), BoundAttributeParameter.DisplayName); + formatter.WriteProperty(nameof(TagHelper), TagHelper.DisplayName); } } From b4c358d4584590a3b3b8bceaab04330ecee2429e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 14:03:26 -0700 Subject: [PATCH 08/13] Clean up TagHelperDirectiveAttributeIntermediateNode - Enable nullability - Make Children property lazy - Remove unnecessary argument-null checks - Add constructor that takes an TagHelperAttributeMatch - Change IsIndexerNameMatch and BoundAttribute properties to computed properties that return values from TagHelperAttributeMatch. - Change other properties to init-only - Make AttributeName, OriginalAttributeName, and AttributeStructure as required. --- ...faultRazorIntermediateNodeLoweringPhase.cs | 8 ++--- ...elperDirectiveAttributeIntermediateNode.cs | 33 +++++++++---------- ...ctiveAttributeParameterIntermediateNode.cs | 4 --- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 4e65216a377..2aeccca6d56 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1906,12 +1906,10 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null } - : new TagHelperDirectiveAttributeIntermediateNode() + : new TagHelperDirectiveAttributeIntermediateNode(match) { AttributeName = directiveAttributeName.Text, OriginalAttributeName = attributeName, - BoundAttribute = match.Attribute, - IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, }; @@ -2000,12 +1998,10 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec Source = BuildSourceSpanFromNode(attributeValueNode), OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) } - : new TagHelperDirectiveAttributeIntermediateNode() + : new TagHelperDirectiveAttributeIntermediateNode(match) { AttributeName = directiveAttributeName.Text, OriginalAttributeName = attributeName, - BoundAttribute = match.Attribute, - IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs index 156ac499888..28c59414477 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs @@ -1,40 +1,37 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; +using System.Diagnostics; namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperDirectiveAttributeIntermediateNode : IntermediateNode { - public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); - - public string AttributeName { get; set; } - - public string OriginalAttributeName { get; set; } + private readonly TagHelperAttributeMatch _match; - public SourceSpan? OriginalAttributeSpan { get; set; } + public required string AttributeName { get; init; } + public required string OriginalAttributeName { get; init; } - public AttributeStructure AttributeStructure { get; set; } + public required AttributeStructure AttributeStructure { get; init; } + public SourceSpan? OriginalAttributeSpan { get; init; } - public BoundAttributeDescriptor BoundAttribute { get; set; } + public bool IsIndexerNameMatch => _match.IsIndexerMatch; + public BoundAttributeDescriptor BoundAttribute => _match.Attribute; public TagHelperDescriptor TagHelper => BoundAttribute.Parent; - public bool IsIndexerNameMatch { get; set; } + public override IntermediateNodeCollection Children { get => field ??= []; } - public override void Accept(IntermediateNodeVisitor visitor) + internal TagHelperDirectiveAttributeIntermediateNode(TagHelperAttributeMatch match) { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } + Debug.Assert(!match.IsParameterMatch); - visitor.VisitTagHelperDirectiveAttribute(this); + _match = match; } + public override void Accept(IntermediateNodeVisitor visitor) + => visitor.VisitTagHelperDirectiveAttribute(this); + public override void FormatNode(IntermediateNodeFormatter formatter) { formatter.WriteContent(AttributeName); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs index 34f36bd217a..cd7496f84da 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeParameterIntermediateNode.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; - namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperDirectiveAttributeParameterIntermediateNode : IntermediateNode @@ -27,8 +25,6 @@ public sealed class TagHelperDirectiveAttributeParameterIntermediateNode : Inter internal TagHelperDirectiveAttributeParameterIntermediateNode(TagHelperAttributeMatch match) { - Debug.Assert(match.IsParameterMatch); - _match = match; } From e4ff36881957dcf87e4b701ad61d91918d85e25c Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 14:30:22 -0700 Subject: [PATCH 09/13] Clean up TagHelperPropertyIntermediateNode - Enable nullability - Make Children property lazy - Remove unnecessary argument-null checks - Add constructor that takes an TagHelperAttributeMatch - Change IsIndexerNameMatch and BoundAttribute properties to computed properties that return values from TagHelperAttributeMatch. - Change other properties to init-only - Make AttributeName, and AttributeStructure as required. --- .../test/InstrumentationPassTest.cs | 6 +- ...faultRazorIntermediateNodeLoweringPhase.cs | 16 ++--- .../ComponentTypeArgumentIntermediateNode.cs | 68 ++++++------------- ...elperDirectiveAttributeIntermediateNode.cs | 4 +- .../TagHelperPropertyIntermediateNode.cs | 34 ++++------ .../test/GenericTypeNameRewriterTest.cs | 11 ++- 6 files changed, 55 insertions(+), 84 deletions(-) diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs index 098b5972534..7eaa025d19b 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs @@ -190,7 +190,11 @@ public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperProperty() builder.Push(new TagHelperIntermediateNode()); - builder.Push(new TagHelperPropertyIntermediateNode()); + builder.Push(new TagHelperPropertyIntermediateNode(match: default) + { + AttributeName = "Test", + AttributeStructure = 0 + }); builder.Push(new CSharpExpressionIntermediateNode() { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 2aeccca6d56..9e668ae9e1d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -1094,13 +1094,11 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe return; } - var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + var setTagHelperProperty = new TagHelperPropertyIntermediateNode(match) { AttributeName = attributeName, - BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, - IsIndexerNameMatch = match.IsIndexerMatch }; _builder.Add(setTagHelperProperty); @@ -1132,13 +1130,11 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta { foreach (var match in matches) { - var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + var setTagHelperProperty = new TagHelperPropertyIntermediateNode(match) { AttributeName = attributeName, - BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = match.IsIndexerMatch }; _builder.Push(setTagHelperProperty); @@ -1844,11 +1840,9 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe return; } - var setTagHelperProperty = new TagHelperPropertyIntermediateNode + var setTagHelperProperty = new TagHelperPropertyIntermediateNode(match) { AttributeName = attributeName, - BoundAttribute = match.Attribute, - IsIndexerNameMatch = match.IsIndexerMatch, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = null, OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) @@ -1943,13 +1937,11 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta { foreach (var match in matches) { - var setTagHelperProperty = new TagHelperPropertyIntermediateNode + var setTagHelperProperty = new TagHelperPropertyIntermediateNode(match) { AttributeName = attributeName, - BoundAttribute = match.Attribute, AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = match.IsIndexerMatch, OriginalAttributeSpan = BuildSourceSpanFromNode(node.Name) }; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs index 7d7e2a0605c..36af93749ed 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ComponentTypeArgumentIntermediateNode.cs @@ -1,67 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; -using System.Diagnostics; - namespace Microsoft.AspNetCore.Razor.Language.Intermediate; -public sealed class ComponentTypeArgumentIntermediateNode : IntermediateNode +public sealed class ComponentTypeArgumentIntermediateNode( + BoundAttributeDescriptor boundAttribute, CSharpIntermediateToken value) : IntermediateNode { - public ComponentTypeArgumentIntermediateNode(TagHelperPropertyIntermediateNode propertyNode) - { - if (propertyNode == null) - { - throw new ArgumentNullException(nameof(propertyNode)); - } - - BoundAttribute = propertyNode.BoundAttribute; - Source = propertyNode.Source; - - Debug.Assert(propertyNode.Children.Count == 1); - Value = propertyNode.Children[0] switch - { - CSharpIntermediateToken t => t, - CSharpExpressionIntermediateNode c => (CSharpIntermediateToken)c.Children[0], // TODO: can we break this in error cases? - _ => Assumed.Unreachable() - }; - Children = [Value]; - - AddDiagnosticsFromNode(propertyNode); - } - - public override IntermediateNodeCollection Children { get; } - - public BoundAttributeDescriptor BoundAttribute { get; set; } + public BoundAttributeDescriptor BoundAttribute { get; } = boundAttribute; + public TagHelperDescriptor TagHelper => BoundAttribute.Parent; public string TypeParameterName => BoundAttribute.Name; - public TagHelperDescriptor TagHelper => BoundAttribute.Parent; + public CSharpIntermediateToken Value { get; } = value; - public CSharpIntermediateToken Value { get; set; } + public override IntermediateNodeCollection Children { get; } = [value]; - public override void Accept(IntermediateNodeVisitor visitor) + public ComponentTypeArgumentIntermediateNode(TagHelperPropertyIntermediateNode node) + : this(node.BoundAttribute, GetValue(node)) { - if (visitor == null) + Source = node.Source; + AddDiagnosticsFromNode(node); + } + + private static CSharpIntermediateToken GetValue(TagHelperPropertyIntermediateNode node) + => node.Children switch { - throw new ArgumentNullException(nameof(visitor)); - } + [CSharpIntermediateToken t] => t, + [CSharpExpressionIntermediateNode { Children: [CSharpIntermediateToken t] }] => t, + _ => Assumed.Unreachable() + }; - visitor.VisitComponentTypeArgument(this); - } + public override void Accept(IntermediateNodeVisitor visitor) + => visitor.VisitComponentTypeArgument(this); public override void FormatNode(IntermediateNodeFormatter formatter) { - if (formatter == null) - { - throw new ArgumentNullException(nameof(formatter)); - } - formatter.WriteContent(TypeParameterName); - formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute?.DisplayName); - formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); + formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute.DisplayName); + formatter.WriteProperty(nameof(TagHelper), TagHelper.DisplayName); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs index 28c59414477..f7325d0acbe 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperDirectiveAttributeIntermediateNode.cs @@ -39,8 +39,8 @@ public override void FormatNode(IntermediateNodeFormatter formatter) formatter.WriteProperty(nameof(AttributeName), AttributeName); formatter.WriteProperty(nameof(OriginalAttributeName), OriginalAttributeName); formatter.WriteProperty(nameof(AttributeStructure), AttributeStructure.ToString()); - formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute?.DisplayName); + formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute.DisplayName); formatter.WriteProperty(nameof(IsIndexerNameMatch), IsIndexerNameMatch.ToString()); - formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); + formatter.WriteProperty(nameof(TagHelper), TagHelper.DisplayName); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs index f3bd028bbdc..b5a23aa1b0d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperPropertyIntermediateNode.cs @@ -1,46 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; - namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperPropertyIntermediateNode : IntermediateNode { - public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); + private readonly TagHelperAttributeMatch _match; - public string AttributeName { get; set; } + public required string AttributeName { get; init; } + public required AttributeStructure AttributeStructure { get; init; } - public AttributeStructure AttributeStructure { get; set; } + public SourceSpan? OriginalAttributeSpan { get; init; } - public BoundAttributeDescriptor BoundAttribute { get; set; } + public bool IsIndexerNameMatch => _match.IsIndexerMatch; + public BoundAttributeDescriptor BoundAttribute => _match.Attribute; public TagHelperDescriptor TagHelper => BoundAttribute.Parent; - public bool IsIndexerNameMatch { get; set; } + public override IntermediateNodeCollection Children { get => field ??= []; } - public SourceSpan? OriginalAttributeSpan { get; set; } - - public override void Accept(IntermediateNodeVisitor visitor) + internal TagHelperPropertyIntermediateNode(TagHelperAttributeMatch match) { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } - - visitor.VisitTagHelperProperty(this); + _match = match; } + public override void Accept(IntermediateNodeVisitor visitor) + => visitor.VisitTagHelperProperty(this); + public override void FormatNode(IntermediateNodeFormatter formatter) { formatter.WriteContent(AttributeName); formatter.WriteProperty(nameof(AttributeName), AttributeName); formatter.WriteProperty(nameof(AttributeStructure), AttributeStructure.ToString()); - formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute?.DisplayName); + formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute.DisplayName); formatter.WriteProperty(nameof(IsIndexerNameMatch), IsIndexerNameMatch.ToString()); - formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); + formatter.WriteProperty(nameof(TagHelper), TagHelper.DisplayName); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs index 967296692d2..7679a3d66f0 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs @@ -69,9 +69,9 @@ public void GenericTypeNameRewriter_CanReplaceTypeParametersWithTypeArguments(st // Arrange var visitor = new GenericTypeNameRewriter(new Dictionary() { - { "TItem1", new ComponentTypeArgumentIntermediateNode(new() { Children = { IntermediateNodeFactory.CSharpToken("Type1") } })}, - { "TItem2", new ComponentTypeArgumentIntermediateNode(new() { Children = { IntermediateNodeFactory.CSharpToken("Type2") } })}, - { "TItem3", new ComponentTypeArgumentIntermediateNode(new() { Children = { IntermediateNodeFactory.CSharpToken(null!) } })}, + { "TItem1", Create("Type1") }, + { "TItem2", Create("Type2") }, + { "TItem3", Create(null) }, }); // Act @@ -79,5 +79,10 @@ public void GenericTypeNameRewriter_CanReplaceTypeParametersWithTypeArguments(st // Assert Assert.Equal(expected, actual.ToString()); + + static ComponentTypeArgumentIntermediateNode Create(string? typeName) + { + return new(boundAttribute: null!, IntermediateNodeFactory.CSharpToken(typeName!)); + } } } From 5547cc9deee0852bae03f634af7ac1609f40e237 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 14:35:08 -0700 Subject: [PATCH 10/13] Clean up TagHelperHtmlAttributeIntermediateNode - Enable nullability - Make Children property lazy - Remove unnecessary argument-null checks - Change properties to init-only - Mark AttributeName, and AttributeStructure as required. --- .../test/InstrumentationPassTest.cs | 6 +++++- .../TagHelperHtmlAttributeIntermediateNode.cs | 20 ++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs index 7eaa025d19b..adcd8dd0c89 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs @@ -149,7 +149,11 @@ public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperAttribute() builder.Push(new TagHelperIntermediateNode()); - builder.Push(new TagHelperHtmlAttributeIntermediateNode()); + builder.Push(new TagHelperHtmlAttributeIntermediateNode() + { + AttributeName = "Test", + AttributeStructure = 0 + }); builder.Push(new CSharpExpressionIntermediateNode() { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperHtmlAttributeIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperHtmlAttributeIntermediateNode.cs index cc6105f4191..0b72b22c21e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperHtmlAttributeIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperHtmlAttributeIntermediateNode.cs @@ -1,29 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; - namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperHtmlAttributeIntermediateNode : IntermediateNode { - public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); - - public string AttributeName { get; set; } + public required string AttributeName { get; init; } + public required AttributeStructure AttributeStructure { get; init; } - public AttributeStructure AttributeStructure { get; set; } + public override IntermediateNodeCollection Children { get => field ??= []; } public override void Accept(IntermediateNodeVisitor visitor) - { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } - - visitor.VisitTagHelperHtmlAttribute(this); - } + => visitor.VisitTagHelperHtmlAttribute(this); public override void FormatNode(IntermediateNodeFormatter formatter) { From 3edf46fd5941bd2555b95d26134751b0e929c062 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 14:48:11 -0700 Subject: [PATCH 11/13] Clean up TagHelperIntermediateNode - Enable nullability - Make Children property lazy - Remove unnecessary argument-null checks - Change properties to init-only - Mark TagMode, and TagName as required. --- .../test/InstrumentationPassTest.cs | 20 ++- .../DefaultTagHelperTargetExtensionTest.cs | 161 +++++++++++++++--- ...reallocatedAttributeTargetExtensionTest.cs | 28 ++- .../Intermediate/TagHelperIntermediateNode.cs | 38 +---- 4 files changed, 183 insertions(+), 64 deletions(-) diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs index adcd8dd0c89..f59bfe096bc 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/InstrumentationPassTest.cs @@ -147,7 +147,11 @@ public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperAttribute() var builder = IntermediateNodeBuilder.Create(documentNode); - builder.Push(new TagHelperIntermediateNode()); + builder.Push(new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }); builder.Push(new TagHelperHtmlAttributeIntermediateNode() { @@ -192,7 +196,11 @@ public void InstrumentationPass_SkipsCSharpExpression_InsideTagHelperProperty() var builder = IntermediateNodeBuilder.Create(documentNode); - builder.Push(new TagHelperIntermediateNode()); + builder.Push(new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }); builder.Push(new TagHelperPropertyIntermediateNode(match: default) { @@ -239,6 +247,8 @@ public void InstrumentationPass_InstrumentsTagHelper() builder.Add(new TagHelperIntermediateNode() { + TagMode = 0, + TagName = "Test", Source = CreateSource(3) }); @@ -262,7 +272,11 @@ public void InstrumentationPass_SkipsTagHelper_WithoutLocation() var builder = IntermediateNodeBuilder.Create(documentNode); - builder.Push(new TagHelperIntermediateNode()); + builder.Push(new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }); // Act ProjectEngine.ExecutePass(codeDocument, documentNode); diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs index 79587345197..286b781fe76 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/DefaultTagHelperTargetExtensionTest.cs @@ -61,7 +61,12 @@ public void WriteTagHelperBody_DesignTime_WritesChildren() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperBodyIntermediateNode() { Children = @@ -91,7 +96,12 @@ public void WriteTagHelperBody_Runtime_RendersCorrectly_UsesTagNameAndModeFromCo var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperBodyIntermediateNode() { Children = @@ -126,7 +136,12 @@ public void WriteTagHelperCreate_DesignTime_RendersCorrectly_UsesSpecifiedTagHel var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperCreateIntermediateNode() { FieldName = "__TestNamespace_MyTagHelper", @@ -154,7 +169,12 @@ public void WriteTagHelperCreate_Runtime_RendersCorrectly_UsesSpecifiedTagHelper var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperCreateIntermediateNode() { FieldName = "__TestNamespace_MyTagHelper", @@ -183,7 +203,12 @@ public void WriteTagHelperExecute_DesignTime_RendersAsyncCode() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperExecuteIntermediateNode(); tagHelperNode.Children.Add(node); Push(context, tagHelperNode); @@ -207,7 +232,12 @@ public void WriteTagHelperExecute_Runtime_RendersCorrectly() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperExecuteIntermediateNode(); tagHelperNode.Children.Add(node); Push(context, tagHelperNode); @@ -237,7 +267,12 @@ public void WriteTagHelperHtmlAttribute_DesignTime_WritesNothing() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -277,7 +312,12 @@ public void WriteTagHelperHtmlAttribute_Runtime_SimpleAttribute_RendersCorrectly var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -315,7 +355,12 @@ public void WriteTagHelperHtmlAttribute_Runtime_DynamicAttribute_RendersCorrectl var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperHtmlAttributeIntermediateNode() { AttributeName = "name", @@ -425,7 +470,12 @@ public void WriteTagHelperProperty_DesignTime_StringProperty_HtmlContent_Renders var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -465,7 +515,12 @@ public void WriteTagHelperProperty_DesignTime_StringProperty_NonHtmlContent_Rend var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -505,7 +560,12 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -554,7 +614,12 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_SecondUseOfAttri var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node1 = new DefaultTagHelperPropertyIntermediateNode() { // We only look at the attribute name here. @@ -595,7 +660,12 @@ public void WriteTagHelperProperty_DesignTime_NonStringProperty_RendersCorrectly var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -634,7 +704,12 @@ public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly( var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -681,7 +756,12 @@ public void WriteTagHelperProperty_DesignTime_NonStringIndexer_RendersCorrectly_ var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateDesignTime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -720,7 +800,12 @@ public void WriteTagHelperProperty_Runtime_StringProperty_HtmlContent_RendersCor var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -765,7 +850,12 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_RendersCorrectly() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -815,7 +905,12 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_SecondUseOfAttribut var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node1 = new DefaultTagHelperPropertyIntermediateNode() { // We only look at the attribute name here. @@ -856,7 +951,12 @@ public void WriteTagHelperProperty_Runtime_NonStringProperty_RendersCorrectly_Wi var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "bound", @@ -896,7 +996,12 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_RendersCorrectly() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", @@ -948,7 +1053,12 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_MultipleValues() var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node1 = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-first", @@ -1013,7 +1123,12 @@ public void WriteTagHelperProperty_Runtime_NonStringIndexer_RendersCorrectly_Wit var extension = new DefaultTagHelperTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new DefaultTagHelperPropertyIntermediateNode() { AttributeName = "foo-bound", diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs index efac52ca6f8..e709ea0a939 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Extensions/PreallocatedAttributeTargetExtensionTest.cs @@ -73,7 +73,12 @@ public void WriteTagHelperHtmlAttribute_RendersCorrectly() var extension = new PreallocatedAttributeTargetExtension(); using var context = TestCodeRenderingContext.CreateRuntime(); - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new PreallocatedTagHelperHtmlAttributeIntermediateNode() { VariableName = "_tagHelper1" @@ -137,7 +142,12 @@ public void WriteTagHelperProperty_RendersCorrectly() var attribute = tagHelper.BoundAttributes[0]; - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = attribute.Name, @@ -181,7 +191,12 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_RendersCorrec var attribute = tagHelper.BoundAttributes[0]; - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = "pre-Foo", @@ -230,7 +245,12 @@ public void WriteSetPreallocatedTagHelperProperty_IndexerAttribute_MultipleValue var attribute = tagHelper.BoundAttributes[0]; - var tagHelperNode = new TagHelperIntermediateNode(); + var tagHelperNode = new TagHelperIntermediateNode() + { + TagMode = 0, + TagName = "Test" + }; + var node1 = new PreallocatedTagHelperPropertyIntermediateNode() { AttributeName = "pre-Bar", diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs index ac550fb7092..f957e6a0869 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/TagHelperIntermediateNode.cs @@ -1,10 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -12,41 +8,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate; public sealed class TagHelperIntermediateNode : IntermediateNode { - public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection(); - - public TagMode TagMode { get; set; } - - public string TagName { get; set; } + public required TagMode TagMode { get; init; } + public required string TagName { get; init; } public ImmutableArray TagHelpers { get; init => field = value.NullToEmpty(); } = []; - public TagHelperBodyIntermediateNode Body => Children.OfType().SingleOrDefault(); - - public IEnumerable Properties - { - get - { - return Children.OfType(); - } - } - - public IEnumerable HtmlAttributes - { - get - { - return Children.OfType(); - } - } + public override IntermediateNodeCollection Children { get => field ??= []; } public override void Accept(IntermediateNodeVisitor visitor) - { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } - - visitor.VisitTagHelper(this); - } + => visitor.VisitTagHelper(this); public override void FormatNode(IntermediateNodeFormatter formatter) { From f4e35311726ae1949b85555951c44632bdf90e82 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 15:31:33 -0700 Subject: [PATCH 12/13] CR Feedback: Set variable inside loop for consistency --- .../src/Language/Legacy/TagHelperBlockRewriter.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs index 4dac8eec19c..ecd1fb3de02 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs @@ -460,6 +460,7 @@ private static TryParseResult CreateTryParseResult( var isBoundBooleanAttribute = false; var isMissingDictionaryKey = false; var isDirectiveAttribute = false; + var isDuplicateAttribute = false; foreach (var descriptor in descriptors) { @@ -476,17 +477,16 @@ private static TryParseResult CreateTryParseResult( isDirectiveAttribute = match.Attribute.IsDirectiveAttribute; + if (!processedBoundAttributeNames.Add(name)) + { + // A bound attribute with the same name has already been processed. + isDuplicateAttribute = true; + } + break; } } - var isDuplicateAttribute = false; - if (isBoundAttribute && !processedBoundAttributeNames.Add(name)) - { - // A bound attribute with the same name has already been processed. - isDuplicateAttribute = true; - } - return new TryParseResult { AttributeName = name, From 4968ee91a3f1b9e585cd60f1ea2617e35ddc853e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 21 Aug 2025 15:41:47 -0700 Subject: [PATCH 13/13] CR Feedback: Avoid extra ReadOnlySpan.ToString() allocations --- ...faultRazorIntermediateNodeLoweringPhase.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 9e668ae9e1d..02829ac8d2c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -2112,31 +2112,20 @@ private void Combine(HtmlContentIntermediateNode node, SyntaxNode item) } } - private ref struct DirectiveAttributeName(ReadOnlySpan original) + private ref struct DirectiveAttributeName(string original) { // Directive attributes should start with '@' unless the descriptors are misconfigured. // In that case, we would have already logged an error. - public readonly ReadOnlySpan Span = original.StartsWith('@') ? original[1..] : original; + public readonly ReadOnlySpan Span = original.StartsWith('@') ? original.AsSpan()[1..] : original; - private bool? _hasParameter; + public string Text => field ??= (Span.Length < original.Length ? Span.ToString() : original); - public string Text => field ??= Span.ToString(); + private bool? _hasParameter; public bool HasParameter => _hasParameter ??= Span.IndexOf(':') >= 0; public string TextWithoutParameter - { - get - { - return field ??= GetWithoutParameter(Span); - - static string GetWithoutParameter(ReadOnlySpan span) - { - var index = span.IndexOf(':'); - return index >= 0 ? span[..index].ToString() : span.ToString(); - } - } - } + => field ??= Span.IndexOf(':') is int index && index >= 0 ? Span[..index].ToString() : Text; } private class ComponentImportFileKindVisitor : LoweringVisitor