Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10097,6 +10097,122 @@ @namespace X
CompileToAssembly(generated, expectedDiagnostics);
}

[IntegrationTestFact]
public void Component_WithRef_AutoGeneratedField()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
}
}
"));

// Arrange/Act - No manual field declaration required
var generated = CompileToCSharp(@"
<MyComponent @ref=""myInstance"" />

@code {
public void Foo() { System.GC.KeepAlive(myInstance); }
}
");

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

[IntegrationTestFact]
public void Element_WithRef_AutoGeneratedField()
{
// Arrange/Act - No manual field declaration required
var generated = CompileToCSharp(@"
<elem @ref=""myElem"">Hello</elem>

@code {
public void Foo() { System.GC.KeepAlive(myElem); }
}
");

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

[IntegrationTestFact]
public void Component_WithRef_AutoGeneratedField_MultipleRefs()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
}
}
"));

// Arrange/Act - Multiple refs should all be auto-generated
var generated = CompileToCSharp(@"
<MyComponent @ref=""comp1"" />
<MyComponent @ref=""comp2"" />
<elem @ref=""elem1"" />

@code {
public void Foo()
{
System.GC.KeepAlive(comp1);
System.GC.KeepAlive(comp2);
System.GC.KeepAlive(elem1);
}
}
");

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

[IntegrationTestFact]
public void Component_WithRef_AutoGeneratedField_SkipsExistingField()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Components;

namespace Test
{
public class MyComponent : ComponentBase
{
}
}
"));

// Arrange/Act - If field already exists, don't generate it
var generated = CompileToCSharp(@"
<MyComponent @ref=""myInstance"" />

@code {
private Test.MyComponent myInstance = null!;
public void Foo() { System.GC.KeepAlive(myInstance); }
}
");

// Assert - should not duplicate the field
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}

#endregion

#region Templates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.Components;
Expand Down Expand Up @@ -31,17 +34,23 @@ protected override void ExecuteCore(
}

var references = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeIntermediateNode>();

// Track field names to avoid generating duplicates
var generatedFields = new HashSet<string>(StringComparer.Ordinal);

foreach (var reference in references)
{
if (reference.Node.TagHelper.Kind == TagHelperKind.Ref)
{
RewriteUsage(reference);
RewriteUsage(classNode, reference, generatedFields);
}
}
}

private static void RewriteUsage(IntermediateNodeReference<TagHelperDirectiveAttributeIntermediateNode> reference)
private static void RewriteUsage(
ClassDeclarationIntermediateNode classNode,
IntermediateNodeReference<TagHelperDirectiveAttributeIntermediateNode> reference,
HashSet<string> generatedFields)
{
var (node, parent) = reference;

Expand All @@ -59,9 +68,65 @@ private static void RewriteUsage(IntermediateNodeReference<TagHelperDirectiveAtt
? new ReferenceCaptureIntermediateNode(identifierToken, componentTagHelper.TypeName)
: new ReferenceCaptureIntermediateNode(identifierToken);

// Generate field declaration if it doesn't already exist
var fieldName = identifierToken.Content;
if (!string.IsNullOrWhiteSpace(fieldName) && generatedFields.Add(fieldName))
{
// Check if a field/property with this name already exists in the class
if (!FieldOrPropertyExists(classNode, fieldName))
{
AddFieldDeclaration(classNode, fieldName, referenceCapture.FieldTypeName);
}
}

reference.Replace(referenceCapture);
}

private static bool FieldOrPropertyExists(ClassDeclarationIntermediateNode classNode, string name)
{
foreach (var child in classNode.Children)
{
if (child is FieldDeclarationIntermediateNode field && field.Name == name)
{
return true;
}
if (child is PropertyDeclarationIntermediateNode property && property.Name == name)
{
return true;
}
}
return false;
}

private static void AddFieldDeclaration(ClassDeclarationIntermediateNode classNode, string fieldName, string fieldType)
{
// Find the insertion point: after any existing fields and design-time setup code
var children = classNode.Children;
var index = 0;

// Skip past design-time directives and initial CSharpCode blocks (like #pragma warning)
while (index < children.Count &&
(children[index] is DesignTimeDirectiveIntermediateNode ||
(children[index] is CSharpCodeIntermediateNode code &&
code.Children.OfType<IntermediateToken>().Any(t => t.Content.Contains("#pragma") || t.Content.Contains("__o")))))
{
index++;
}

// Skip past any existing field declarations to maintain ordering
while (index < children.Count && children[index] is FieldDeclarationIntermediateNode)
{
index++;
}

children.Insert(index, new FieldDeclarationIntermediateNode()
{
Modifiers = CommonModifiers.Private,
Name = fieldName,
Type = fieldType,
});
}

private static IntermediateToken? DetermineIdentifierToken(TagHelperDirectiveAttributeIntermediateNode attributeNode)
{
var foundToken = attributeNode.Children switch
Expand Down
Loading