diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs index cb969d332a5..dee594f9851 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs @@ -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(@" + + +@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(@" +Hello + +@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(@" + + + + +@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(@" + + +@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 diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs index 694a0170602..b758e279d2c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentReferenceCaptureLoweringPass.cs @@ -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; @@ -31,17 +34,23 @@ protected override void ExecuteCore( } var references = documentNode.FindDescendantReferences(); + + // Track field names to avoid generating duplicates + var generatedFields = new HashSet(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 reference) + private static void RewriteUsage( + ClassDeclarationIntermediateNode classNode, + IntermediateNodeReference reference, + HashSet generatedFields) { var (node, parent) = reference; @@ -59,9 +68,65 @@ private static void RewriteUsage(IntermediateNodeReference().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