Skip to content
Closed
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 @@ -65,7 +65,21 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument)
// This will always be null for a component document.
var tagHelperPrefix = visitor.TagHelperPrefix;

descriptors = visitor.Matches.ToArray();
// Merge descriptors with the same full name to deduplicate partial class definitions.
var finalDescriptors = new Dictionary<TagHelperDescriptor, TagHelperDescriptor>(TagHelperDescriptorSimpleComparer.Default);
foreach (var descriptor in visitor.Matches)
{
if (finalDescriptors.TryGetValue(descriptor, out var existing))
{
finalDescriptors[descriptor] = TagHelperDescriptor.Merge(existing, descriptor);
}
else
{
finalDescriptors.Add(descriptor, descriptor);
}
}

descriptors = finalDescriptors.Values.ToArray();

var context = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors);
codeDocument.SetTagHelperContext(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,33 @@ public virtual IEnumerable<RazorDiagnostic> GetAllDiagnostics()
return _allDiagnostics;
}

internal static TagHelperDescriptor Merge(TagHelperDescriptor x, TagHelperDescriptor y)
{
Debug.Assert(x.Kind == y.Kind &&
x.Name == y.Name &&
x.AssemblyName == y.AssemblyName &&
x.DisplayName == y.DisplayName);

var merged = new DefaultTagHelperDescriptor(
kind: x.Kind,
name: x.Name,
assemblyName: x.AssemblyName,
displayName: x.DisplayName,
documentationObject: x.DocumentationObject.Object is null ? y.DocumentationObject : x.DocumentationObject,
tagOutputHint: x.TagOutputHint ?? y.TagOutputHint,
caseSensitive: x.CaseSensitive || y.CaseSensitive,
tagMatchingRules: x.TagMatchingRules.Union(y.TagMatchingRules).ToArray(),
attributeDescriptors: x.BoundAttributes.Union(y.BoundAttributes).ToArray(),
allowedChildTags: x.AllowedChildTags.Union(y.AllowedChildTags).ToArray(),
metadata: MetadataCollection.Create(x.Metadata.Union(y.Metadata).ToArray()),
diagnostics: x.Diagnostics.Union(y.Diagnostics).ToArray());

Debug.Assert(TagHelperDescriptorSimpleComparer.Default.Equals(x, merged) &&
TagHelperDescriptorSimpleComparer.Default.Equals(y, merged));

return merged;
}

public override string ToString()
{
return DisplayName ?? base.ToString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Razor.Language;

/// <summary>
/// Considers two descriptors equal if they are definitions of the same class.
/// </summary>
internal sealed class TagHelperDescriptorSimpleComparer : IEqualityComparer<TagHelperDescriptor>
{
public static readonly TagHelperDescriptorSimpleComparer Default = new();

private TagHelperDescriptorSimpleComparer() { }

public bool Equals(TagHelperDescriptor? x, TagHelperDescriptor? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (x is null || y is null)
{
return false;
}

return x.Kind == y.Kind &&
x.AssemblyName == y.AssemblyName &&
x.Name == y.Name &&
x.IsComponentFullyQualifiedNameMatch() == y.IsComponentFullyQualifiedNameMatch();
}

public int GetHashCode(TagHelperDescriptor obj)
{
var hash = HashCodeCombiner.Start();
hash.Add(obj.Kind, StringComparer.Ordinal);
hash.Add(obj.AssemblyName, StringComparer.Ordinal);
hash.Add(obj.Name, StringComparer.Ordinal);
return hash.CombinedHash;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
Expand Down Expand Up @@ -115,7 +114,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
if (root is CompilationUnitSyntax { Members: [NamespaceDeclarationSyntax { Members: [ClassDeclarationSyntax classSyntax, ..] }, ..] })
{
var declaredClass = compilationWithDeclarations.GetSemanticModel(generatedDeclarationSyntaxTree).GetDeclaredSymbol(classSyntax, ct);
Debug.Assert(declaredClass is null || declaredClass is { AllInterfaces: [{ Name: "IComponent" }, ..] });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken assert - a component does not have to inherit from IComponent, nothing stops users from declaring component like

@inherits object

This manifested in the new test ComponentAndTagHelper where a component inherits from TagHelper.

targetSymbol = declaredClass ?? targetSymbol;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Xunit;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

public sealed class RazorSourceGeneratorComponentTests : RazorSourceGeneratorTestsBase
{
[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task PartialClass()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
<Component2 Param="42" />
""",
["Shared/Component2.razor"] = """
@inherits ComponentBase

Value: @(Param + 1)

@code {
[Parameter]
public int Param { get; set; }
}
"""
}, new()
{
["Component2.razor.cs"] = """
using Microsoft.AspNetCore.Components;

namespace MyApp.Shared;

public partial class Component2 : ComponentBase { }
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});

// Act
var result = RunGenerator(compilation!, ref driver, out compilation);

// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(3, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}

[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task PartialClass_NoBaseInCSharp()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@(await Html.RenderComponentAsync<MyApp.Shared.Component1>(RenderMode.Static))
""",
["Shared/Component1.razor"] = """
<Component2 Param="42" />
""",
["Shared/Component2.razor"] = """
@inherits ComponentBase

Value: @(Param + 1)

@code {
[Parameter]
public int Param { get; set; }
}
"""
}, new()
{
["Component2.razor.cs"] = """
using Microsoft.AspNetCore.Components;

namespace MyApp.Shared;

public partial class Component2 { }
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project, options =>
{
options.TestGlobalOptions["build_property.RazorLangVersion"] = "7.0";
});

// Act
var result = RunGenerator(compilation!, ref driver, out compilation);

// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(3, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;

Expand Down Expand Up @@ -1253,4 +1254,54 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
// Assert
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}

[Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")]
public async Task ComponentAndTagHelper()
{
// Arrange
var project = CreateTestProject(new()
{
["Views/Home/Index.cshtml"] = """
@addTagHelper *, TestProject

<email mail="example">custom tag helper</email>
""",
["Shared/EmailTagHelper.razor"] = """
@inherits ComponentAndTagHelper
@code {
public string? Mail { get; set; }
}
""",
}, new()
{
["EmailTagHelper.cs"] = """
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.Shared;

public abstract class ComponentAndTagHelper : TagHelper
{
protected abstract void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder);
}

public partial class EmailTagHelper : ComponentAndTagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Mail}");
}
}
"""
});
var compilation = await project.GetCompilationAsync();
var driver = await GetDriverAsync(project);

// Act
var result = RunGenerator(compilation!, ref driver, out compilation);

// Assert
Assert.Empty(result.Diagnostics);
Assert.Equal(2, result.GeneratedSources.Length);
await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
Expand Down Expand Up @@ -154,6 +155,14 @@ protected static async Task<string> RenderRazorPageAsync(Compilation compilation
{
RequestServices = app.Services
};
var requestFeature = new HttpRequestFeature
{
Method = HttpMethods.Get,
Protocol = HttpProtocol.Http2,
Scheme = "http"
};
requestFeature.Headers.Host = "localhost";
httpContext.Features.Set<IHttpRequestFeature>(requestFeature);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small improvement of the test infra to allow rendering also Blazor components.

var actionContext = new ActionContext(
httpContext,
new AspNetCore.Routing.RouteData(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Value: 43
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Value: 43
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

<a href="mailto:example">custom tag helper</a>