Skip to content

Commit

Permalink
Match by type instead of name in ComponentTagHelperDescriptorProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Feb 23, 2023
1 parent 23b7552 commit a653a5b
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ internal static class ComponentsApi
public static class ComponentBase
{
public const string Namespace = "Microsoft.AspNetCore.Components";
public const string FullTypeName = Namespace + ".ComponentBase";
public const string MetadataName = FullTypeName;
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsComponentBase;
public const string FullTypeName = MetadataName;

public const string BuildRenderTree = nameof(BuildRenderTree);
}

public static class ParameterAttribute
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
public const string MetadataName = FullTypeName;
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsParameterAttribute;
public const string FullTypeName = MetadataName;
}

public static class LayoutAttribute
Expand All @@ -50,15 +50,15 @@ public static class IDictionary
public static class RenderFragment
{
public const string Namespace = "Microsoft.AspNetCore.Components";
public const string FullTypeName = Namespace + ".RenderFragment";
public const string MetadataName = FullTypeName;
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsRenderFragment;
public const string FullTypeName = MetadataName;
}

public static class RenderFragmentOfT
{
public const string Namespace = "Microsoft.AspNetCore.Components";
public const string FullTypeName = Namespace + ".RenderFragment<>";
public const string MetadataName = Namespace + ".RenderFragment`1";
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsRenderFragment1;
public const string DisplayName = Namespace + ".RenderFragment<TValue>";
}

Expand Down Expand Up @@ -138,15 +138,15 @@ public static class ElementReference

public static class EventCallback
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.EventCallback";
public const string MetadataName = FullTypeName;
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsEventCallback;
public const string FullTypeName = MetadataName;

public const string FactoryAccessor = FullTypeName + ".Factory";
}

public static class EventCallbackOfT
{
public const string MetadataName = "Microsoft.AspNetCore.Components.EventCallback`1";
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsEventCallback1;
public const string DisplayName = "Microsoft.AspNetCore.Components.EventCallback<TValue>";
}

Expand All @@ -164,6 +164,6 @@ public static class BindConverter

public static class CascadingTypeParameterAttribute
{
public const string MetadataName = "Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute";
public const string MetadataName = WellKnownTypeNames.MicrosoftAspNetCoreComponentsCascadingTypeParameterAttribute;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ namespace Microsoft.AspNetCore.Razor.Language;

public static class WellKnownTypeNames
{
public const string MicrosoftAspNetCoreComponentsCascadingTypeParameterAttribute = "Microsoft.AspNetCore.Components.CascadingTypeParameterAttribute";
public const string MicrosoftAspNetCoreComponentsComponentBase = "Microsoft.AspNetCore.Components.ComponentBase";
public const string MicrosoftAspNetCoreComponentsEditorRequiredAttribute = "Microsoft.AspNetCore.Components.EditorRequiredAttribute";
public const string MicrosoftAspNetCoreComponentsEventCallback = "Microsoft.AspNetCore.Components.EventCallback";
public const string MicrosoftAspNetCoreComponentsEventCallback1 = "Microsoft.AspNetCore.Components.EventCallback`1";
public const string MicrosoftAspNetCoreComponentsIComponent = "Microsoft.AspNetCore.Components.IComponent";
public const string MicrosoftAspNetCoreComponentsParameterAttribute = "Microsoft.AspNetCore.Components.ParameterAttribute";
public const string MicrosoftAspNetCoreComponentsRenderFragment = "Microsoft.AspNetCore.Components.RenderFragment";
public const string MicrosoftAspNetCoreComponentsRenderFragment1 = "Microsoft.AspNetCore.Components.RenderFragment`1";
public const string SystemThreadingTasksTask1 = "System.Threading.Tasks.Task`1";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
Expand All @@ -31,15 +32,14 @@ public void Execute(TagHelperDescriptorProviderContext context)
}

var typeProvider = context.GetTypeProvider();
if (typeProvider == null
|| !typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsIComponent, out var icomponentSymbol))
if (typeProvider == null || !ComponentSymbols.TryCreate(typeProvider, out var symbols))
{
// No compilation, nothing to do.
return;
}

var types = new List<INamedTypeSymbol>();
var visitor = new ComponentTypeVisitor(types, icomponentSymbol);
var visitor = new ComponentTypeVisitor(types, symbols.IComponent);

var targetSymbol = context.Items.GetTargetSymbol();
if (targetSymbol is not null)
Expand All @@ -66,9 +66,9 @@ public void Execute(TagHelperDescriptorProviderContext context)
// Components have very simple matching rules.
// 1. The type name (short) matches the tag name.
// 2. The fully qualified name matches the tag name.
var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(type);
var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(symbols, type);
context.Results.Add(shortNameMatchingDescriptor);
var fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(type);
var fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(symbols, type);
context.Results.Add(fullyQualifiedNameMatchingDescriptor);

foreach (var childContent in shortNameMatchingDescriptor.GetChildContentProperties())
Expand All @@ -80,9 +80,9 @@ public void Execute(TagHelperDescriptorProviderContext context)
}
}

private TagHelperDescriptor CreateShortNameMatchingDescriptor(INamedTypeSymbol type)
private TagHelperDescriptor CreateShortNameMatchingDescriptor(ComponentSymbols symbols, INamedTypeSymbol type)
{
var builder = CreateDescriptorBuilder(type);
var builder = CreateDescriptorBuilder(symbols, type);
builder.TagMatchingRule(r =>
{
r.TagName = type.Name;
Expand All @@ -91,9 +91,9 @@ private TagHelperDescriptor CreateShortNameMatchingDescriptor(INamedTypeSymbol t
return builder.Build();
}

private TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor(INamedTypeSymbol type)
private TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor(ComponentSymbols symbols, INamedTypeSymbol type)
{
var builder = CreateDescriptorBuilder(type);
var builder = CreateDescriptorBuilder(symbols, type);
var containingNamespace = type.ContainingNamespace.ToDisplayString();
var fullName = $"{containingNamespace}.{type.Name}";
builder.TagMatchingRule(r =>
Expand All @@ -105,7 +105,7 @@ private TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor(INamedTyp
return builder.Build();
}

private TagHelperDescriptorBuilder CreateDescriptorBuilder(INamedTypeSymbol type)
private TagHelperDescriptorBuilder CreateDescriptorBuilder(ComponentSymbols symbols, INamedTypeSymbol type)
{
var typeName = type.ToDisplayString(SymbolExtensions.FullNameTypeDisplayFormat);
var assemblyName = type.ContainingAssembly.Identity.Name;
Expand All @@ -124,11 +124,20 @@ private TagHelperDescriptorBuilder CreateDescriptorBuilder(INamedTypeSymbol type
{
builder.Metadata[ComponentMetadata.Component.GenericTypedKey] = bool.TrueString;

var cascadeGenericTypeAttributes = type
.GetAttributes()
.Where(a => a.AttributeClass.HasFullName(ComponentsApi.CascadingTypeParameterAttribute.MetadataName))
.Select(attribute => attribute.ConstructorArguments.FirstOrDefault().Value as string)
.ToList();
List<string> cascadeGenericTypeAttributes;
if (symbols.CascadingTypeParameterAttribute is { } cascadingTypeParameterAttribute)
{
cascadeGenericTypeAttributes = type
.GetAttributes()
.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, cascadingTypeParameterAttribute))
.Select(attribute => attribute.ConstructorArguments.FirstOrDefault().Value as string)
.ToList();
}
else
{
cascadeGenericTypeAttributes = new List<string>();
}


for (var i = 0; i < type.TypeArguments.Length; i++)
{
Expand All @@ -147,14 +156,14 @@ private TagHelperDescriptorBuilder CreateDescriptorBuilder(INamedTypeSymbol type
builder.Documentation = xml;
}

foreach (var property in GetProperties(type))
foreach (var property in GetProperties(symbols, type))
{
if (property.kind == PropertyKind.Ignored)
{
continue;
}

CreateProperty(builder, property.property, property.kind);
CreateProperty(symbols, builder, property.property, property.kind);
}

if (builder.BoundAttributes.Any(a => a.IsParameterizedChildContentProperty()) &&
Expand All @@ -169,14 +178,14 @@ private TagHelperDescriptorBuilder CreateDescriptorBuilder(INamedTypeSymbol type
return builder;
}

private void CreateProperty(TagHelperDescriptorBuilder builder, IPropertySymbol property, PropertyKind kind)
private void CreateProperty(ComponentSymbols symbols, TagHelperDescriptorBuilder builder, IPropertySymbol property, PropertyKind kind)
{
builder.BindAttribute(pb =>
{
pb.Name = property.Name;
pb.TypeName = property.Type.ToDisplayString(SymbolExtensions.FullNameTypeDisplayFormat);
pb.SetPropertyName(property.Name);
pb.IsEditorRequired = property.GetAttributes().Any(a => a.AttributeClass.HasFullName("Microsoft.AspNetCore.Components.EditorRequiredAttribute"));
pb.IsEditorRequired = property.GetAttributes().Any(static (a, symbols) => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.EditorRequiredAttribute), symbols);
pb.SetGloballyQualifiedTypeName(property.Type.ToDisplayString(GloballyQualifiedFullNameTypeDisplayFormat));
if (kind == PropertyKind.Enum)
{
Expand Down Expand Up @@ -447,12 +456,12 @@ private void CreateContextParameter(TagHelperDescriptorBuilder builder, string c
// - have the [Parameter] attribute
// - have a setter, even if private
// - are not indexers
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type)
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(ComponentSymbols symbols, INamedTypeSymbol type)
{
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
do
{
if (type.HasFullName(ComponentsApi.ComponentBase.MetadataName))
if (SymbolEqualityComparer.Default.Equals(type, symbols.ComponentBase))
{
// The ComponentBase base class doesn't have any [Parameter].
// Bail out now to avoid walking through its many members, plus the members
Expand Down Expand Up @@ -505,7 +514,7 @@ private void CreateContextParameter(TagHelperDescriptorBuilder builder, string c
kind = PropertyKind.Ignored;
}

if (!property.GetAttributes().Any(a => a.AttributeClass.HasFullName(ComponentsApi.ParameterAttribute.MetadataName)))
if (!property.GetAttributes().Any(static (a, symbols) => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.ParameterAttribute), symbols))
{
if (property.IsOverride)
{
Expand All @@ -523,28 +532,28 @@ private void CreateContextParameter(TagHelperDescriptorBuilder builder, string c
kind = PropertyKind.Enum;
}

if (kind == PropertyKind.Default && property.Type.HasFullName(ComponentsApi.RenderFragment.MetadataName))
if (kind == PropertyKind.Default && SymbolEqualityComparer.Default.Equals(property.Type, symbols.RenderFragment))
{
kind = PropertyKind.ChildContent;
}

if (kind == PropertyKind.Default &&
property.Type is INamedTypeSymbol namedType &&
namedType.IsGenericType &&
namedType.ConstructedFrom.HasFullName(ComponentsApi.RenderFragmentOfT.DisplayName))
SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, symbols.RenderFragmentOfT))
{
kind = PropertyKind.ChildContent;
}

if (kind == PropertyKind.Default && property.Type.HasFullName(ComponentsApi.EventCallback.MetadataName))
if (kind == PropertyKind.Default && SymbolEqualityComparer.Default.Equals(property.Type, symbols.EventCallback))
{
kind = PropertyKind.EventCallback;
}

if (kind == PropertyKind.Default &&
property.Type is INamedTypeSymbol namedType2 &&
namedType2.IsGenericType &&
namedType2.ConstructedFrom.HasFullName(ComponentsApi.EventCallbackOfT.DisplayName))
SymbolEqualityComparer.Default.Equals(namedType2.ConstructedFrom, symbols.EventCallbackOfT))
{
kind = PropertyKind.EventCallback;
}
Expand Down Expand Up @@ -574,6 +583,127 @@ private enum PropertyKind
EventCallback,
}

#nullable restore
private class ComponentSymbols
{
private readonly WellKnownTypeProvider _typeProvider;
private INamedTypeSymbol? _editorRequiredAttribute;

private ComponentSymbols(
WellKnownTypeProvider typeProvider,
INamedTypeSymbol componentBase,
INamedTypeSymbol icomponent,
INamedTypeSymbol parameterAttribute,
INamedTypeSymbol renderFragment,
INamedTypeSymbol renderFragment1,
INamedTypeSymbol eventCallback,
INamedTypeSymbol eventCallback1,
INamedTypeSymbol? cascadingTypeParameterAttribute)
{
_typeProvider = typeProvider;
ComponentBase = componentBase;
IComponent = icomponent;
ParameterAttribute = parameterAttribute;
RenderFragment = renderFragment;
RenderFragmentOfT = renderFragment1;
EventCallback = eventCallback;
EventCallbackOfT = eventCallback1;
CascadingTypeParameterAttribute = cascadingTypeParameterAttribute;
}

public static bool TryCreate(WellKnownTypeProvider typeProvider, [NotNullWhen(true)] out ComponentSymbols? symbols)
{
// We find a bunch of important and fundamental types here that are needed to discover
// components. If one of these isn't defined then we just bail, because the results will
// be unpredictable.
symbols = null;

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsComponentBase, out var componentBase))
{
// No definition for ComponentBase, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsIComponent, out var icomponent))
{
// No definition for IComponent, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsParameterAttribute, out var parameterAttribute))
{
// No definition for [Parameter], nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsRenderFragment, out var renderFragment))
{
// No definition for RenderFragment, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsRenderFragment1, out var renderFragment1))
{
// No definition for RenderFragment<T>, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsEventCallback, out var eventCallback))
{
// No definition for EventCallback, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsEventCallback1, out var eventCallback1))
{
// No definition for EventCallback<T>, nothing to do.
return false;
}

if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsCascadingTypeParameterAttribute, out var cascadingTypeParameterAttribute))
{
// No definition for [CascadingTypeParameter]. For back-compat, just don't activate this feature.
}

symbols = new ComponentSymbols(
typeProvider,
componentBase: componentBase,
icomponent: icomponent,
parameterAttribute: parameterAttribute,
renderFragment: renderFragment,
renderFragment1: renderFragment1,
eventCallback: eventCallback,
eventCallback1: eventCallback1,
cascadingTypeParameterAttribute: cascadingTypeParameterAttribute);
return true;
}

public INamedTypeSymbol ComponentBase { get; }

public INamedTypeSymbol IComponent { get; }

public INamedTypeSymbol ParameterAttribute { get; }

public INamedTypeSymbol RenderFragment { get; }

public INamedTypeSymbol RenderFragmentOfT { get; }

public INamedTypeSymbol EventCallback { get; }

public INamedTypeSymbol EventCallbackOfT { get; }

public INamedTypeSymbol? CascadingTypeParameterAttribute { get; }

public INamedTypeSymbol? EditorRequiredAttribute
{
get
{
return _editorRequiredAttribute ??= _typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftAspNetCoreComponentsEditorRequiredAttribute);
}
}
}
#nullable disable

private class ComponentTypeVisitor : SymbolVisitor
{
private readonly List<INamedTypeSymbol> _results;
Expand Down
Loading

0 comments on commit a653a5b

Please sign in to comment.