diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs index 2602ff39c660b..13922de8bb3da 100644 --- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -37,6 +37,28 @@ internal static string StripGenericArity(string typeName) return baseName.Length == typeName.Length ? typeName : baseName.ToString(); } + /// + /// Returns the name of the component type a descriptor belongs to. A component descriptor's owning + /// type is itself, but descriptors derived from a component -- child content and bind, for example -- + /// carry the owning component's namespace and identifier while their own + /// is suffixed (e.g. Ns.Card.Header for the Header child content of Ns.Card). + /// Reconstructing the owning name from the namespace and identifier lets ownership filtering keep or + /// exclude a fallback component and all its derived descriptors together, rather than only the + /// component descriptor whose matches exactly. + /// + internal static string GetOwningTypeName(TagHelperDescriptor descriptor) + { + var identifier = descriptor.TypeNameIdentifier; + if (identifier is null) + { + return descriptor.TypeName; + } + + return descriptor.TypeNamespace is { Length: > 0 } typeNamespace + ? typeNamespace + "." + identifier + : identifier; + } + /// /// Returns the hint name for the decl half of a Razor component generated source given /// the impl half's hint name. The decl file substitutes .decl.g.cs for the diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 273ad807cf1bc..26dbc6f6160e2 100644 --- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -222,7 +222,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return TagHelperCollection.Empty; } - return all.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(descriptor.TypeName))); + return all.Where(fallbackTypeNames, static (descriptor, names) => names.Contains(StripGenericArity(GetOwningTypeName(descriptor)))); }) .WithLambdaComparer(static (a, b) => a!.SequenceEqual(b!)) .WithTrackingName("SlowTagHelpers"); @@ -239,7 +239,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var ((fast, slow), fallbackTypeNames) = pair; var fastOwned = fallbackTypeNames.IsEmpty ? fast - : fast.Where(fallbackTypeNames, static (descriptor, names) => !names.Contains(StripGenericArity(descriptor.TypeName))); + : fast.Where(fallbackTypeNames, static (descriptor, names) => !names.Contains(StripGenericArity(GetOwningTypeName(descriptor)))); return TagHelperCollection.Merge(fastOwned, slow); }) .WithTrackingName("TagHelpersFromCompilation"); diff --git a/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs b/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs index 23e2aacbc5057..be0276d7b3638 100644 --- a/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs +++ b/src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs @@ -1329,6 +1329,52 @@ public class MyGrid : ComponentBase Assert.Equal(2, result.GeneratedSources.Length); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/84817")] + public async Task Component_FallbackComponent_ChildContentParameters() + { + // A fallback component (unsplittable via @inherits) exposes RenderFragment child-content + // parameters. A consumer referencing them as child elements must resolve them, i.e. no + // RZ10012 "unexpected name" for Header/ChildContent. + var project = CreateTestProject(new() + { + ["Shared/Card.razor"] = """ + @inherits EmptyBase + +
@Header
+
@ChildContent
+ + @code { + [Parameter] + public RenderFragment? Header { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + } + """, + ["Shared/Consumer.razor"] = """ + +
Expected header
+ Expected content +
+ """, + }, new() + { + ["EmptyBase.cs"] = """ + using Microsoft.AspNetCore.Components; + + public abstract class EmptyBase : ComponentBase; + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver); + + // Assert -- no RZ10012 for the fallback component's child-content parameters + result.Diagnostics.Verify(); + } + [Fact] public async Task Component_WithImplicitContext_NestedInWrapper() {