Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.AspNetCore.Razor.Language.Components;

Expand Down Expand Up @@ -529,11 +531,15 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE
}
else
{
using var _ = ArrayBuilderPool<IntermediateNode>.GetPooledObject(out var builder);

var valuePropertyName = valueAttribute?.GetPropertyName();

ComponentAttributeIntermediateNode valueNode = node != null ? new ComponentAttributeIntermediateNode(node) : new ComponentAttributeIntermediateNode(getNode);
valueNode.Annotations[ComponentMetadata.Common.OriginalAttributeName] = bindEntry.GetOriginalAttributeName();
valueNode.AttributeName = valueAttributeName;
valueNode.BoundAttribute = valueAttribute; // Might be null if it doesn't match a component attribute
valueNode.PropertyName = valueAttribute?.GetPropertyName();
valueNode.PropertyName = valuePropertyName;
valueNode.TagHelper = valueAttribute == null ? null : bindEntry.GetEffectiveNodeTagHelperDescriptor();
valueNode.TypeName = valueAttribute?.IsWeaklyTyped() == false ? valueAttribute.TypeName : null;

Expand All @@ -544,6 +550,8 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE
valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
}

builder.Add(valueNode);

var changeNode = node != null ? new ComponentAttributeIntermediateNode(node) : new ComponentAttributeIntermediateNode(getNode);
changeNode.Annotations[ComponentMetadata.Common.OriginalAttributeName] = bindEntry.GetOriginalAttributeName();
changeNode.AttributeName = changeAttributeName;
Expand All @@ -559,12 +567,13 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE
changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
}

builder.Add(changeNode);

// Finally, also emit a node for the "Expression" attribute, but only if the target
// component is defined to accept one
ComponentAttributeIntermediateNode expressionNode = null;
if (expressionAttribute != null)
{
expressionNode = node != null ? new ComponentAttributeIntermediateNode(node) : new ComponentAttributeIntermediateNode(getNode);
var expressionNode = node != null ? new ComponentAttributeIntermediateNode(node) : new ComponentAttributeIntermediateNode(getNode);
expressionNode.Annotations[ComponentMetadata.Common.OriginalAttributeName] = bindEntry.GetOriginalAttributeName();
expressionNode.AttributeName = expressionAttributeName;
expressionNode.BoundAttribute = expressionAttribute;
Expand All @@ -579,12 +588,34 @@ private IntermediateNode[] RewriteUsage(IntermediateNode parent, BindEntry bindE
Content = $"() => {original.Content}",
Kind = TokenKind.CSharp
});

builder.Add(expressionNode);
}

return expressionNode == null
? new[] { valueNode, changeNode }
: new[] { valueNode, changeNode, expressionNode };
// We don't need to generate any runtime code for these attributes normally, as they're handled by the above nodes,
// but in order for IDE scenarios around component attributes to work we need to generate a little bit of design
// time code, so we create design time specific nodes with minimal information in order to do so.
TryAddDesignTimePropertyAccessHelperNode(builder, bindEntry.BindSetNode, valuePropertyName);
TryAddDesignTimePropertyAccessHelperNode(builder, bindEntry.BindEventNode, valuePropertyName);
TryAddDesignTimePropertyAccessHelperNode(builder, bindEntry.BindAfterNode, valuePropertyName);

return builder.ToArray();
}
}

private static void TryAddDesignTimePropertyAccessHelperNode(ImmutableArray<IntermediateNode>.Builder builder, TagHelperDirectiveAttributeParameterIntermediateNode intermediateNode, string propertyName)
{
if (intermediateNode is null || propertyName is null)
{
return;
}

var helperNode = new ComponentAttributeIntermediateNode(intermediateNode);
helperNode.Annotations[ComponentMetadata.Common.OriginalAttributeName] = intermediateNode.OriginalAttributeName;
helperNode.Annotations[ComponentMetadata.Common.IsDesignTimePropertyAccessHelper] = bool.TrueString;
helperNode.PropertyName = propertyName;

builder.Add(helperNode);
}

private bool TryParseBindAttribute(BindEntry bindEntry, out string valueAttributeName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ public override void WriteComponent(CodeRenderingContext context, ComponentInter
throw new ArgumentNullException(nameof(node));
}

// We might need a scope for inferring types,
// We might need a scope for inferring types,
CodeWriterExtensions.CSharpCodeWritingScope? typeInferenceCaptureScope = null;
string typeInferenceLocalName = null;

Expand Down Expand Up @@ -632,6 +632,12 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo
throw new ArgumentNullException(nameof(node));
}

// This attribute might only be here in order to allow us to generate code in WritePropertyAccess
if (node.IsDesignTimePropertyAccessHelper())
{
return;
}

// Looks like:
// __o = 17;
context.CodeWriter.Write(DesignTimeVariable);
Expand All @@ -647,7 +653,7 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo
private void WritePropertyAccess(CodeRenderingContext context, ComponentAttributeIntermediateNode node, ComponentIntermediateNode componentNode, string typeInferenceLocalName, bool shouldWriteBL0005Disable, out bool wrotePropertyAccess)
{
wrotePropertyAccess = false;
if (node?.TagHelper?.Name is null || node.Annotations["OriginalAttributeSpan"] is null)
if (node?.TagHelper?.Name is null || node.Annotations[ComponentMetadata.Common.OriginalAttributeSpan] is null)
{
return;
}
Expand Down Expand Up @@ -683,7 +689,7 @@ private void WritePropertyAccess(CodeRenderingContext context, ComponentAttribut
context.CodeWriter.WriteLine("#pragma warning disable BL0005");
}

var attributeSourceSpan = (SourceSpan)node.Annotations["OriginalAttributeSpan"];
var attributeSourceSpan = (SourceSpan)node.Annotations[ComponentMetadata.Common.OriginalAttributeSpan];
attributeSourceSpan = new SourceSpan(attributeSourceSpan.FilePath, attributeSourceSpan.AbsoluteIndex + offset, attributeSourceSpan.LineIndex, attributeSourceSpan.CharacterIndex + offset, node.PropertyName.Length, attributeSourceSpan.LineCount, attributeSourceSpan.CharacterIndex + offset + node.PropertyName.Length);

if (componentNode.TypeInferenceNode == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public static class Common
public const string DirectiveAttribute = "Common.DirectiveAttribute";

public const string AddAttributeMethodName = "Common.AddAttributeMethodName";

public const string OriginalAttributeSpan = "Common.OriginalAttributeSpan";

public const string IsDesignTimePropertyAccessHelper = "Common.IsDesignTimePropertyAccessHelper";
}

public static class Bind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ protected List<TypeInferenceMethodParameter> GetTypeInferenceMethodParameters(Co
{
if (child is ComponentAttributeIntermediateNode attribute)
{
// Some nodes just exist to help with property access at design time, and don't need anything else written
if (child.IsDesignTimePropertyAccessHelper())
{
continue;
}

string typeName;
if (attribute.GloballyQualifiedTypeName != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,11 @@ public override void WriteComponentAttribute(CodeRenderingContext context, Compo
throw new ArgumentNullException(nameof(node));
}

if (node.IsDesignTimePropertyAccessHelper())
{
return;
}

var addAttributeMethod = node.Annotations[ComponentMetadata.Common.AddAttributeMethodName] as string ?? AddComponentParameterMethodName;

// _builder.AddComponentParameter(1, "Foo", 42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHe
IsIndexerNameMatch = indexerMatch,
};

setTagHelperProperty.Annotations.Add("OriginalAttributeSpan", BuildSourceSpanFromNode(node.Name));
setTagHelperProperty.Annotations.Add(ComponentMetadata.Common.OriginalAttributeSpan, BuildSourceSpanFromNode(node.Name));

_builder.Add(setTagHelperProperty);
}
Expand Down Expand Up @@ -1909,7 +1909,7 @@ public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSynta
IsIndexerNameMatch = indexerMatch,
};

setTagHelperProperty.Annotations.Add("OriginalAttributeSpan", BuildSourceSpanFromNode(node.Name));
setTagHelperProperty.Annotations.Add(ComponentMetadata.Common.OriginalAttributeSpan, BuildSourceSpanFromNode(node.Name));

_builder.Push(setTagHelperProperty);
VisitAttributeValue(attributeValueNode);
Expand Down Expand Up @@ -1988,7 +1988,7 @@ public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirec
};
}

attributeNode.Annotations.Add("OriginalAttributeSpan", BuildSourceSpanFromNode(node.Name));
attributeNode.Annotations.Add(ComponentMetadata.Common.OriginalAttributeSpan, BuildSourceSpanFromNode(node.Name));

_builder.Push(attributeNode);
VisitAttributeValue(attributeValueNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public ComponentAttributeIntermediateNode(TagHelperHtmlAttributeIntermediateNode
AttributeStructure = attributeNode.AttributeStructure;
Source = attributeNode.Source;

foreach (var annotation in attributeNode.Annotations)
{
Annotations[annotation.Key] = annotation.Value;
}

for (var i = 0; i < attributeNode.Children.Count; i++)
{
Children.Add(attributeNode.Children[i]);
Expand Down Expand Up @@ -115,6 +120,11 @@ public ComponentAttributeIntermediateNode(TagHelperDirectiveAttributeParameterIn
TagHelper = directiveAttributeParameterNode.TagHelper;
TypeName = directiveAttributeParameterNode.BoundAttributeParameter.TypeName;

foreach (var annotation in directiveAttributeParameterNode.Annotations)
{
Annotations[annotation.Key] = annotation.Value;
}

for (var i = 0; i < directiveAttributeParameterNode.Children.Count; i++)
{
Children.Add(directiveAttributeParameterNode.Children[i]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Components;

namespace Microsoft.AspNetCore.Razor.Language.Intermediate;

Expand All @@ -18,6 +19,13 @@ public static bool IsImported(this IntermediateNode node)
return ReferenceEquals(node.Annotations[CommonAnnotations.Imported], CommonAnnotations.Imported);
}

public static bool IsDesignTimePropertyAccessHelper(this IntermediateNode tagHelper)
{
return tagHelper.Annotations[ComponentMetadata.Common.IsDesignTimePropertyAccessHelper] is string text &&
bool.TryParse(text, out var result) &&
result;
}

public static IReadOnlyList<RazorDiagnostic> GetAllDiagnostics(this IntermediateNode node)
{
if (node == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,71 +354,6 @@ public class MyComponent : ComponentBase
CompileToAssembly(generated);
}

[Fact]
public void Component_WithWriteOnlyParameter()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse("""
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
public int Prop { set { _ = value; } }
}
}
"""));

// Act
var generated = CompileToCSharp("""
<MyComponent Prop="1">
</MyComponent>
""");

// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}

[Fact]
public void Component_WithInitOnlyParameter()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse("""
namespace System.Runtime.CompilerServices;

internal static class IsExternalInit
{
}
"""));
AdditionalSyntaxTrees.Add(Parse("""
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
public int Prop { get; init; }
}
}
"""));

// Act
var generated = CompileToCSharp("""
<MyComponent Prop="1">
</MyComponent>
""");

// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}

[Fact]
public void ComponentWithTypeParameters_WithSemicolon()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
Value

#line default
#line hidden
#nullable disable
= default;
((global::Test.MyComponent)default).
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
Value

#line default
#line hidden
#nullable disable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
ComponentAttribute - (26:0,26 [11] x:\dir\subdir\Test\TestComponent.cshtml) - OnChanged - OnChanged - AttributeStructure.DoubleQuotes
CSharpExpression -
IntermediateToken - - CSharp - __value => ParentValue = __value
ComponentAttribute - (58:0,58 [9] x:\dir\subdir\Test\TestComponent.cshtml) - bind-Value - Value - AttributeStructure.DoubleQuotes
HtmlContent - (58:0,58 [9] x:\dir\subdir\Test\TestComponent.cshtml)
LazyIntermediateToken - (58:0,58 [9] x:\dir\subdir\Test\TestComponent.cshtml) - Html - OnChanged
HtmlContent - (71:0,71 [2] x:\dir\subdir\Test\TestComponent.cshtml)
LazyIntermediateToken - (71:0,71 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
CSharpCode - (80:1,7 [50] x:\dir\subdir\Test\TestComponent.cshtml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ Source Location: (19:0,19 [5] x:\dir\subdir\Test\TestComponent.cshtml)
Generated Location: (1544:40,19 [5] )
|Value|

Source Location: (45:0,45 [5] x:\dir\subdir\Test\TestComponent.cshtml)
|Value|
Generated Location: (1791:49,45 [5] )
|Value|

Source Location: (80:1,7 [50] x:\dir\subdir\Test\TestComponent.cshtml)
|
public int ParentValue { get; set; } = 42;
|
Generated Location: (1957:58,7 [50] )
Generated Location: (2204:67,7 [50] )
|
public int ParentValue { get; set; } = 42;
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
__builder.AddAttribute(-1, "ChildContent", (global::Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
}
));
#pragma warning disable BL0005
((global::Test.MyComponent)default).
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
Value

#line default
#line hidden
#nullable disable
= default;
((global::Test.MyComponent)default).
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
Value

#line default
#line hidden
#nullable disable
= default;
#pragma warning restore BL0005
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
__o = typeof(global::Test.MyComponent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
ComponentAttribute - (30:0,30 [11] x:\dir\subdir\Test\TestComponent.cshtml) - ValueChanged - ValueChanged - AttributeStructure.DoubleQuotes
CSharpExpression -
IntermediateToken - - CSharp - __value => { ParentValue = __value; global::Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.InvokeSynchronousDelegate(Update); }
ComponentAttribute - (62:0,62 [6] x:\dir\subdir\Test\TestComponent.cshtml) - bind-Value - Value - AttributeStructure.DoubleQuotes
LazyIntermediateToken - (62:0,62 [6] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - Update
HtmlContent - (72:0,72 [2] x:\dir\subdir\Test\TestComponent.cshtml)
LazyIntermediateToken - (72:0,72 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
CSharpCode - (81:1,7 [82] x:\dir\subdir\Test\TestComponent.cshtml)
Expand Down
Loading